@astrosheep/square 0.3.27 → 0.3.29

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 (49) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  3. package/dist/artifact.d.ts +4 -4
  4. package/dist/artifact.js +24 -19
  5. package/dist/automatic-session.js +18 -7
  6. package/dist/boundary-presentation.d.ts +1 -1
  7. package/dist/boundary-presentation.js +2 -2
  8. package/dist/cli/context.d.ts +4 -4
  9. package/dist/cli/context.js +11 -9
  10. package/dist/cli/maintenance-commands.js +2 -2
  11. package/dist/cli/observation-commands.js +2 -2
  12. package/dist/cli/program.js +1 -1
  13. package/dist/cli/square-commands.js +15 -16
  14. package/dist/codex-boundary-state.d.ts +4 -4
  15. package/dist/codex-boundary-state.js +19 -19
  16. package/dist/codex-hook.js +3 -3
  17. package/dist/codex-queue.js +2 -2
  18. package/dist/file-lock.d.ts +1 -1
  19. package/dist/file-lock.js +22 -50
  20. package/dist/harness-links.d.ts +2 -1
  21. package/dist/harness-links.js +41 -12
  22. package/dist/harness.js +11 -5
  23. package/dist/inbox.js +2 -2
  24. package/dist/list.js +3 -3
  25. package/dist/notifications.d.ts +1 -1
  26. package/dist/notifications.js +10 -10
  27. package/dist/opencode.d.ts +19 -0
  28. package/dist/opencode.js +49 -0
  29. package/dist/presented.d.ts +5 -13
  30. package/dist/presented.js +48 -158
  31. package/dist/registry.d.ts +18 -30
  32. package/dist/registry.js +89 -320
  33. package/dist/routes.d.ts +6 -6
  34. package/dist/routes.js +20 -20
  35. package/dist/square-file-adapter.d.ts +1 -1
  36. package/dist/square-file-adapter.js +5 -9
  37. package/dist/square-storage.d.ts +3 -3
  38. package/dist/square-storage.js +23 -20
  39. package/dist/square-wiring.js +1 -1
  40. package/dist/wake-attempts.d.ts +6 -6
  41. package/dist/wake-attempts.js +39 -31
  42. package/dist/wake-evidence.d.ts +1 -1
  43. package/dist/wake-evidence.js +11 -11
  44. package/dist/wake-port.d.ts +1 -1
  45. package/dist/wake-port.js +1 -1
  46. package/dist/watch.js +1 -1
  47. package/extensions/square-pi.js +30 -3
  48. package/package.json +5 -1
  49. package/extensions/square-opencode.js +0 -48
package/dist/registry.js CHANGED
@@ -1,11 +1,5 @@
1
- /**
2
- * Machine-local participant discovery cache.
3
- *
4
- * The Square artifact remains authoritative for membership. This append-only
5
- * cache only maps native harness sessions and optional Paseo agent ids back to
6
- * active (square path, participant name) pairs.
7
- */
8
- import fs from 'node:fs';
1
+ /** Machine-local participant discovery cache. */
2
+ import { promises as fs } from 'node:fs';
9
3
  import path from 'node:path';
10
4
  import { homedir } from 'node:os';
11
5
  import { randomUUID } from 'node:crypto';
@@ -23,29 +17,16 @@ const LOCAL_SESSION_SOURCES = [
23
17
  { variable: 'OPENCODE_SESSION_ID', channel: 'opencode' },
24
18
  { variable: 'SQUARE_PI_SESSION_ID', channel: 'pi' },
25
19
  ];
26
- export function registryPath() {
27
- if (process.env['SQUARE_REGISTRY'])
28
- return process.env['SQUARE_REGISTRY'];
29
- return path.join(homedir(), '.square', 'sessions.ndjsonl');
30
- }
31
- export function canonicalSquarePath(squarePath) {
32
- const absolute = path.resolve(squarePath);
33
- try {
34
- return fs.realpathSync.native(absolute);
35
- }
36
- catch {
37
- return absolute;
38
- }
39
- }
40
- function bindingKey(sessionId, squarePath, name, channel) {
41
- return JSON.stringify([sessionId, canonicalSquarePath(squarePath), nameKey(name), channel]);
42
- }
43
- function participantKey(squarePath, name) {
44
- return JSON.stringify([canonicalSquarePath(squarePath), nameKey(name)]);
45
- }
46
- function nextOwnerId() {
47
- return randomUUID();
48
- }
20
+ export function registryPath() { return process.env.SQUARE_REGISTRY || path.join(homedir(), '.square', 'sessions.ndjsonl'); }
21
+ export async function canonicalSquarePath(squarePath) { const absolute = path.resolve(squarePath); try {
22
+ return await fs.realpath(absolute);
23
+ }
24
+ catch {
25
+ return absolute;
26
+ } }
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(); }
49
30
  function parseLine(raw, now) {
50
31
  let value;
51
32
  try {
@@ -57,320 +38,108 @@ function parseLine(raw, now) {
57
38
  if (value === null || typeof value !== 'object')
58
39
  return undefined;
59
40
  const entry = value;
60
- if ((entry.v !== undefined && entry.v !== 1) ||
61
- (entry.op !== 'join' && entry.op !== 'done') ||
62
- typeof entry.session_id !== 'string' ||
63
- entry.session_id === '' ||
64
- typeof entry.name !== 'string' ||
65
- entry.name === '' ||
66
- typeof entry.square_path !== 'string' ||
67
- entry.square_path === '' ||
68
- typeof entry.ts !== 'string') {
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')
69
42
  return undefined;
70
- }
71
43
  const updatedAt = Date.parse(entry.ts);
72
44
  if (!Number.isFinite(updatedAt) || updatedAt > now || now - updatedAt > MAX_AGE_MS)
73
45
  return undefined;
74
46
  const channel = entry.channel ?? 'unknown';
75
- if (!VALID_CHANNELS.has(channel))
76
- return undefined;
77
- if (entry.child !== undefined && entry.child !== true)
78
- return undefined;
79
- if (entry.paseo_agent_id !== undefined && typeof entry.paseo_agent_id !== 'string')
80
- return undefined;
81
- if (entry.owner_id !== undefined && typeof entry.owner_id !== 'string')
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'))
82
48
  return undefined;
83
49
  return { ...entry, v: 1, channel };
84
50
  }
85
- function foldRegistry(raw, now) {
51
+ async function foldRegistry(raw, now) {
86
52
  const state = new Map();
87
- // A later claim replaces the participant's prior agent, while one claim may retain multiple adapter identities.
88
53
  const owners = new Map();
89
54
  let order = 0;
90
55
  for (const line of raw.split('\n')) {
91
- if (line.trim() === '')
56
+ if (!line.trim())
92
57
  continue;
93
58
  const entry = parseLine(line, now);
94
59
  if (!entry)
95
60
  continue;
96
61
  order++;
97
62
  const ownerId = entry.owner_id ?? `legacy:${order}`;
98
- state.set(bindingKey(entry.session_id, entry.square_path, entry.name, entry.channel), {
99
- entry,
100
- updatedAt: Date.parse(entry.ts),
101
- ownerId,
102
- });
63
+ state.set(await bindingKey(entry.session_id, entry.square_path, entry.name, entry.channel), { entry, updatedAt: Date.parse(entry.ts), ownerId });
103
64
  if (entry.op === 'join')
104
- owners.set(participantKey(entry.square_path, entry.name), ownerId);
65
+ owners.set(await participantKey(entry.square_path, entry.name), ownerId);
105
66
  }
106
67
  const active = [];
107
68
  for (const { entry, updatedAt, ownerId } of state.values()) {
108
- if (entry.op !== 'join')
69
+ if (entry.op !== 'join' || owners.get(await participantKey(entry.square_path, entry.name)) !== ownerId)
109
70
  continue;
110
- if (owners.get(participantKey(entry.square_path, entry.name)) !== ownerId)
111
- continue;
112
- active.push({
113
- sessionId: entry.session_id,
114
- name: entry.name,
115
- squarePath: canonicalSquarePath(entry.square_path),
116
- channel: entry.channel,
117
- child: entry.child === true,
118
- ...(entry.paseo_agent_id ? { paseoAgentId: entry.paseo_agent_id } : {}),
119
- ownerId,
120
- updatedAt,
121
- });
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 });
122
72
  }
123
73
  return active.sort((a, b) => b.updatedAt - a.updatedAt);
124
74
  }
125
- function writeRegistryBindings(filePath, bindings) {
126
- const compacted = bindings
127
- .slice()
128
- .reverse()
129
- .map((binding) => JSON.stringify({
130
- v: 1,
131
- ts: new Date(binding.updatedAt).toISOString(),
132
- op: 'join',
133
- channel: binding.channel,
134
- session_id: binding.sessionId,
135
- name: binding.name,
136
- square_path: binding.squarePath,
137
- ...(binding.child ? { child: true } : {}),
138
- ...(binding.paseoAgentId ? { paseo_agent_id: binding.paseoAgentId } : {}),
139
- owner_id: binding.ownerId,
140
- }))
141
- .join('\n');
142
- const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
143
- fs.writeFileSync(temporary, compacted === '' ? '' : `${compacted}\n`, { mode: 0o600 });
144
- fs.renameSync(temporary, filePath);
145
- }
146
- function compactRegistry(filePath, raw, now) {
147
- writeRegistryBindings(filePath, foldRegistry(raw, now));
148
- }
149
- function maybeCompactRegistry(filePath, now) {
150
- let stat;
151
- try {
152
- stat = fs.statSync(filePath);
153
- }
154
- catch (error) {
155
- if (error.code === 'ENOENT')
156
- return;
157
- throw error;
158
- }
159
- if (stat.size <= COMPACT_BYTES)
160
- return;
161
- const raw = fs.readFileSync(filePath, 'utf8');
162
- const lines = raw.split('\n').filter(Boolean).length;
163
- if (stat.size > COMPACT_BYTES || lines > COMPACT_LINES)
164
- compactRegistry(filePath, raw, now);
165
- }
166
- function appendRegistryLine(entry, now) {
167
- const filePath = registryPath();
168
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
169
- maybeCompactRegistry(filePath, now);
170
- const fd = fs.openSync(filePath, 'a', 0o600);
171
- try {
172
- fs.writeSync(fd, `${JSON.stringify(entry)}\n`);
173
- }
174
- finally {
175
- fs.closeSync(fd);
176
- }
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);
177
78
  }
178
- function writeLifecycle(op, sessionId, name, squarePath, options) {
179
- if (!sessionId || !name || !squarePath)
79
+ catch (error) {
80
+ if (error.code === 'ENOENT')
180
81
  return;
181
- const at = options.at ?? Date.now();
182
- if (!Number.isFinite(at))
183
- return;
184
- try {
185
- appendRegistryLine({
186
- v: 1,
187
- ts: new Date(at).toISOString(),
188
- op,
189
- channel: options.channel ?? 'unknown',
190
- session_id: sessionId,
191
- name,
192
- square_path: canonicalSquarePath(squarePath),
193
- ...(options.child ? { child: true } : {}),
194
- ...(options.paseoAgentId ? { paseo_agent_id: options.paseoAgentId } : {}),
195
- ...(op === 'join' ? { owner_id: options.ownerId ?? nextOwnerId() } : {}),
196
- }, at);
197
- }
198
- catch (error) {
199
- process.stderr.write(`! square registry write failed: ${error instanceof Error ? error.message : String(error)}\n`);
200
- }
201
- }
202
- export function recordJoin(sessionId, name, squarePath, options = {}) {
203
- writeLifecycle('join', sessionId, name, squarePath, options);
204
- }
205
- export function recordDone(sessionId, name, squarePath, options = {}) {
206
- writeLifecycle('done', sessionId, name, squarePath, options);
207
- }
208
- export function readActiveBindings(now = Date.now()) {
209
- try {
210
- return foldRegistry(fs.readFileSync(registryPath(), 'utf8'), now);
211
- }
212
- catch (error) {
213
- if (error.code === 'ENOENT')
214
- return [];
215
- return [];
216
- }
217
- }
218
- export function lookupSessionBindings(sessionId, now = Date.now()) {
219
- return readActiveBindings(now).filter((binding) => binding.sessionId === sessionId);
220
- }
221
- export function lookupSession(sessionId, now = Date.now()) {
222
- return lookupSessionBindings(sessionId, now).map(({ name, squarePath }) => ({ name, squarePath }));
223
- }
224
- export function lookupParticipant(squarePath, name, now = Date.now()) {
225
- const canonicalPath = canonicalSquarePath(squarePath);
226
- return readActiveBindings(now).filter((binding) => binding.squarePath === canonicalPath && sameName(binding.name, name));
227
- }
228
- /** Resolve the current local harness owner for a participant, if one is registered. */
229
- export function localParticipantOwner(squarePath, name, env = process.env, now = Date.now()) {
230
- const sessionIds = new Set(localSessionIdentities(env).map((identity) => identity.sessionId));
231
- if (sessionIds.size === 0)
232
- return undefined;
233
- return lookupParticipant(squarePath, name, now).find((binding) => sessionIds.has(binding.sessionId))?.ownerId;
234
- }
235
- export function localParticipantName(squarePath, env = process.env) {
236
- const identities = localSessionIdentities(env);
237
- const names = new Set(identities.flatMap((identity) => lookupSession(identity.sessionId).filter((item) => canonicalSquarePath(item.squarePath) === canonicalSquarePath(squarePath)).map((item) => item.name)));
238
- return names.size === 1 ? [...names][0] : undefined;
239
- }
240
- /** Compute the current participant from the live harness identity, without registry history. */
241
- export function squareAssignedParticipantName(env = process.env) {
242
- return computeSquareAssignedParticipantName(env);
243
- }
244
- /** Bind the current harness to one explicit Square participant. */
245
- export function bindCurrentParticipant(squarePath, name, env = process.env) {
246
- if (squareAssignedParticipantName(env) !== name) {
247
- throw new SquareError('invalid_args', `The current session is not assigned ${name}`);
248
- }
249
- const localOwner = localParticipantOwner(squarePath, name, env);
250
- if (localOwner !== undefined)
251
- return { created: false, ownerId: localOwner };
252
- const occupied = lookupParticipant(squarePath, name).at(0);
253
- if (occupied !== undefined) {
254
- throw new SquareError('already_joined', `${name} is already bound to another session`);
255
- }
256
- recordLocalJoin(name, squarePath, env);
257
- const ownerId = localParticipantOwner(squarePath, name, env);
258
- if (ownerId === undefined)
259
- throw new Error(`Current participant binding did not commit for ${name}`);
260
- return { created: true, ownerId };
261
- }
262
- /** Retire only this harness's binding for one Square participant. */
263
- export function unbindCurrentParticipant(squarePath, name, env = process.env) {
264
- const identities = new Set(localSessionIdentities(env).map((identity) => identity.sessionId));
265
- const current = lookupParticipant(squarePath, name).filter((binding) => identities.has(binding.sessionId));
266
- for (const binding of current) {
267
- recordSessionDone(binding.sessionId, binding.name, binding.squarePath, binding.channel, env);
268
- }
269
- return current.length > 0;
270
- }
271
- function bindingIsProvablyObsolete(binding, acts) {
272
- return acts !== undefined && !isCurrentlyJoined(acts, binding.name);
273
- }
274
- /** Compact the registry and remove only bindings disproved by their authoritative artifact. */
275
- export function pruneRegistry(readActs, now = Date.now()) {
276
- const filePath = registryPath();
277
- let raw;
278
- try {
279
- raw = fs.readFileSync(filePath, 'utf8');
280
- }
281
- catch (error) {
282
- if (error.code === 'ENOENT')
283
- return { removed: 0, kept: 0 };
284
- throw error;
285
- }
286
- const active = foldRegistry(raw, now);
287
- const kept = active.filter((binding) => !bindingIsProvablyObsolete(binding, readActs(binding.squarePath)));
288
- writeRegistryBindings(filePath, kept);
289
- return { removed: active.length - kept.length, kept: kept.length };
290
- }
291
- function addLocalSession(identities, sessionId, channel, child, paseoAgentId) {
292
- if (!sessionId || identities.some((identity) => identity.sessionId === sessionId))
293
- return;
294
- identities.push({ sessionId, channel, child, ...(paseoAgentId ? { paseoAgentId } : {}) });
295
- }
296
- export function localSessionIdentities(env = process.env) {
297
- const paseoAgentId = env['PASEO_AGENT_ID']?.trim() || undefined;
298
- const identities = [];
299
- for (const source of LOCAL_SESSION_SOURCES) {
300
- addLocalSession(identities, env[source.variable]?.trim(), source.channel, source.child !== undefined && env[source.child] === '1', paseoAgentId);
301
- }
302
- addLocalSession(identities, paseoAgentId, 'paseo', false, paseoAgentId);
303
- return identities;
304
- }
305
- /** True when this process belongs to a harness that can deliver Square attention without a foreground catch. */
306
- export function hasAutomaticDeliveryIdentity(env = process.env) {
307
- return localSessionIdentities(env).length > 0;
308
- }
309
- export function recordLocalJoin(name, squarePath, env = process.env) {
310
- const at = Date.now();
311
- const identities = localSessionIdentities(env);
312
- const current = lookupParticipant(squarePath, name, at);
313
- const ownerId = nextOwnerId();
314
- for (const binding of current) {
315
- recordDone(binding.sessionId, binding.name, binding.squarePath, {
316
- channel: binding.channel,
317
- child: binding.child,
318
- ...(binding.paseoAgentId ? { paseoAgentId: binding.paseoAgentId } : {}),
319
- at,
320
- });
321
- }
322
- for (const identity of identities) {
323
- recordJoin(identity.sessionId, name, squarePath, { ...identity, at, ownerId });
324
- }
325
- publishWakeRoutes(ownerId, { at, env });
326
- for (const previousOwnerId of new Set(current.map((binding) => binding.ownerId))) {
327
- if (previousOwnerId !== ownerId)
328
- retireOwnerWakeRoutes(previousOwnerId, { at, env });
329
- }
330
- }
331
- export function recordLocalDone(name, squarePath, env = process.env) {
332
- const at = Date.now();
333
- const current = lookupParticipant(squarePath, name, at);
334
- for (const binding of current) {
335
- recordDone(binding.sessionId, binding.name, binding.squarePath, {
336
- channel: binding.channel,
337
- child: binding.child,
338
- ...(binding.paseoAgentId ? { paseoAgentId: binding.paseoAgentId } : {}),
339
- at,
340
- });
341
- }
342
- for (const ownerId of new Set(current.map((binding) => binding.ownerId))) {
343
- retireOwnerWakeRoutes(ownerId, { at, env });
344
- }
345
- }
346
- export function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) {
347
- const at = Date.now();
348
- const ownerId = nextOwnerId();
349
- const current = lookupParticipant(squarePath, name, at);
350
- for (const binding of current) {
351
- recordDone(binding.sessionId, binding.name, binding.squarePath, {
352
- channel: binding.channel,
353
- child: binding.child,
354
- ...(binding.paseoAgentId ? { paseoAgentId: binding.paseoAgentId } : {}),
355
- at,
356
- });
357
- retireOwnerWakeRoutes(binding.ownerId, { at, env });
358
- }
359
- recordJoin(sessionId, name, squarePath, { channel, at, ownerId });
360
- publishWakeRoutes(ownerId, { at, env });
361
- return ownerId;
362
- }
363
- export function recordSessionDone(sessionId, name, squarePath, channel, env = process.env) {
364
- const binding = lookupSessionBindings(sessionId).find((item) => canonicalSquarePath(item.squarePath) === canonicalSquarePath(squarePath)
365
- && sameName(item.name, name)
366
- && item.channel === channel);
367
- if (binding === undefined)
368
- return false;
369
- const at = Date.now();
370
- recordDone(sessionId, binding.name, binding.squarePath, { channel, at });
371
- const remaining = lookupParticipant(squarePath, binding.name, at)
372
- .some((candidate) => candidate.ownerId === binding.ownerId);
373
- if (!remaining)
374
- retireOwnerWakeRoutes(binding.ownerId, { at, env });
375
- return true;
376
- }
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);
99
+ }
100
+ catch {
101
+ return [];
102
+ } }
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)); }
106
+ 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; }
109
+ export function squareAssignedParticipantName(env = process.env) { return computeSquareAssignedParticipantName(env); }
110
+ 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)
116
+ await recordSessionDone(binding.sessionId, binding.name, binding.squarePath, binding.channel, env); return current.length > 0; }
117
+ 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 }; }
126
+ function addLocalSession(identities, sessionId, channel, child, paseoAgentId) { if (!sessionId || identities.some((identity) => identity.sessionId === sessionId))
127
+ return; identities.push({ sessionId, channel, child, ...(paseoAgentId ? { paseoAgentId } : {}) }); }
128
+ export function localSessionIdentities(env = process.env) { const paseoAgentId = env.PASEO_AGENT_ID?.trim() || undefined; const identities = []; for (const source of LOCAL_SESSION_SOURCES)
129
+ 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
+ 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; }
package/dist/routes.d.ts CHANGED
@@ -8,20 +8,20 @@ export declare function readWakeRoutes(opts?: {
8
8
  freshOnly?: boolean;
9
9
  now?: number;
10
10
  env?: NodeJS.ProcessEnv;
11
- }): WakeRoute[];
11
+ }): Promise<WakeRoute[]>;
12
12
  export declare function upsertWakeRoute(route: Omit<WakeRoute, 'updatedAt'>, opts?: {
13
13
  at?: number;
14
14
  env?: NodeJS.ProcessEnv;
15
- }): void;
15
+ }): Promise<void>;
16
16
  export declare function retireOwnerWakeRoutes(ownerId: string, opts?: {
17
17
  at?: number;
18
18
  env?: NodeJS.ProcessEnv;
19
- }): void;
19
+ }): Promise<void>;
20
20
  /** Retire one capability without touching sibling identities in the same claim. */
21
21
  export declare function retireWakeRoute(route: Pick<WakeRoute, 'ownerId' | 'sessionId' | 'kind'>, opts?: {
22
22
  at?: number;
23
23
  env?: NodeJS.ProcessEnv;
24
- }): void;
24
+ }): Promise<void>;
25
25
  export interface WakeRouteEvidence {
26
26
  sessionId: string;
27
27
  address: Record<string, string>;
@@ -39,9 +39,9 @@ export declare const WAKE_ROUTE_PROBES: Readonly<Record<WakeRouteKind, WakeRoute
39
39
  export declare function publishWakeRoutesFrom(ownerId: string, probes: Readonly<Record<WakeRouteKind, WakeRouteProbe>>, opts?: {
40
40
  at?: number;
41
41
  env?: NodeJS.ProcessEnv;
42
- }): void;
42
+ }): Promise<void>;
43
43
  /** Publication boundary: every route written is complete provider evidence. */
44
44
  export declare function publishWakeRoutes(ownerId: string, opts?: {
45
45
  at?: number;
46
46
  env?: NodeJS.ProcessEnv;
47
- }): void;
47
+ }): Promise<void>;
package/dist/routes.js CHANGED
@@ -1,4 +1,4 @@
1
- import fs from 'node:fs';
1
+ import { promises as fs } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { WAKE_ROUTE_KINDS, isWakeRouteKind } from './model.js';
@@ -42,10 +42,10 @@ function parseRow(raw, now) {
42
42
  return undefined;
43
43
  return row;
44
44
  }
45
- function readRows(env, now) {
45
+ async function readRows(env, now) {
46
46
  let raw;
47
47
  try {
48
- raw = fs.readFileSync(routesPath(env), 'utf8');
48
+ raw = await fs.readFile(routesPath(env), 'utf8');
49
49
  }
50
50
  catch (error) {
51
51
  if (error.code === 'ENOENT')
@@ -54,10 +54,10 @@ function readRows(env, now) {
54
54
  }
55
55
  return raw.split('\n').filter(Boolean).map((line) => parseRow(line, now)).filter((row) => row !== undefined);
56
56
  }
57
- export function readWakeRoutes(opts = {}) {
57
+ export async function readWakeRoutes(opts = {}) {
58
58
  const now = opts.now ?? Date.now();
59
59
  const state = new Map();
60
- for (const row of readRows(opts.env ?? process.env, now)) {
60
+ for (const row of await readRows(opts.env ?? process.env, now)) {
61
61
  const key = routeKey(row.owner_id, row.kind);
62
62
  const current = state.get(key);
63
63
  if (current === undefined || row.ts >= current.ts)
@@ -77,14 +77,14 @@ export function readWakeRoutes(opts = {}) {
77
77
  .sort((a, b) => (ROUTE_KIND_PRIORITY.get(a.kind) ?? 0) - (ROUTE_KIND_PRIORITY.get(b.kind) ?? 0) ||
78
78
  b.updatedAt - a.updatedAt);
79
79
  }
80
- function appendRouteRow(row, env) {
80
+ async function appendRouteRow(row, env) {
81
81
  const file = routesPath(env);
82
- fs.mkdirSync(path.dirname(file), { recursive: true });
83
- fs.appendFileSync(file, `${JSON.stringify(row)}\n`, { mode: 0o600 });
82
+ await fs.mkdir(path.dirname(file), { recursive: true });
83
+ await fs.appendFile(file, `${JSON.stringify(row)}\n`, { mode: 0o600 });
84
84
  }
85
- export function upsertWakeRoute(route, opts = {}) {
85
+ export async function upsertWakeRoute(route, opts = {}) {
86
86
  const at = opts.at ?? Date.now();
87
- appendRouteRow({
87
+ await appendRouteRow({
88
88
  v: 1,
89
89
  ts: at,
90
90
  op: 'upsert',
@@ -94,11 +94,11 @@ export function upsertWakeRoute(route, opts = {}) {
94
94
  address: route.address,
95
95
  }, opts.env ?? process.env);
96
96
  }
97
- export function retireOwnerWakeRoutes(ownerId, opts = {}) {
97
+ export async function retireOwnerWakeRoutes(ownerId, opts = {}) {
98
98
  const at = opts.at ?? Date.now();
99
99
  const env = opts.env ?? process.env;
100
- for (const route of readWakeRoutes({ ownerId, now: at, env })) {
101
- appendRouteRow({
100
+ for (const route of await readWakeRoutes({ ownerId, now: at, env })) {
101
+ await appendRouteRow({
102
102
  v: 1,
103
103
  ts: at,
104
104
  op: 'retire',
@@ -109,14 +109,14 @@ export function retireOwnerWakeRoutes(ownerId, opts = {}) {
109
109
  }
110
110
  }
111
111
  /** Retire one capability without touching sibling identities in the same claim. */
112
- export function retireWakeRoute(route, opts = {}) {
112
+ export async function retireWakeRoute(route, opts = {}) {
113
113
  const at = opts.at ?? Date.now();
114
114
  const env = opts.env ?? process.env;
115
- const current = readWakeRoutes({ ownerId: route.ownerId, now: at, env })
115
+ const current = (await readWakeRoutes({ ownerId: route.ownerId, now: at, env }))
116
116
  .find((candidate) => candidate.sessionId === route.sessionId && candidate.kind === route.kind);
117
117
  if (current === undefined)
118
118
  return;
119
- appendRouteRow({
119
+ await appendRouteRow({
120
120
  v: 1,
121
121
  ts: at,
122
122
  op: 'retire',
@@ -157,17 +157,17 @@ export const WAKE_ROUTE_PROBES = {
157
157
  },
158
158
  };
159
159
  /** The kind-neutral publication loop; probes supply complete evidence per kind. */
160
- export function publishWakeRoutesFrom(ownerId, probes, opts = {}) {
160
+ export async function publishWakeRoutesFrom(ownerId, probes, opts = {}) {
161
161
  const at = opts.at ?? Date.now();
162
162
  const env = opts.env ?? process.env;
163
163
  for (const kind of WAKE_ROUTE_KINDS) {
164
164
  const evidence = probes[kind](env);
165
165
  if (!completeRouteEvidence(evidence))
166
166
  continue;
167
- upsertWakeRoute({ ownerId, sessionId: evidence.sessionId, kind, address: evidence.address }, { at, env });
167
+ await upsertWakeRoute({ ownerId, sessionId: evidence.sessionId, kind, address: evidence.address }, { at, env });
168
168
  }
169
169
  }
170
170
  /** Publication boundary: every route written is complete provider evidence. */
171
- export function publishWakeRoutes(ownerId, opts = {}) {
172
- publishWakeRoutesFrom(ownerId, WAKE_ROUTE_PROBES, opts);
171
+ export async function publishWakeRoutes(ownerId, opts = {}) {
172
+ await publishWakeRoutesFrom(ownerId, WAKE_ROUTE_PROBES, opts);
173
173
  }
@@ -13,7 +13,7 @@ export interface SquareBuildOptions {
13
13
  notifier?: WakeNotifier;
14
14
  }
15
15
  export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'notifier'>): Promise<OpenSquare>;
16
- export declare function probeSquare(squarePath: string): OpenSquare | undefined;
16
+ export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
17
17
  export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
18
18
  export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
19
19
  /** Wait for any bound artifact to change; delivery callers re-project after the edge. */
@@ -2,17 +2,13 @@ import fs from 'node:fs';
2
2
  import { createSquareState, probeSquareFile, writeSquareSnapshot, withSquareFileLock, openSquareCell, createMemoryCell, } from './square-storage.js';
3
3
  import { InternalSquareError, SquareError, } from './model.js';
4
4
  import { closeOpenSquare } from './open-square.js';
5
- /** The current CLI file mutation boundary. */
6
- function writeSquareState(squarePath, squareState) {
7
- writeSquareSnapshot(squarePath, squareState);
8
- }
9
5
  /** File-owned artifact creation for the CLI and path-backed public facade. */
10
6
  export async function createSquare(squarePath, options, snippet) {
11
- await withSquareFileLock(squarePath, () => {
12
- if (fs.existsSync(squarePath) && !options.force) {
7
+ await withSquareFileLock(squarePath, async () => {
8
+ if (await fs.promises.access(squarePath).then(() => true, () => false) && !options.force) {
13
9
  throw new InternalSquareError('conflict', `Refusing to overwrite existing square: ${squarePath}\nPass -f to overwrite.`);
14
10
  }
15
- writeSquareSnapshot(squarePath, createSquareState(options, snippet));
11
+ await writeSquareSnapshot(squarePath, await createSquareState(options, snippet));
16
12
  });
17
13
  }
18
14
  function validateBuildOptions(options) {
@@ -39,8 +35,8 @@ export async function openSquare(squarePath, options = {}) {
39
35
  throw error;
40
36
  }
41
37
  }
42
- export function probeSquare(squarePath) {
43
- const state = probeSquareFile(squarePath);
38
+ export async function probeSquare(squarePath) {
39
+ const state = await probeSquareFile(squarePath);
44
40
  return state === undefined ? undefined : { cell: createMemoryCell(state), clock: Date.now, location: squarePath };
45
41
  }
46
42
  export async function buildSquare(squarePath, options) {