@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/presented.js CHANGED
@@ -1,23 +1,19 @@
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 { createHash } from 'node:crypto';
5
- import { withFileLock, withFileLockSync } from './file-lock.js';
5
+ import { withFileLock } from './file-lock.js';
6
6
  import { canonicalSquarePath, lookupParticipant, lookupSessionBindings } from './registry.js';
7
7
  import { sameName } from './model.js';
8
8
  const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
9
9
  const LOCK_STALE_MS = 5 * 60_000;
10
10
  const LOCK_RETRY_MS = 10;
11
- export function presentedPath(env = process.env) {
12
- return env.SQUARE_PRESENTED || path.join(os.homedir(), '.square', 'presented.ndjsonl');
13
- }
14
- function rowKey(row) {
15
- return `${row.owner_id}\u0000${canonicalSquarePath(row.square_path)}\u0000${row.name.toLocaleLowerCase()}\u0000${row.act_index}`;
16
- }
17
- function readRows(filePath, now = Date.now()) {
11
+ 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()) {
18
14
  let text;
19
15
  try {
20
- text = fs.readFileSync(filePath, 'utf8');
16
+ text = await fs.readFile(filePath, 'utf8');
21
17
  }
22
18
  catch (error) {
23
19
  if (error.code === 'ENOENT')
@@ -31,158 +27,52 @@ function readRows(filePath, now = Date.now()) {
31
27
  continue;
32
28
  try {
33
29
  const parsed = JSON.parse(line);
34
- if (parsed.v !== 2 ||
35
- typeof parsed.ts !== 'number' || !Number.isFinite(parsed.ts) ||
36
- typeof parsed.owner_id !== 'string' ||
37
- typeof parsed.square_path !== 'string' ||
38
- typeof parsed.name !== 'string' ||
39
- typeof parsed.act_index !== 'number' ||
40
- parsed.ts < cutoff) {
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)
41
31
  continue;
42
- }
43
32
  rows.push(parsed);
44
33
  }
45
- catch {
46
- // The presented ledger is a disposable cache; malformed rows are ignored.
47
- }
34
+ catch { }
48
35
  }
49
36
  return rows;
50
37
  }
51
- /** Read the current presentation facts once for a derived evidence projection. */
52
- export function readPresentedAttentions(env = process.env, now = Date.now()) {
53
- return readRows(presentedPath(env), now).map((row) => ({
54
- ownerId: row.owner_id,
55
- squarePath: canonicalSquarePath(row.square_path),
56
- name: row.name,
57
- actIndex: row.act_index,
58
- }));
59
- }
60
- function writeRows(filePath, rows) {
61
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
62
- const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
63
- fs.writeFileSync(temp, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), {
64
- mode: 0o600,
65
- });
66
- fs.renameSync(temp, filePath);
67
- }
68
- function membershipKey(membership) {
69
- return `${canonicalSquarePath(membership.squarePath)}\u0000${membership.name.toLocaleLowerCase()}`;
70
- }
71
- function attentionLockPath(filePath, membership) {
72
- const digest = createHash('sha256').update(membershipKey(membership)).digest('hex');
73
- return `${filePath}.${digest}.lock`;
74
- }
75
- function withAttentionLocks(filePath, inbox, fn) {
76
- const lockPaths = [...new Set(inbox.map((membership) => attentionLockPath(filePath, membership)))].sort();
77
- function acquire(index) {
78
- if (index >= lockPaths.length)
79
- return fn();
80
- return withFileLockSync(lockPaths[index], { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => acquire(index + 1));
81
- }
82
- return acquire(0);
83
- }
84
- async function withAttentionLocksAsync(filePath, inbox, fn) {
85
- const lockPaths = [...new Set(inbox.map((membership) => attentionLockPath(filePath, membership)))].sort();
86
- async function acquire(index) {
87
- if (index >= lockPaths.length)
88
- return fn();
89
- return withFileLock(lockPaths[index], { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => acquire(index + 1));
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);
90
52
  }
91
- return acquire(0);
92
- }
93
- function ownerFor(sessionId, membership) {
94
- const squarePath = canonicalSquarePath(membership.squarePath);
95
- const binding = lookupSessionBindings(sessionId).find((candidate) => candidate.squarePath === squarePath && sameName(candidate.name, membership.name));
96
- return binding?.ownerId ?? `session:${sessionId}`;
97
- }
98
- function selectUnpresented(sessionId, inbox, rows) {
99
- const known = new Set(rows.map(rowKey));
100
- return inbox.flatMap((membership) => {
101
- const ownerId = ownerFor(sessionId, membership);
102
- const notifications = membership.notifications.filter((notification) => !known.has(rowKey({
103
- owner_id: ownerId,
104
- square_path: membership.squarePath,
105
- name: membership.name,
106
- act_index: notification.actIndex,
107
- })));
108
- return notifications.length === 0
109
- ? []
110
- : [{ membership: { ...membership, notifications }, ownerId }];
111
- });
112
- }
113
- export function hasPresentedForOwner(ownerId, squarePath, name, actIndex, env = process.env, now = Date.now()) {
114
- const resolved = canonicalSquarePath(squarePath);
115
- return readRows(presentedPath(env), now).some((row) => row.owner_id === ownerId &&
116
- canonicalSquarePath(row.square_path) === resolved &&
117
- sameName(row.name, name) &&
118
- row.act_index === actIndex);
119
- }
120
- /** True when any current participant owner has already received this attention. */
121
- export function hasPresentedAttention(squarePath, name, actIndex, env = process.env, now = Date.now()) {
122
- const ownerIds = new Set(lookupParticipant(squarePath, name, now).map((binding) => binding.ownerId));
123
- if (ownerIds.size === 0)
124
- return false;
125
- return [...ownerIds].some((ownerId) => hasPresentedForOwner(ownerId, squarePath, name, actIndex, env, now));
126
- }
127
- /** Record presentation by a transport that delivered the bounded attention body. */
128
- export function recordPresentedForOwner(ownerId, squarePath, name, actIndex, env = process.env, at = Date.now()) {
129
- const filePath = presentedPath(env);
130
- const row = {
131
- v: 2,
132
- ts: at,
133
- owner_id: ownerId,
134
- square_path: canonicalSquarePath(squarePath),
135
- name,
136
- act_index: actIndex,
137
- };
138
- withAttentionLocks(filePath, [{ name, squarePath, notifications: [] }], () => {
139
- withFileLockSync(`${filePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
140
- const rows = readRows(filePath, at);
141
- const known = new Set(rows.map(rowKey));
142
- if (!known.has(rowKey(row)))
143
- writeRows(filePath, [...rows, row]);
144
- });
145
- });
146
- }
147
- /**
148
- * Serialize presentation only for the affected participants. Delivery runs
149
- * outside the short ledger-write lock, so unrelated owners never wait on an
150
- * adapter. A throwing or rejecting callback leaves no row and remains unpresented.
151
- */
152
- export async function presentOnce(sessionId, lookup, deliver, env = process.env, at = Date.now()) {
153
- const filePath = presentedPath(env);
154
- const initial = (await lookup(sessionId)).filter((membership) => membership.notifications.length > 0);
155
- if (initial.length === 0)
156
- return undefined;
157
- const lockedMemberships = new Set(initial.map(membershipKey));
158
- return withAttentionLocksAsync(filePath, initial, async () => {
159
- const current = (await lookup(sessionId)).filter((membership) => lockedMemberships.has(membershipKey(membership)));
160
- const selected = selectUnpresented(sessionId, current, readRows(filePath, at));
161
- if (selected.length === 0)
162
- return undefined;
163
- const result = await deliver(selected.map(({ membership }) => membership));
164
- await withFileLock(`${filePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
165
- const rows = readRows(filePath, at);
166
- const known = new Set(rows.map(rowKey));
167
- for (const { membership, ownerId } of selected) {
168
- for (const notification of membership.notifications) {
169
- const row = {
170
- v: 2,
171
- ts: at,
172
- owner_id: ownerId,
173
- square_path: canonicalSquarePath(membership.squarePath),
174
- name: membership.name,
175
- act_index: notification.actIndex,
176
- };
177
- const key = rowKey(row);
178
- if (known.has(key))
179
- continue;
180
- rows.push(row);
181
- known.add(key);
182
- }
183
- }
184
- writeRows(filePath, rows);
185
- });
186
- return result;
187
- });
188
- }
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); }
@@ -1,10 +1,4 @@
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
- */
1
+ /** Machine-local participant discovery cache. */
8
2
  import { type StoredAct } from './model.js';
9
3
  export type SessionChannel = 'claude-code' | 'codex' | 'opencode' | 'pi' | 'paseo' | 'unknown';
10
4
  export interface RegistryBinding {
@@ -25,35 +19,30 @@ export interface RegistryWriteOptions {
25
19
  at?: number;
26
20
  }
27
21
  export declare function registryPath(): string;
28
- export declare function canonicalSquarePath(squarePath: string): string;
29
- export declare function recordJoin(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): void;
30
- export declare function recordDone(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): void;
31
- export declare function readActiveBindings(now?: number): RegistryBinding[];
32
- export declare function lookupSessionBindings(sessionId: string, now?: number): RegistryBinding[];
33
- export declare function lookupSession(sessionId: string, now?: number): Array<{
22
+ export declare function canonicalSquarePath(squarePath: string): Promise<string>;
23
+ export declare function recordJoin(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): Promise<void>;
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<{
34
28
  name: string;
35
29
  squarePath: string;
36
- }>;
37
- export declare function lookupParticipant(squarePath: string, name: string, now?: number): RegistryBinding[];
38
- /** Resolve the current local harness owner for a participant, if one is registered. */
39
- export declare function localParticipantOwner(squarePath: string, name: string, env?: NodeJS.ProcessEnv, now?: number): string | undefined;
40
- export declare function localParticipantName(squarePath: string, env?: NodeJS.ProcessEnv): string | undefined;
41
- /** Compute the current participant from the live harness identity, without registry history. */
30
+ }>>;
31
+ export declare function lookupParticipant(squarePath: string, name: string, now?: number): Promise<RegistryBinding[]>;
32
+ export declare function localParticipantOwner(squarePath: string, name: string, env?: NodeJS.ProcessEnv, now?: number): Promise<string | undefined>;
33
+ export declare function localParticipantName(squarePath: string, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
42
34
  export declare function squareAssignedParticipantName(env?: NodeJS.ProcessEnv): string | undefined;
43
35
  export type CurrentParticipantBinding = Readonly<{
44
36
  created: boolean;
45
37
  ownerId: string;
46
38
  }>;
47
- /** Bind the current harness to one explicit Square participant. */
48
- export declare function bindCurrentParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): CurrentParticipantBinding;
49
- /** Retire only this harness's binding for one Square participant. */
50
- export declare function unbindCurrentParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): boolean;
39
+ export declare function bindCurrentParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): Promise<CurrentParticipantBinding>;
40
+ export declare function unbindCurrentParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
51
41
  export interface RegistryPruneResult {
52
42
  removed: number;
53
43
  kept: number;
54
44
  }
55
- /** Compact the registry and remove only bindings disproved by their authoritative artifact. */
56
- export declare function pruneRegistry(readActs: (squarePath: string) => StoredAct[] | undefined, now?: number): RegistryPruneResult;
45
+ export declare function pruneRegistry(readActs: (squarePath: string) => StoredAct[] | undefined | Promise<StoredAct[] | undefined>, now?: number): Promise<RegistryPruneResult>;
57
46
  export interface LocalSessionIdentity {
58
47
  sessionId: string;
59
48
  channel: SessionChannel;
@@ -61,9 +50,8 @@ export interface LocalSessionIdentity {
61
50
  paseoAgentId?: string;
62
51
  }
63
52
  export declare function localSessionIdentities(env?: NodeJS.ProcessEnv): LocalSessionIdentity[];
64
- /** True when this process belongs to a harness that can deliver Square attention without a foreground catch. */
65
53
  export declare function hasAutomaticDeliveryIdentity(env?: NodeJS.ProcessEnv): boolean;
66
- export declare function recordLocalJoin(name: string, squarePath: string, env?: NodeJS.ProcessEnv): void;
67
- export declare function recordLocalDone(name: string, squarePath: string, env?: NodeJS.ProcessEnv): void;
68
- export declare function recordSessionJoin(sessionId: string, name: string, squarePath: string, channel: SessionChannel, env?: NodeJS.ProcessEnv): string;
69
- export declare function recordSessionDone(sessionId: string, name: string, squarePath: string, channel: SessionChannel, env?: NodeJS.ProcessEnv): boolean;
54
+ export declare function recordLocalJoin(name: string, squarePath: string, env?: NodeJS.ProcessEnv): Promise<void>;
55
+ export declare function recordLocalDone(name: string, squarePath: string, env?: NodeJS.ProcessEnv): Promise<void>;
56
+ export declare function recordSessionJoin(sessionId: string, name: string, squarePath: string, channel: SessionChannel, env?: NodeJS.ProcessEnv): Promise<string>;
57
+ export declare function recordSessionDone(sessionId: string, name: string, squarePath: string, channel: SessionChannel, env?: NodeJS.ProcessEnv): Promise<boolean>;