@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.
Files changed (77) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/hooks/hooks.json +0 -3
  3. package/claude-plugin/skills/square/SKILL.md +8 -7
  4. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  5. package/dist/activity.js +7 -3
  6. package/dist/artifact.js +7 -50
  7. package/dist/automatic-session.js +31 -14
  8. package/dist/boundary-presentation.d.ts +1 -1
  9. package/dist/boundary-presentation.js +58 -14
  10. package/dist/catch-decisions.d.ts +17 -0
  11. package/dist/catch-decisions.js +53 -0
  12. package/dist/claude-hook.d.ts +1 -1
  13. package/dist/cli/context.js +10 -2
  14. package/dist/cli/observation-commands.d.ts +5 -1
  15. package/dist/cli/observation-commands.js +68 -33
  16. package/dist/cli/square-commands.js +17 -10
  17. package/dist/codex-hook.d.ts +1 -1
  18. package/dist/decisions.js +2 -2
  19. package/dist/delivery-health.d.ts +1 -1
  20. package/dist/delivery-operations.d.ts +25 -0
  21. package/dist/delivery-operations.js +124 -0
  22. package/dist/help.js +1 -1
  23. package/dist/host-ledger-file-adapter.d.ts +34 -0
  24. package/dist/host-ledger-file-adapter.js +165 -0
  25. package/dist/host-ledger.d.ts +160 -0
  26. package/dist/host-ledger.js +1 -0
  27. package/dist/inbox.d.ts +2 -2
  28. package/dist/inbox.js +22 -14
  29. package/dist/index.d.ts +4 -1
  30. package/dist/index.js +1 -0
  31. package/dist/landing.d.ts +18 -14
  32. package/dist/landing.js +29 -113
  33. package/dist/model.d.ts +6 -17
  34. package/dist/notifications.d.ts +6 -10
  35. package/dist/notifications.js +71 -192
  36. package/dist/open-square.d.ts +4 -4
  37. package/dist/open-square.js +1 -1
  38. package/dist/ports.d.ts +127 -0
  39. package/dist/ports.js +1 -0
  40. package/dist/presence.d.ts +1 -2
  41. package/dist/presence.js +13 -46
  42. package/dist/presentation-operations.d.ts +3 -0
  43. package/dist/presentation-operations.js +51 -0
  44. package/dist/presentation.d.ts +2 -2
  45. package/dist/presentation.js +11 -7
  46. package/dist/presented.d.ts +1 -12
  47. package/dist/presented.js +6 -75
  48. package/dist/registry.d.ts +10 -10
  49. package/dist/registry.js +48 -113
  50. package/dist/routes.d.ts +6 -33
  51. package/dist/routes.js +6 -170
  52. package/dist/runtime.d.ts +1 -1
  53. package/dist/runtime.js +3 -4
  54. package/dist/square-actions.d.ts +32 -0
  55. package/dist/square-actions.js +167 -0
  56. package/dist/square-facade.d.ts +7 -7
  57. package/dist/square-file-adapter.d.ts +4 -3
  58. package/dist/square-file-adapter.js +22 -5
  59. package/dist/square-projections.d.ts +68 -0
  60. package/dist/square-projections.js +87 -0
  61. package/dist/square-storage.d.ts +2 -2
  62. package/dist/square-storage.js +14 -9
  63. package/dist/square-wiring.d.ts +3 -3
  64. package/dist/square-wiring.js +44 -29
  65. package/dist/views.d.ts +8 -2
  66. package/dist/views.js +28 -24
  67. package/dist/wake-attempts.d.ts +29 -22
  68. package/dist/wake-attempts.js +36 -119
  69. package/dist/wake-evidence.d.ts +6 -18
  70. package/dist/wake-evidence.js +17 -80
  71. package/dist/wakes.d.ts +2 -16
  72. package/dist/wakes.js +7 -27
  73. package/dist/watch.js +2 -3
  74. package/extensions/square-pi.js +7 -2
  75. package/package.json +1 -1
  76. package/skills/brainstorm/SKILL.md +2 -2
  77. package/skills/square/SKILL.md +8 -7
package/dist/presence.js CHANGED
@@ -1,49 +1,21 @@
1
- import { extractMentions, formatActivityId } from './square-core.js';
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
- function expose(squareState, activity, viewer, delivery) {
10
- if (activity.kind === 'read' || activity.actor === undefined)
11
- throw new Error(`Cannot expose stored activity ${formatActivityId(activity.index)}`);
12
- const perception = delivery.perceive(activity, viewer);
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) } : {}) };
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 idle = options.idle ?? 0;
21
- if (!Number.isFinite(idle) || idle < 0)
22
- throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
23
- const deadline = Date.now() + idle;
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, ownerId, actIndexes, at = Date.now()) {
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.cell.transact((state) => {
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, ownerId) || changed;
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,3 @@
1
+ import type { PresentationResult, PresentPendingInput } from './ports.js';
2
+ /** Present one pending activity; artifact seen remains the authoritative receipt. */
3
+ export declare function presentPending(input: PresentPendingInput): Promise<PresentationResult>;
@@ -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
+ }
@@ -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, full: boolean | undefined, squarePath: string, viewer?: string, mode?: 'ambient' | 'archive'): string;
80
- export declare function renderGrepActivitiesView(visible: StoredAct[], totalMatches: number, full: boolean | undefined, squarePath: string, pattern: string, fixed?: boolean): string;
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;
@@ -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, full, squarePath, viewer = '', mode = 'ambient') {
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 = full ? undefined : BODY_PREVIEW_LENGTH;
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 --full`);
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, full, squarePath, pattern, fixed = false) {
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 (full === true) {
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 ? ' --full' : ''}`);
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 --full`);
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
  }
@@ -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 readPresentedAttentions(env?: NodeJS.ProcessEnv, now?: number): Promise<PresentedAttention[]>;
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 { createHash } from 'node:crypto';
5
- import { withFileLock } from './file-lock.js';
6
- import { canonicalSquarePath, lookupParticipant, lookupSessionBindings } from './registry.js';
7
- import { sameName } from './model.js';
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
- async function rowKey(row) { return `${row.owner_id}\u0000${await canonicalSquarePath(row.square_path)}\u0000${row.name.toLocaleLowerCase()}\u0000${row.act_index}`; }
13
- async function readRows(filePath, now = Date.now()) {
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'); }
@@ -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
- paseoAgentId?: string;
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
- paseoAgentId?: string;
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
- ownerId: string;
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 { randomUUID } from 'node:crypto';
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 process.env.SQUARE_REGISTRY || path.join(homedir(), '.square', 'sessions.ndjsonl'); }
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
- async function bindingKey(sessionId, squarePath, name, channel) { return JSON.stringify([sessionId, await canonicalSquarePath(squarePath), nameKey(name), channel]); }
28
- async function participantKey(squarePath, name) { return JSON.stringify([await canonicalSquarePath(squarePath), nameKey(name)]); }
29
- function nextOwnerId() { return randomUUID(); }
30
- function parseLine(raw, now) {
31
- let value;
32
- try {
33
- value = JSON.parse(raw);
34
- }
35
- catch {
36
- return undefined;
37
- }
38
- if (value === null || typeof value !== 'object')
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 foldRegistry(raw, now) {
52
- const state = new Map();
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))?.ownerId; }
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 localOwner = await localParticipantOwner(squarePath, name, env); if (localOwner !== undefined)
112
- return { created: false, ownerId: localOwner }; if ((await lookupParticipant(squarePath, name)).at(0) !== undefined)
113
- throw new SquareError('already_joined', `${name} is already bound to another session`); await recordLocalJoin(name, squarePath, env); const ownerId = await localParticipantOwner(squarePath, name, env); if (ownerId === undefined)
114
- throw new Error(`Current participant binding did not commit for ${name}`); return { created: true, ownerId }; }
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 filePath = registryPath(); let raw; try {
119
- raw = await fs.readFile(filePath, 'utf8');
120
- }
121
- catch (error) {
122
- if (error.code === 'ENOENT')
123
- return { removed: 0, kept: 0 };
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
- export async function recordLocalJoin(name, squarePath, env = process.env) { const at = Date.now(); const identities = localSessionIdentities(env); const current = await lookupParticipant(squarePath, name, at); const ownerId = nextOwnerId(); for (const binding of current)
132
- await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, child: binding.child, ...(binding.paseoAgentId ? { paseoAgentId: binding.paseoAgentId } : {}), at }); for (const identity of identities)
133
- await recordJoin(identity.sessionId, name, squarePath, { ...identity, at, ownerId }); await publishWakeRoutes(ownerId, { at, env }); for (const previousOwnerId of new Set(current.map((binding) => binding.ownerId)))
134
- if (previousOwnerId !== ownerId)
135
- await retireOwnerWakeRoutes(previousOwnerId, { at, env }); }
136
- export async function recordLocalDone(name, squarePath, env = process.env) { const at = Date.now(); const current = await lookupParticipant(squarePath, name, at); for (const binding of current)
137
- await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, child: binding.child, ...(binding.paseoAgentId ? { paseoAgentId: binding.paseoAgentId } : {}), at }); for (const ownerId of new Set(current.map((binding) => binding.ownerId)))
138
- await retireOwnerWakeRoutes(ownerId, { at, env }); }
139
- export async function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) { const at = Date.now(); const ownerId = nextOwnerId(); const current = await lookupParticipant(squarePath, name, at); for (const binding of current) {
140
- await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, child: binding.child, ...(binding.paseoAgentId ? { paseoAgentId: binding.paseoAgentId } : {}), at });
141
- await retireOwnerWakeRoutes(binding.ownerId, { at, env });
142
- } await recordJoin(sessionId, name, squarePath, { channel, at, ownerId }); await publishWakeRoutes(ownerId, { at, env }); return ownerId; }
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 at = Date.now(); await recordDone(sessionId, binding.name, binding.squarePath, { channel, at }); const remaining = (await lookupParticipant(squarePath, binding.name, at)).some((candidate) => candidate.ownerId === binding.ownerId); if (!remaining)
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; }