@astrosheep/square 0.3.29 → 0.3.31
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/claude-plugin/hooks/hooks.json +0 -3
- package/claude-plugin/skills/square/SKILL.md +8 -7
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +7 -3
- package/dist/artifact.js +7 -50
- package/dist/automatic-session.js +31 -14
- package/dist/boundary-presentation.d.ts +1 -1
- package/dist/boundary-presentation.js +58 -14
- package/dist/catch-decisions.d.ts +17 -0
- package/dist/catch-decisions.js +53 -0
- package/dist/claude-hook.d.ts +1 -1
- package/dist/cli/context.js +10 -2
- package/dist/cli/observation-commands.d.ts +5 -1
- package/dist/cli/observation-commands.js +68 -33
- package/dist/cli/square-commands.js +17 -10
- package/dist/codex-hook.d.ts +1 -1
- package/dist/decisions.js +2 -2
- package/dist/delivery-health.d.ts +1 -1
- package/dist/delivery-operations.d.ts +25 -0
- package/dist/delivery-operations.js +124 -0
- package/dist/help.js +1 -1
- package/dist/host-ledger-file-adapter.d.ts +34 -0
- package/dist/host-ledger-file-adapter.js +165 -0
- package/dist/host-ledger.d.ts +160 -0
- package/dist/host-ledger.js +1 -0
- package/dist/inbox.d.ts +2 -2
- package/dist/inbox.js +22 -14
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/landing.d.ts +18 -14
- package/dist/landing.js +29 -113
- package/dist/model.d.ts +6 -17
- package/dist/notifications.d.ts +6 -10
- package/dist/notifications.js +71 -192
- package/dist/open-square.d.ts +4 -4
- package/dist/open-square.js +1 -1
- package/dist/ports.d.ts +127 -0
- package/dist/ports.js +1 -0
- package/dist/presence.d.ts +1 -2
- package/dist/presence.js +13 -46
- package/dist/presentation-operations.d.ts +3 -0
- package/dist/presentation-operations.js +51 -0
- package/dist/presentation.d.ts +2 -2
- package/dist/presentation.js +11 -7
- package/dist/presented.d.ts +1 -12
- package/dist/presented.js +6 -75
- package/dist/registry.d.ts +10 -10
- package/dist/registry.js +48 -113
- package/dist/routes.d.ts +6 -33
- package/dist/routes.js +6 -170
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +3 -4
- package/dist/square-actions.d.ts +32 -0
- package/dist/square-actions.js +167 -0
- package/dist/square-facade.d.ts +7 -7
- package/dist/square-file-adapter.d.ts +4 -3
- package/dist/square-file-adapter.js +22 -5
- package/dist/square-projections.d.ts +68 -0
- package/dist/square-projections.js +87 -0
- package/dist/square-storage.d.ts +2 -2
- package/dist/square-storage.js +14 -9
- package/dist/square-wiring.d.ts +3 -3
- package/dist/square-wiring.js +44 -29
- package/dist/views.d.ts +8 -2
- package/dist/views.js +28 -24
- package/dist/wake-attempts.d.ts +29 -22
- package/dist/wake-attempts.js +36 -119
- package/dist/wake-evidence.d.ts +6 -18
- package/dist/wake-evidence.js +17 -80
- package/dist/wakes.d.ts +2 -16
- package/dist/wakes.js +7 -27
- package/dist/watch.js +2 -3
- package/extensions/square-pi.js +7 -2
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +2 -2
- package/skills/square/SKILL.md +8 -7
package/dist/presence.js
CHANGED
|
@@ -1,49 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { deliveryDelta, directedPeerSays, matchesFeedFilter } from './activity-feed.js';
|
|
3
|
-
import { deriveDeliveryModel, markSeenNotifications } from './delivery.js';
|
|
4
|
-
import { SquareError } from './model.js';
|
|
1
|
+
import { deriveDeliveryModel } from './delivery.js';
|
|
5
2
|
import { openSquare } from './square-file-adapter.js';
|
|
6
3
|
import { closeOpenSquare } from './open-square.js';
|
|
7
|
-
import { resolveKnownName } from './decisions.js';
|
|
8
4
|
import { recordObservation } from './runtime.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (perception === 'full' || !('body' in result))
|
|
15
|
-
return { ...result, perception };
|
|
16
|
-
const { body: _body, ...withoutBody } = result;
|
|
17
|
-
return { ...withoutBody, perception };
|
|
5
|
+
import { catchUp as actionCatchUp } from './square-actions.js';
|
|
6
|
+
function operationContext(square) {
|
|
7
|
+
return 'artifact' in square
|
|
8
|
+
? { artifact: square.artifact, clock: square.clock }
|
|
9
|
+
: { artifact: square.cell, clock: square.clock };
|
|
18
10
|
}
|
|
19
11
|
export async function catchUp(square, name, options = {}, deriveDelivery = deriveDeliveryModel) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
while (true) {
|
|
25
|
-
const attempt = await square.cell.transact((state, version) => {
|
|
26
|
-
const at = square.clock();
|
|
27
|
-
const viewer = resolveKnownName(state, name);
|
|
28
|
-
const delivery = deriveDelivery(state);
|
|
29
|
-
const delta = deliveryDelta(state, viewer, delivery);
|
|
30
|
-
const filter = { ...(options.from === undefined ? {} : { participants: [...options.from] }), ...(options.mention === true ? { mention: viewer } : {}) };
|
|
31
|
-
const delivered = directedPeerSays(state, delta, viewer, delivery).filter((activity) => matchesFeedFilter(activity, filter))
|
|
32
|
-
.filter((activity, index, activities) => activities.findIndex((candidate) => candidate.index === activity.index) === index)
|
|
33
|
-
.sort((left, right) => left.index - right.index);
|
|
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 } } };
|
|
37
|
-
});
|
|
38
|
-
if (attempt.caught.activities.length > 0 || idle === 0)
|
|
39
|
-
return attempt.caught;
|
|
40
|
-
const remaining = deadline - Date.now();
|
|
41
|
-
if (remaining <= 0 || !await square.cell.changed(attempt.version, remaining))
|
|
42
|
-
return { ...attempt.caught, idleExpired: true };
|
|
43
|
-
}
|
|
12
|
+
const project = deriveDelivery === deriveDeliveryModel
|
|
13
|
+
? undefined
|
|
14
|
+
: (state) => deriveDelivery(state);
|
|
15
|
+
return actionCatchUp(operationContext(square), name, options, project);
|
|
44
16
|
}
|
|
45
17
|
/** Commit seen only for complete, actually rendered boundary bodies. */
|
|
46
|
-
export async function markBoundarySeen(squarePath, name,
|
|
18
|
+
export async function markBoundarySeen(squarePath, name, actIndexes, at = Date.now()) {
|
|
47
19
|
let square;
|
|
48
20
|
try {
|
|
49
21
|
square = await openSquare(squarePath);
|
|
@@ -52,10 +24,10 @@ export async function markBoundarySeen(squarePath, name, ownerId, actIndexes, at
|
|
|
52
24
|
return;
|
|
53
25
|
}
|
|
54
26
|
try {
|
|
55
|
-
await square.
|
|
27
|
+
await square.artifact.transact((state) => {
|
|
56
28
|
let changed = false;
|
|
57
29
|
for (const index of actIndexes)
|
|
58
|
-
changed = recordObservation(state, name, index, 'seen', at
|
|
30
|
+
changed = recordObservation(state, name, index, 'seen', at) || changed;
|
|
59
31
|
return changed ? { state, result: undefined } : { result: undefined };
|
|
60
32
|
});
|
|
61
33
|
}
|
|
@@ -63,8 +35,3 @@ export async function markBoundarySeen(squarePath, name, ownerId, actIndexes, at
|
|
|
63
35
|
await closeOpenSquare(square);
|
|
64
36
|
}
|
|
65
37
|
}
|
|
66
|
-
export async function markNotificationNotified(square, name, actIndex, ownerId, at = Date.now()) {
|
|
67
|
-
await square.cell.transact((state) => recordObservation(state, name, actIndex, 'notified', at, ownerId)
|
|
68
|
-
? { state, result: undefined }
|
|
69
|
-
: { result: undefined });
|
|
70
|
-
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
2
|
+
import { deriveDeliveryModel } from './delivery.js';
|
|
3
|
+
import { recordObservation } from './runtime.js';
|
|
4
|
+
/** Present one pending activity; artifact seen remains the authoritative receipt. */
|
|
5
|
+
export async function presentPending(input) {
|
|
6
|
+
const index = typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
|
|
7
|
+
if (index === undefined)
|
|
8
|
+
return { presented: false };
|
|
9
|
+
let before;
|
|
10
|
+
try {
|
|
11
|
+
before = await input.artifact.read();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return { presented: false };
|
|
15
|
+
}
|
|
16
|
+
const delivery = deriveDeliveryModel(before.state);
|
|
17
|
+
const item = before.state.acts.find((activity) => activity.index === index);
|
|
18
|
+
if (item === undefined || delivery.isSeen(input.participant, index) || !delivery.pendingFor(input.participant).some((notification) => notification.item.index === index))
|
|
19
|
+
return { presented: false };
|
|
20
|
+
if (input.hostLedger !== undefined && input.session !== undefined) {
|
|
21
|
+
const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', leaseMs: input.timeoutMs ?? 5000, now: input.now });
|
|
22
|
+
if (claim.status !== 'acquired')
|
|
23
|
+
return { presented: false };
|
|
24
|
+
let current;
|
|
25
|
+
try {
|
|
26
|
+
current = await input.artifact.read();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', now: input.now }).catch(() => undefined);
|
|
30
|
+
return { presented: false };
|
|
31
|
+
}
|
|
32
|
+
if (deriveDeliveryModel(current.state).isSeen(input.participant, index)) {
|
|
33
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', now: input.now }).catch(() => undefined);
|
|
34
|
+
return { presented: false };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
await input.sink.present(item);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
await input.hostLedger.appendEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', outcome: 'failed', message: error instanceof Error ? error.message : String(error), at: input.now });
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
await input.sink.present(item);
|
|
46
|
+
if (input.markSeen !== false)
|
|
47
|
+
await input.artifact.transact((state) => { const changed = recordObservation(state, input.participant, index, 'seen', input.now ?? Date.now()); return changed ? { state, result: undefined } : { result: undefined }; });
|
|
48
|
+
if (input.hostLedger !== undefined && input.session !== undefined)
|
|
49
|
+
await input.hostLedger.appendEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', outcome: input.markSeen === false ? 'clipped' : 'presented', ...(input.markSeen === false ? { message: 'presentation clipped' } : {}), at: input.now });
|
|
50
|
+
return { presented: true, activity: item };
|
|
51
|
+
}
|
package/dist/presentation.d.ts
CHANGED
|
@@ -76,8 +76,8 @@ export declare function renderExpressWaiting(opts: ExpressWaitingOptions): strin
|
|
|
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
78
|
export declare function renderPresenceAnchor(names: readonly string[]): string;
|
|
79
|
-
export declare function renderActivitiesView(squareState: SquareState, visible: StoredAct[], lastN: number | null | undefined,
|
|
80
|
-
export declare function renderGrepActivitiesView(visible: StoredAct[], totalMatches: number,
|
|
79
|
+
export declare function renderActivitiesView(squareState: SquareState, visible: StoredAct[], lastN: number | null | undefined, noTruncate: boolean | undefined, squarePath: string, viewer?: string, mode?: 'ambient' | 'archive'): string;
|
|
80
|
+
export declare function renderGrepActivitiesView(visible: StoredAct[], totalMatches: number, noTruncate: boolean | undefined, squarePath: string, pattern: string, fixed?: boolean, perception?: (act: StoredAct) => Perception): string;
|
|
81
81
|
export declare function renderActivityLimit(opts: ActivityLimitOptions): string;
|
|
82
82
|
export declare function renderWatchAlreadyActive(opts: ParticipantOutputOptions): string;
|
|
83
83
|
export declare function renderWatchForceTakeover(_opts: ParticipantOutputOptions): string;
|
package/dist/presentation.js
CHANGED
|
@@ -295,10 +295,10 @@ export function renderPresenceAnchor(names) {
|
|
|
295
295
|
const participants = names.map((name) => participantIdentity(name)).join(', ');
|
|
296
296
|
return names.length === 1 ? `→ ${participants} was here` : `→ ${participants} were here`;
|
|
297
297
|
}
|
|
298
|
-
export function renderActivitiesView(squareState, visible, lastN,
|
|
298
|
+
export function renderActivitiesView(squareState, visible, lastN, noTruncate, squarePath, viewer = '', mode = 'ambient') {
|
|
299
299
|
const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
|
|
300
300
|
const shown = lastN == null ? publicVisible : publicVisible.slice(-lastN);
|
|
301
|
-
const previewLen =
|
|
301
|
+
const previewLen = noTruncate ? undefined : BODY_PREVIEW_LENGTH;
|
|
302
302
|
const markers = new Map();
|
|
303
303
|
for (const participant of rosterNames(squareState)) {
|
|
304
304
|
const anchor = lastPresenceAnchor(squareState, participant);
|
|
@@ -325,7 +325,7 @@ export function renderActivitiesView(squareState, visible, lastN, full, squarePa
|
|
|
325
325
|
if (previewLen !== undefined) {
|
|
326
326
|
const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen && (mode === 'archive' || perceiveActivity(squareState, act, viewer) === 'full'));
|
|
327
327
|
if (truncated)
|
|
328
|
-
chunks.push(`» ${commandPrefix(squarePath)} history --
|
|
328
|
+
chunks.push(`» ${commandPrefix(squarePath)} history --no-truncate`);
|
|
329
329
|
}
|
|
330
330
|
return chunks.join('\n\n');
|
|
331
331
|
}
|
|
@@ -335,7 +335,7 @@ function highlightGrepMatch(text) {
|
|
|
335
335
|
return text;
|
|
336
336
|
return `\x1b[38;5;222m\x1b[1m${text}\x1b[0m`;
|
|
337
337
|
}
|
|
338
|
-
export function renderGrepActivitiesView(visible, totalMatches,
|
|
338
|
+
export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squarePath, pattern, fixed = false, perception) {
|
|
339
339
|
const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
|
|
340
340
|
if (totalMatches === 0)
|
|
341
341
|
return `○ no activity matched ${quoteShell(pattern)}`;
|
|
@@ -344,7 +344,11 @@ export function renderGrepActivitiesView(visible, totalMatches, full, squarePath
|
|
|
344
344
|
let truncated = false;
|
|
345
345
|
for (const act of publicVisible) {
|
|
346
346
|
const rawBody = act.body ?? '';
|
|
347
|
-
if (
|
|
347
|
+
if (perception?.(act) === 'presence') {
|
|
348
|
+
chunks.push(`${actId(act.index)} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${formatTimestamp(act.at)}`);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (noTruncate === true) {
|
|
348
352
|
const body = rawBody.split('\n').map((line) => ` ${line}`).join('\n');
|
|
349
353
|
chunks.push(`${actId(act.index)} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${formatTimestamp(act.at)}\n${body}`);
|
|
350
354
|
continue;
|
|
@@ -365,10 +369,10 @@ export function renderGrepActivitiesView(visible, totalMatches, full, squarePath
|
|
|
365
369
|
chunks.push(`${actId(act.index)} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${formatTimestamp(act.at)}\n ${text.trim()}${omitted}`);
|
|
366
370
|
}
|
|
367
371
|
if (publicVisible.length === 1) {
|
|
368
|
-
chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --
|
|
372
|
+
chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --no-truncate' : ''}`);
|
|
369
373
|
}
|
|
370
374
|
else if (truncated && publicVisible.length > 1) {
|
|
371
|
-
chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2 --
|
|
375
|
+
chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2 --no-truncate`);
|
|
372
376
|
}
|
|
373
377
|
return chunks.join('\n\n');
|
|
374
378
|
}
|
package/dist/presented.d.ts
CHANGED
|
@@ -1,13 +1,2 @@
|
|
|
1
|
-
import { type InboxMembership } from './model.js';
|
|
2
|
-
export interface PresentedAttention {
|
|
3
|
-
ownerId: string;
|
|
4
|
-
squarePath: string;
|
|
5
|
-
name: string;
|
|
6
|
-
actIndex: number;
|
|
7
|
-
}
|
|
8
1
|
export declare function presentedPath(env?: NodeJS.ProcessEnv): string;
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function hasPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): Promise<boolean>;
|
|
11
|
-
export declare function hasPresentedAttention(squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): Promise<boolean>;
|
|
12
|
-
export declare function recordPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, at?: number): Promise<void>;
|
|
13
|
-
export declare function presentOnce<T>(sessionId: string, lookup: (sessionId: string) => InboxMembership[] | Promise<InboxMembership[]>, deliver: (inbox: InboxMembership[]) => T | Promise<T>, env?: NodeJS.ProcessEnv, at?: number, signal?: AbortSignal): Promise<T | undefined>;
|
|
2
|
+
export declare function hasPresentedForOwner(sessionId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): Promise<boolean>;
|
package/dist/presented.js
CHANGED
|
@@ -1,78 +1,9 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
1
|
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
-
const LOCK_STALE_MS = 5 * 60_000;
|
|
10
|
-
const LOCK_RETRY_MS = 10;
|
|
3
|
+
import { canonicalSquarePath } from './registry.js';
|
|
4
|
+
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
5
|
+
import { formatActivityId } from './square-core.js';
|
|
6
|
+
import { projectPresentationEvidence } from './square-projections.js';
|
|
11
7
|
export function presentedPath(env = process.env) { return env.SQUARE_PRESENTED || path.join(os.homedir(), '.square', 'presented.ndjsonl'); }
|
|
12
|
-
|
|
13
|
-
async function
|
|
14
|
-
let text;
|
|
15
|
-
try {
|
|
16
|
-
text = await fs.readFile(filePath, 'utf8');
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
if (error.code === 'ENOENT')
|
|
20
|
-
return [];
|
|
21
|
-
throw error;
|
|
22
|
-
}
|
|
23
|
-
const cutoff = now - RETENTION_MS;
|
|
24
|
-
const rows = [];
|
|
25
|
-
for (const line of text.split('\n')) {
|
|
26
|
-
if (!line.trim())
|
|
27
|
-
continue;
|
|
28
|
-
try {
|
|
29
|
-
const parsed = JSON.parse(line);
|
|
30
|
-
if (parsed.v !== 2 || typeof parsed.ts !== 'number' || !Number.isFinite(parsed.ts) || typeof parsed.owner_id !== 'string' || typeof parsed.square_path !== 'string' || typeof parsed.name !== 'string' || typeof parsed.act_index !== 'number' || parsed.ts < cutoff)
|
|
31
|
-
continue;
|
|
32
|
-
rows.push(parsed);
|
|
33
|
-
}
|
|
34
|
-
catch { }
|
|
35
|
-
}
|
|
36
|
-
return rows;
|
|
37
|
-
}
|
|
38
|
-
export async function readPresentedAttentions(env = process.env, now = Date.now()) { return Promise.all((await readRows(presentedPath(env), now)).map(async (row) => ({ ownerId: row.owner_id, squarePath: await canonicalSquarePath(row.square_path), name: row.name, actIndex: row.act_index }))); }
|
|
39
|
-
async function writeRows(filePath, rows) { await fs.mkdir(path.dirname(filePath), { recursive: true }); const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`; await fs.writeFile(temp, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), { mode: 0o600 }); await fs.rename(temp, filePath); }
|
|
40
|
-
async function membershipKey(membership) { return `${await canonicalSquarePath(membership.squarePath)}\u0000${membership.name.toLocaleLowerCase()}`; }
|
|
41
|
-
async function attentionLockPath(filePath, membership) { return `${filePath}.${createHash('sha256').update(await membershipKey(membership)).digest('hex')}.lock`; }
|
|
42
|
-
async function withAttentionLocks(filePath, inbox, fn, signal) { const lockPaths = [...new Set(await Promise.all(inbox.map((membership) => attentionLockPath(filePath, membership))))].sort(); async function acquire(index) { if (signal?.aborted)
|
|
43
|
-
throw signal.reason ?? new Error('Presentation aborted'); if (index >= lockPaths.length)
|
|
44
|
-
return fn(); return withFileLock(lockPaths[index], { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS, signal }, () => acquire(index + 1)); } return acquire(0); }
|
|
45
|
-
async function ownerFor(sessionId, membership) { const squarePath = await canonicalSquarePath(membership.squarePath); const binding = (await lookupSessionBindings(sessionId)).find((candidate) => candidate.squarePath === squarePath && sameName(candidate.name, membership.name)); return binding?.ownerId ?? `session:${sessionId}`; }
|
|
46
|
-
async function selectUnpresented(sessionId, inbox, rows) { const known = new Set(await Promise.all(rows.map(rowKey))); const selected = []; for (const membership of inbox) {
|
|
47
|
-
const ownerId = await ownerFor(sessionId, membership);
|
|
48
|
-
const notifications = [];
|
|
49
|
-
for (const notification of membership.notifications) {
|
|
50
|
-
if (!known.has(await rowKey({ owner_id: ownerId, square_path: membership.squarePath, name: membership.name, act_index: notification.actIndex })))
|
|
51
|
-
notifications.push(notification);
|
|
52
|
-
}
|
|
53
|
-
if (notifications.length > 0)
|
|
54
|
-
selected.push({ membership: { ...membership, notifications }, ownerId });
|
|
55
|
-
} return selected; }
|
|
56
|
-
export async function hasPresentedForOwner(ownerId, squarePath, name, actIndex, env = process.env, now = Date.now()) { const resolved = await canonicalSquarePath(squarePath); for (const row of await readRows(presentedPath(env), now))
|
|
57
|
-
if (row.owner_id === ownerId && await canonicalSquarePath(row.square_path) === resolved && sameName(row.name, name) && row.act_index === actIndex)
|
|
58
|
-
return true; return false; }
|
|
59
|
-
export async function hasPresentedAttention(squarePath, name, actIndex, env = process.env, now = Date.now()) { const ownerIds = new Set((await lookupParticipant(squarePath, name, now)).map((binding) => binding.ownerId)); for (const ownerId of ownerIds)
|
|
60
|
-
if (await hasPresentedForOwner(ownerId, squarePath, name, actIndex, env, now))
|
|
61
|
-
return true; return false; }
|
|
62
|
-
export async function recordPresentedForOwner(ownerId, squarePath, name, actIndex, env = process.env, at = Date.now()) { const filePath = presentedPath(env); const row = { v: 2, ts: at, owner_id: ownerId, square_path: await canonicalSquarePath(squarePath), name, act_index: actIndex }; await withAttentionLocks(filePath, [{ name, squarePath, notifications: [] }], () => withFileLock(`${filePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, async () => { const rows = await readRows(filePath, at); const known = new Set(await Promise.all(rows.map(rowKey))); if (!known.has(await rowKey(row)))
|
|
63
|
-
await writeRows(filePath, [...rows, row]); })); }
|
|
64
|
-
export async function presentOnce(sessionId, lookup, deliver, env = process.env, at = Date.now(), signal) { const filePath = presentedPath(env); const initial = (await lookup(sessionId)).filter((membership) => membership.notifications.length > 0); if (initial.length === 0)
|
|
65
|
-
return undefined; const lockedMemberships = new Set(await Promise.all(initial.map(membershipKey))); return withAttentionLocks(filePath, initial, async () => { const current = []; for (const membership of await lookup(sessionId))
|
|
66
|
-
if (lockedMemberships.has(await membershipKey(membership)))
|
|
67
|
-
current.push(membership); const selected = await selectUnpresented(sessionId, current, await readRows(filePath, at)); if (selected.length === 0)
|
|
68
|
-
return undefined; if (signal?.aborted)
|
|
69
|
-
throw signal.reason ?? new Error('Presentation aborted'); const result = await deliver(selected.map(({ membership }) => membership)); if (signal?.aborted)
|
|
70
|
-
throw signal.reason ?? new Error('Presentation aborted'); await withFileLock(`${filePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS, signal }, async () => { const rows = await readRows(filePath, at); const known = new Set(await Promise.all(rows.map(rowKey))); for (const { membership, ownerId } of selected)
|
|
71
|
-
for (const notification of membership.notifications) {
|
|
72
|
-
const row = { v: 2, ts: at, owner_id: ownerId, square_path: await canonicalSquarePath(membership.squarePath), name: membership.name, act_index: notification.actIndex };
|
|
73
|
-
const key = await rowKey(row);
|
|
74
|
-
if (!known.has(key)) {
|
|
75
|
-
rows.push(row);
|
|
76
|
-
known.add(key);
|
|
77
|
-
}
|
|
78
|
-
} await writeRows(filePath, rows); }); return result; }, signal); }
|
|
8
|
+
function evidence(env = process.env) { const file = presentedPath(env); return createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? path.dirname(file), writableScope: 'user', readableScopes: ['user'] }); }
|
|
9
|
+
export async function hasPresentedForOwner(sessionId, squarePath, name, actIndex, env = process.env, now = Date.now()) { const resolved = await canonicalSquarePath(squarePath); return (await projectPresentationEvidence({ hostLedger: evidence(env), now, location: resolved, participant: name, sessionId, activity: formatActivityId(actIndex) })).some((row) => row.outcome === 'presented'); }
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Machine-local participant discovery cache. */
|
|
2
2
|
import { type StoredAct } from './model.js';
|
|
3
|
+
import type { PresenceRecord } from './host-ledger.js';
|
|
3
4
|
export type SessionChannel = 'claude-code' | 'codex' | 'opencode' | 'pi' | 'paseo' | 'unknown';
|
|
4
5
|
export interface RegistryBinding {
|
|
5
6
|
sessionId: string;
|
|
@@ -7,34 +8,33 @@ export interface RegistryBinding {
|
|
|
7
8
|
squarePath: string;
|
|
8
9
|
channel: SessionChannel;
|
|
9
10
|
child: boolean;
|
|
10
|
-
|
|
11
|
-
ownerId: string;
|
|
11
|
+
route?: PresenceRecord['route'];
|
|
12
12
|
updatedAt: number;
|
|
13
13
|
}
|
|
14
14
|
export interface RegistryWriteOptions {
|
|
15
15
|
channel?: SessionChannel;
|
|
16
16
|
child?: boolean;
|
|
17
|
-
|
|
18
|
-
ownerId?: string;
|
|
17
|
+
route?: PresenceRecord['route'];
|
|
19
18
|
at?: number;
|
|
19
|
+
env?: NodeJS.ProcessEnv;
|
|
20
20
|
}
|
|
21
|
-
export declare function registryPath(): string;
|
|
21
|
+
export declare function registryPath(env?: NodeJS.ProcessEnv): string;
|
|
22
22
|
export declare function canonicalSquarePath(squarePath: string): Promise<string>;
|
|
23
23
|
export declare function recordJoin(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): Promise<void>;
|
|
24
24
|
export declare function recordDone(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): Promise<void>;
|
|
25
|
-
export declare function readActiveBindings(now?: number): Promise<RegistryBinding[]>;
|
|
26
|
-
export declare function lookupSessionBindings(sessionId: string, now?: number): Promise<RegistryBinding[]>;
|
|
27
|
-
export declare function lookupSession(sessionId: string, now?: number): Promise<Array<{
|
|
25
|
+
export declare function readActiveBindings(now?: number, env?: NodeJS.ProcessEnv): Promise<RegistryBinding[]>;
|
|
26
|
+
export declare function lookupSessionBindings(sessionId: string, now?: number, env?: NodeJS.ProcessEnv): Promise<RegistryBinding[]>;
|
|
27
|
+
export declare function lookupSession(sessionId: string, now?: number, env?: NodeJS.ProcessEnv): Promise<Array<{
|
|
28
28
|
name: string;
|
|
29
29
|
squarePath: string;
|
|
30
30
|
}>>;
|
|
31
|
-
export declare function lookupParticipant(squarePath: string, name: string, now?: number): Promise<RegistryBinding[]>;
|
|
31
|
+
export declare function lookupParticipant(squarePath: string, name: string, now?: number, env?: NodeJS.ProcessEnv): Promise<RegistryBinding[]>;
|
|
32
32
|
export declare function localParticipantOwner(squarePath: string, name: string, env?: NodeJS.ProcessEnv, now?: number): Promise<string | undefined>;
|
|
33
33
|
export declare function localParticipantName(squarePath: string, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
|
|
34
34
|
export declare function squareAssignedParticipantName(env?: NodeJS.ProcessEnv): string | undefined;
|
|
35
35
|
export type CurrentParticipantBinding = Readonly<{
|
|
36
36
|
created: boolean;
|
|
37
|
-
|
|
37
|
+
sessionId: string;
|
|
38
38
|
}>;
|
|
39
39
|
export declare function bindCurrentParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): Promise<CurrentParticipantBinding>;
|
|
40
40
|
export declare function unbindCurrentParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
|
package/dist/registry.js
CHANGED
|
@@ -1,145 +1,80 @@
|
|
|
1
1
|
/** Machine-local participant discovery cache. */
|
|
2
|
-
import { promises as fs } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { homedir } from 'node:os';
|
|
5
|
-
import {
|
|
6
|
-
import { nameKey, sameName, SquareError } from './model.js';
|
|
4
|
+
import { sameName, SquareError } from './model.js';
|
|
7
5
|
import { isCurrentlyJoined } from './runtime.js';
|
|
8
|
-
import { publishWakeRoutes, retireOwnerWakeRoutes } from './routes.js';
|
|
9
6
|
import { squareAssignedParticipantName as computeSquareAssignedParticipantName } from './participant-identity.js';
|
|
7
|
+
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
10
8
|
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
|
-
const COMPACT_BYTES = 64 * 1024;
|
|
12
|
-
const COMPACT_LINES = 1000;
|
|
13
|
-
const VALID_CHANNELS = new Set(['claude-code', 'codex', 'opencode', 'pi', 'paseo', 'unknown']);
|
|
14
9
|
const LOCAL_SESSION_SOURCES = [
|
|
15
10
|
{ variable: 'CLAUDE_CODE_SESSION_ID', channel: 'claude-code', child: 'CLAUDE_CODE_CHILD_SESSION' },
|
|
16
11
|
{ variable: 'CODEX_THREAD_ID', channel: 'codex' },
|
|
17
12
|
{ variable: 'OPENCODE_SESSION_ID', channel: 'opencode' },
|
|
18
13
|
{ variable: 'SQUARE_PI_SESSION_ID', channel: 'pi' },
|
|
19
14
|
];
|
|
20
|
-
export function registryPath() { return
|
|
15
|
+
export function registryPath(env = process.env) { return env.SQUARE_REGISTRY || path.join(homedir(), '.square', 'sessions.ndjsonl'); }
|
|
21
16
|
export async function canonicalSquarePath(squarePath) { const absolute = path.resolve(squarePath); try {
|
|
22
|
-
return await fs.realpath(absolute);
|
|
17
|
+
return await (await import('node:fs/promises')).realpath(absolute);
|
|
23
18
|
}
|
|
24
19
|
catch {
|
|
25
20
|
return absolute;
|
|
26
21
|
} }
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
function
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return undefined;
|
|
40
|
-
const entry = value;
|
|
41
|
-
if ((entry.v !== undefined && entry.v !== 1) || (entry.op !== 'join' && entry.op !== 'done') || typeof entry.session_id !== 'string' || entry.session_id === '' || typeof entry.name !== 'string' || entry.name === '' || typeof entry.square_path !== 'string' || entry.square_path === '' || typeof entry.ts !== 'string')
|
|
42
|
-
return undefined;
|
|
43
|
-
const updatedAt = Date.parse(entry.ts);
|
|
44
|
-
if (!Number.isFinite(updatedAt) || updatedAt > now || now - updatedAt > MAX_AGE_MS)
|
|
45
|
-
return undefined;
|
|
46
|
-
const channel = entry.channel ?? 'unknown';
|
|
47
|
-
if (!VALID_CHANNELS.has(channel) || (entry.child !== undefined && entry.child !== true) || (entry.paseo_agent_id !== undefined && typeof entry.paseo_agent_id !== 'string') || (entry.owner_id !== undefined && typeof entry.owner_id !== 'string'))
|
|
48
|
-
return undefined;
|
|
49
|
-
return { ...entry, v: 1, channel };
|
|
22
|
+
function ledger(env, writableScope = 'local') { const root = path.dirname(registryPath(env)); return createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? (env.SQUARE_REGISTRY ? root : path.join(homedir(), '.square', 'host-ledger')), localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? (env.SQUARE_REGISTRY ? root : path.join(process.cwd(), '.square', 'host-ledger')), writableScope }); }
|
|
23
|
+
function toBinding(record) { return { sessionId: record.session, name: record.participant, squarePath: record.location, channel: record.channel, child: false, ...(record.route === undefined ? {} : { route: record.route }), updatedAt: record.updatedAt ?? 0 }; }
|
|
24
|
+
async function activeBindings(now, env) { return (await ledger(env).listPresence({ now, scopes: ['user', 'local'] })).map(toBinding).sort((a, b) => b.updatedAt - a.updatedAt); }
|
|
25
|
+
async function writePresence(sessionId, name, squarePath, options, done, scope = 'local') { if (!sessionId || !name || !squarePath)
|
|
26
|
+
return; const env = options.env ?? process.env; const channel = options.channel ?? 'unknown'; const port = ledger(env, scope); const location = await canonicalSquarePath(squarePath); if (done)
|
|
27
|
+
await port.removePresence({ location, participant: name, session: sessionId, channel });
|
|
28
|
+
else
|
|
29
|
+
await port.ensurePresence({ location, participant: name, session: sessionId, channel, route: options.route, updatedAt: options.at ?? Date.now() }); }
|
|
30
|
+
export function recordJoin(sessionId, name, squarePath, options = {}) { return writePresence(sessionId, name, squarePath, options, false); }
|
|
31
|
+
export async function recordDone(sessionId, name, squarePath, options = {}) {
|
|
32
|
+
await writePresence(sessionId, name, squarePath, options, true);
|
|
33
|
+
await writePresence(sessionId, name, squarePath, options, true, 'user');
|
|
50
34
|
}
|
|
51
|
-
async function
|
|
52
|
-
|
|
53
|
-
const owners = new Map();
|
|
54
|
-
let order = 0;
|
|
55
|
-
for (const line of raw.split('\n')) {
|
|
56
|
-
if (!line.trim())
|
|
57
|
-
continue;
|
|
58
|
-
const entry = parseLine(line, now);
|
|
59
|
-
if (!entry)
|
|
60
|
-
continue;
|
|
61
|
-
order++;
|
|
62
|
-
const ownerId = entry.owner_id ?? `legacy:${order}`;
|
|
63
|
-
state.set(await bindingKey(entry.session_id, entry.square_path, entry.name, entry.channel), { entry, updatedAt: Date.parse(entry.ts), ownerId });
|
|
64
|
-
if (entry.op === 'join')
|
|
65
|
-
owners.set(await participantKey(entry.square_path, entry.name), ownerId);
|
|
66
|
-
}
|
|
67
|
-
const active = [];
|
|
68
|
-
for (const { entry, updatedAt, ownerId } of state.values()) {
|
|
69
|
-
if (entry.op !== 'join' || owners.get(await participantKey(entry.square_path, entry.name)) !== ownerId)
|
|
70
|
-
continue;
|
|
71
|
-
active.push({ sessionId: entry.session_id, name: entry.name, squarePath: await canonicalSquarePath(entry.square_path), channel: entry.channel, child: entry.child === true, ...(entry.paseo_agent_id ? { paseoAgentId: entry.paseo_agent_id } : {}), ownerId, updatedAt });
|
|
72
|
-
}
|
|
73
|
-
return active.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
74
|
-
}
|
|
75
|
-
async function writeRegistryBindings(filePath, bindings) { const compacted = bindings.slice().reverse().map((binding) => JSON.stringify({ v: 1, ts: new Date(binding.updatedAt).toISOString(), op: 'join', channel: binding.channel, session_id: binding.sessionId, name: binding.name, square_path: binding.squarePath, ...(binding.child ? { child: true } : {}), ...(binding.paseoAgentId ? { paseo_agent_id: binding.paseoAgentId } : {}), owner_id: binding.ownerId })).join('\n'); const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`; await fs.writeFile(temporary, compacted === '' ? '' : `${compacted}\n`, { mode: 0o600 }); await fs.rename(temporary, filePath); }
|
|
76
|
-
async function maybeCompactRegistry(filePath, now) { let stat; try {
|
|
77
|
-
stat = await fs.stat(filePath);
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
if (error.code === 'ENOENT')
|
|
81
|
-
return;
|
|
82
|
-
throw error;
|
|
83
|
-
} if (stat.size <= COMPACT_BYTES)
|
|
84
|
-
return; const raw = await fs.readFile(filePath, 'utf8'); if (stat.size > COMPACT_BYTES || raw.split('\n').filter(Boolean).length > COMPACT_LINES)
|
|
85
|
-
await writeRegistryBindings(filePath, await foldRegistry(raw, now)); }
|
|
86
|
-
async function appendRegistryLine(entry, now) { const filePath = registryPath(); await fs.mkdir(path.dirname(filePath), { recursive: true }); await maybeCompactRegistry(filePath, now); await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, { mode: 0o600 }); }
|
|
87
|
-
async function writeLifecycle(op, sessionId, name, squarePath, options) { if (!sessionId || !name || !squarePath)
|
|
88
|
-
return; const at = options.at ?? Date.now(); if (!Number.isFinite(at))
|
|
89
|
-
return; try {
|
|
90
|
-
await appendRegistryLine({ v: 1, ts: new Date(at).toISOString(), op, channel: options.channel ?? 'unknown', session_id: sessionId, name, square_path: await canonicalSquarePath(squarePath), ...(options.child ? { child: true } : {}), ...(options.paseoAgentId ? { paseo_agent_id: options.paseoAgentId } : {}), ...(op === 'join' ? { owner_id: options.ownerId ?? nextOwnerId() } : {}) }, at);
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
process.stderr.write(`! square registry write failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
94
|
-
} }
|
|
95
|
-
export function recordJoin(sessionId, name, squarePath, options = {}) { return writeLifecycle('join', sessionId, name, squarePath, options); }
|
|
96
|
-
export function recordDone(sessionId, name, squarePath, options = {}) { return writeLifecycle('done', sessionId, name, squarePath, options); }
|
|
97
|
-
export async function readActiveBindings(now = Date.now()) { try {
|
|
98
|
-
return await foldRegistry(await fs.readFile(registryPath(), 'utf8'), now);
|
|
35
|
+
export async function readActiveBindings(now = Date.now(), env = process.env) { try {
|
|
36
|
+
return await activeBindings(now, env);
|
|
99
37
|
}
|
|
100
38
|
catch {
|
|
101
39
|
return [];
|
|
102
40
|
} }
|
|
103
|
-
export async function lookupSessionBindings(sessionId, now = Date.now()) { return (await readActiveBindings(now)).filter((binding) => binding.sessionId === sessionId); }
|
|
104
|
-
export async function lookupSession(sessionId, now = Date.now()) { return (await lookupSessionBindings(sessionId, now)).map(({ name, squarePath }) => ({ name, squarePath })); }
|
|
105
|
-
export async function lookupParticipant(squarePath, name, now = Date.now()) { const canonicalPath = await canonicalSquarePath(squarePath); return (await readActiveBindings(now)).filter((binding) => binding.squarePath === canonicalPath && sameName(binding.name, name)); }
|
|
41
|
+
export async function lookupSessionBindings(sessionId, now = Date.now(), env = process.env) { return (await readActiveBindings(now, env)).filter((binding) => binding.sessionId === sessionId); }
|
|
42
|
+
export async function lookupSession(sessionId, now = Date.now(), env = process.env) { return (await lookupSessionBindings(sessionId, now, env)).map(({ name, squarePath }) => ({ name, squarePath })); }
|
|
43
|
+
export async function lookupParticipant(squarePath, name, now = Date.now(), env = process.env) { const canonicalPath = await canonicalSquarePath(squarePath); return (await readActiveBindings(now, env)).filter((binding) => binding.squarePath === canonicalPath && sameName(binding.name, name)); }
|
|
106
44
|
export async function localParticipantOwner(squarePath, name, env = process.env, now = Date.now()) { const sessionIds = new Set(localSessionIdentities(env).map((identity) => identity.sessionId)); if (sessionIds.size === 0)
|
|
107
|
-
return undefined; return (await lookupParticipant(squarePath, name, now)).find((binding) => sessionIds.has(binding.sessionId))?.
|
|
108
|
-
export async function localParticipantName(squarePath, env = process.env) { const canonicalPath = await canonicalSquarePath(squarePath); const names = new Set((await Promise.all(localSessionIdentities(env).map(async (identity) => (await lookupSession(identity.sessionId)).filter((item) => item.squarePath === canonicalPath).map((item) => item.name)))).flat()); return names.size === 1 ? [...names][0] : undefined; }
|
|
45
|
+
return undefined; return (await lookupParticipant(squarePath, name, now, env)).find((binding) => sessionIds.has(binding.sessionId))?.sessionId; }
|
|
46
|
+
export async function localParticipantName(squarePath, env = process.env) { const canonicalPath = await canonicalSquarePath(squarePath); const names = new Set((await Promise.all(localSessionIdentities(env).map(async (identity) => (await lookupSession(identity.sessionId, Date.now(), env)).filter((item) => item.squarePath === canonicalPath).map((item) => item.name)))).flat()); return names.size === 1 ? [...names][0] : undefined; }
|
|
109
47
|
export function squareAssignedParticipantName(env = process.env) { return computeSquareAssignedParticipantName(env); }
|
|
110
48
|
export async function bindCurrentParticipant(squarePath, name, env = process.env) { if (squareAssignedParticipantName(env) !== name)
|
|
111
|
-
throw new SquareError('invalid_args', `The current session is not assigned ${name}`); const
|
|
112
|
-
return { created: false,
|
|
113
|
-
throw new SquareError('already_joined', `${name} is already bound to another session`); await recordLocalJoin(name, squarePath, env); const
|
|
114
|
-
throw new Error(`Current participant binding did not commit for ${name}`); return { created: true,
|
|
115
|
-
export async function unbindCurrentParticipant(squarePath, name, env = process.env) { const identities = new Set(localSessionIdentities(env).map((identity) => identity.sessionId)); const current = (await lookupParticipant(squarePath, name)).filter((binding) => identities.has(binding.sessionId)); for (const binding of current)
|
|
49
|
+
throw new SquareError('invalid_args', `The current session is not assigned ${name}`); const sessionId = await localParticipantOwner(squarePath, name, env); if (sessionId !== undefined)
|
|
50
|
+
return { created: false, sessionId }; if ((await lookupParticipant(squarePath, name, Date.now(), env)).at(0) !== undefined)
|
|
51
|
+
throw new SquareError('already_joined', `${name} is already bound to another session`); await recordLocalJoin(name, squarePath, env); const currentSessionId = await localParticipantOwner(squarePath, name, env); if (currentSessionId === undefined)
|
|
52
|
+
throw new Error(`Current participant binding did not commit for ${name}`); return { created: true, sessionId: currentSessionId }; }
|
|
53
|
+
export async function unbindCurrentParticipant(squarePath, name, env = process.env) { const identities = new Set(localSessionIdentities(env).map((identity) => identity.sessionId)); const current = (await lookupParticipant(squarePath, name, Date.now(), env)).filter((binding) => identities.has(binding.sessionId)); for (const binding of current)
|
|
116
54
|
await recordSessionDone(binding.sessionId, binding.name, binding.squarePath, binding.channel, env); return current.length > 0; }
|
|
117
55
|
function bindingIsProvablyObsolete(binding, acts) { return acts !== undefined && !isCurrentlyJoined(acts, binding.name); }
|
|
118
|
-
export async function pruneRegistry(readActs, now = Date.now()) { const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
throw error;
|
|
125
|
-
} const active = await foldRegistry(raw, now); const observed = await Promise.all(active.map(async (binding) => ({ binding, acts: await readActs(binding.squarePath) }))); const kept = observed.filter(({ binding, acts }) => !bindingIsProvablyObsolete(binding, acts)).map(({ binding }) => binding); await writeRegistryBindings(filePath, kept); return { removed: active.length - kept.length, kept: kept.length }; }
|
|
56
|
+
export async function pruneRegistry(readActs, now = Date.now()) { const active = await readActiveBindings(now); let removed = 0; for (const binding of active) {
|
|
57
|
+
if (!bindingIsProvablyObsolete(binding, await readActs(binding.squarePath)))
|
|
58
|
+
continue;
|
|
59
|
+
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at: now });
|
|
60
|
+
removed++;
|
|
61
|
+
} return { removed, kept: active.length - removed }; }
|
|
126
62
|
function addLocalSession(identities, sessionId, channel, child, paseoAgentId) { if (!sessionId || identities.some((identity) => identity.sessionId === sessionId))
|
|
127
63
|
return; identities.push({ sessionId, channel, child, ...(paseoAgentId ? { paseoAgentId } : {}) }); }
|
|
128
64
|
export function localSessionIdentities(env = process.env) { const paseoAgentId = env.PASEO_AGENT_ID?.trim() || undefined; const identities = []; for (const source of LOCAL_SESSION_SOURCES)
|
|
129
65
|
addLocalSession(identities, env[source.variable]?.trim(), source.channel, source.child !== undefined && env[source.child] === '1', paseoAgentId); addLocalSession(identities, paseoAgentId, 'paseo', false, paseoAgentId); return identities; }
|
|
130
66
|
export function hasAutomaticDeliveryIdentity(env = process.env) { return localSessionIdentities(env).length > 0; }
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await
|
|
139
|
-
export async function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) { const at = Date.now(); const
|
|
140
|
-
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel,
|
|
141
|
-
await
|
|
142
|
-
} await recordJoin(sessionId, name, squarePath, { channel, at,
|
|
143
|
-
export async function recordSessionDone(sessionId, name, squarePath, channel, env = process.env) { const canonicalPath = await canonicalSquarePath(squarePath); const binding = (await lookupSessionBindings(sessionId)).find((item) => item.squarePath === canonicalPath && sameName(item.name, name) && item.channel === channel); if (binding === undefined)
|
|
144
|
-
return false; const
|
|
145
|
-
await retireOwnerWakeRoutes(binding.ownerId, { at, env }); return true; }
|
|
67
|
+
function callableRoute(sessionId, channel, env) { if (channel === 'codex' && env.CODEX_THREAD_ID?.trim() === sessionId)
|
|
68
|
+
return { kind: 'codex-queue', address: { threadId: sessionId } }; if (channel === 'paseo' && env.PASEO_AGENT_ID?.trim() === sessionId)
|
|
69
|
+
return { kind: 'paseo', address: { agentId: sessionId } }; return undefined; }
|
|
70
|
+
export async function recordLocalJoin(name, squarePath, env = process.env) { const at = Date.now(); const identities = localSessionIdentities(env); const current = await lookupParticipant(squarePath, name, at, env); for (const binding of current)
|
|
71
|
+
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }); for (const identity of identities)
|
|
72
|
+
await recordJoin(identity.sessionId, name, squarePath, { ...identity, at, env }); }
|
|
73
|
+
export async function recordLocalDone(name, squarePath, env = process.env) { const at = Date.now(); const current = await lookupParticipant(squarePath, name, at, env); for (const binding of current)
|
|
74
|
+
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }); }
|
|
75
|
+
export async function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) { const at = Date.now(); const current = await lookupParticipant(squarePath, name, at, env); for (const binding of current) {
|
|
76
|
+
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env });
|
|
77
|
+
await writePresence(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }, true, 'user');
|
|
78
|
+
} const route = callableRoute(sessionId, channel, env); await recordJoin(sessionId, name, squarePath, { channel, at, env }); await writePresence(sessionId, name, squarePath, { channel, route, at, env }, false, 'user'); return sessionId; }
|
|
79
|
+
export async function recordSessionDone(sessionId, name, squarePath, channel, env = process.env) { const canonicalPath = await canonicalSquarePath(squarePath); const binding = (await lookupSessionBindings(sessionId, Date.now(), env)).find((item) => item.squarePath === canonicalPath && sameName(item.name, name) && item.channel === channel); if (binding === undefined)
|
|
80
|
+
return false; const options = { channel, at: Date.now(), env }; await recordDone(sessionId, binding.name, binding.squarePath, options); await writePresence(sessionId, binding.name, binding.squarePath, options, true, 'user'); return true; }
|