@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
@@ -7,10 +7,10 @@ import { type SquareState } from './model.js';
7
7
  * protects it.
8
8
  */
9
9
  export { createSquareState, };
10
- export declare function readSquareFile(squarePath: string): SquareState;
11
- export declare function probeSquareFile(squarePath: string): SquareState | undefined;
10
+ export declare function readSquareFile(squarePath: string): Promise<SquareState>;
11
+ export declare function probeSquareFile(squarePath: string): Promise<SquareState | undefined>;
12
12
  export declare function diagnoseSquareFile(squarePath: string): ReturnType<typeof diagnoseArtifactFile>;
13
- export declare function writeSquareSnapshot(squarePath: string, squareState: SquareState): void;
13
+ export declare function writeSquareSnapshot(squarePath: string, squareState: SquareState): Promise<void>;
14
14
  export declare function withSquareFileLock<T>(squarePath: string, fn: () => T | Promise<T>): Promise<T>;
15
15
  /** In-process cell for fast application tests and embedded consumers. */
16
16
  export declare function createMemoryCell(initial: SquareState): StateCell;
@@ -9,17 +9,17 @@ import { LOCK_RETRY_MS, LOCK_STALE_MS } from './runtime.js';
9
9
  * protects it.
10
10
  */
11
11
  export { createSquareState, };
12
- export function readSquareFile(squarePath) {
12
+ export async function readSquareFile(squarePath) {
13
13
  return loadSquare(squarePath);
14
14
  }
15
- export function probeSquareFile(squarePath) {
15
+ export async function probeSquareFile(squarePath) {
16
16
  return probeSquare(squarePath);
17
17
  }
18
- export function diagnoseSquareFile(squarePath) {
18
+ export async function diagnoseSquareFile(squarePath) {
19
19
  return diagnoseArtifactFile(squarePath);
20
20
  }
21
- export function writeSquareSnapshot(squarePath, squareState) {
22
- writeSquareFile(squarePath, squareState);
21
+ export async function writeSquareSnapshot(squarePath, squareState) {
22
+ await writeSquareFile(squarePath, squareState);
23
23
  }
24
24
  export function withSquareFileLock(squarePath, fn) {
25
25
  return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, fn);
@@ -102,9 +102,9 @@ export function createMemoryCell(initial) {
102
102
  };
103
103
  return cell;
104
104
  }
105
- function fileFingerprint(squarePath) {
105
+ async function fileFingerprint(squarePath) {
106
106
  try {
107
- const stat = fs.statSync(squarePath);
107
+ const stat = await fs.promises.stat(squarePath);
108
108
  return `${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
109
109
  }
110
110
  catch {
@@ -115,36 +115,39 @@ function fileFingerprint(squarePath) {
115
115
  export function createFileCell(squarePath) {
116
116
  let closed = false;
117
117
  let version = 0;
118
- let fingerprint = fileFingerprint(squarePath);
118
+ let fingerprint;
119
119
  let cached;
120
- function observe() {
121
- const next = fileFingerprint(squarePath);
122
- if (next !== fingerprint) {
120
+ async function observe() {
121
+ const next = await fileFingerprint(squarePath);
122
+ if (fingerprint === undefined) {
123
+ fingerprint = next;
124
+ }
125
+ else if (next !== fingerprint) {
123
126
  fingerprint = next;
124
127
  cached = undefined;
125
128
  version += 1;
126
129
  }
127
130
  return fingerprint;
128
131
  }
129
- function currentState() {
130
- const observed = observe();
132
+ async function currentState() {
133
+ const observed = await observe();
131
134
  if (cached?.fingerprint === observed)
132
135
  return cloneState(cached.state);
133
- const decoded = readSquareFile(squarePath);
136
+ const decoded = await readSquareFile(squarePath);
134
137
  cached = { fingerprint: observed, state: cloneState(decoded) };
135
138
  return cloneState(cached.state);
136
139
  }
137
140
  return {
138
141
  async transact(fn) {
139
142
  assertCellOpen(closed);
140
- return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
143
+ return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, async () => {
141
144
  assertCellOpen(closed);
142
- const current = currentState();
145
+ const current = await currentState();
143
146
  const working = cloneState(current);
144
147
  const outcome = fn(working, version);
145
148
  if (outcome.state !== undefined) {
146
- writeSquareSnapshot(squarePath, outcome.state);
147
- fingerprint = fileFingerprint(squarePath);
149
+ await writeSquareSnapshot(squarePath, outcome.state);
150
+ fingerprint = await fileFingerprint(squarePath);
148
151
  cached = { fingerprint, state: cloneState(outcome.state) };
149
152
  version += 1;
150
153
  }
@@ -153,13 +156,13 @@ export function createFileCell(squarePath) {
153
156
  },
154
157
  async read() {
155
158
  assertCellOpen(closed);
156
- return { state: currentState(), version };
159
+ return { state: await currentState(), version };
157
160
  },
158
161
  async changed(sinceVersion, timeoutMs) {
159
162
  assertCellOpen(closed);
160
163
  const deadline = Date.now() + Math.max(0, timeoutMs);
161
164
  while (true) {
162
- observe();
165
+ await observe();
163
166
  if (version > sinceVersion)
164
167
  return true;
165
168
  const remaining = deadline - Date.now();
@@ -73,7 +73,7 @@ export class Square {
73
73
  snapshot() { return snapshot(this.square); }
74
74
  history(query) { return history(this.square, query); }
75
75
  async recognize(env) {
76
- const registered = localParticipantName(this.square.location, env);
76
+ const registered = await localParticipantName(this.square.location, env);
77
77
  if (registered === undefined)
78
78
  return null;
79
79
  const canonicalName = await currentParticipant(this.square, registered);
@@ -16,24 +16,24 @@ export interface WakeAttempt {
16
16
  diagnostic?: unknown;
17
17
  }
18
18
  export declare function wakeAttemptsPath(env?: NodeJS.ProcessEnv): string;
19
- export declare function wakeAttentionKey(attention: WakeAttention): string;
19
+ export declare function wakeAttentionKey(attention: WakeAttention): Promise<string>;
20
20
  export declare function readWakeAttempts(opts?: {
21
21
  attention?: WakeAttention;
22
22
  now?: number;
23
23
  env?: NodeJS.ProcessEnv;
24
- }): WakeAttempt[];
24
+ }): Promise<WakeAttempt[]>;
25
25
  export declare function terminalWakeEvidence(attempts: readonly WakeAttempt[]): WakeAttempt | undefined;
26
26
  export declare function terminalWakeAttempt(attention: WakeAttention, opts?: {
27
27
  now?: number;
28
28
  env?: NodeJS.ProcessEnv;
29
- }): WakeAttempt | undefined;
29
+ }): Promise<WakeAttempt | undefined>;
30
30
  export declare function isWakeRouteAttemptable(route: Pick<WakeRoute, 'kind' | 'updatedAt'>, attempts: readonly WakeAttempt[]): boolean;
31
31
  export declare function hasAttemptableWakeRoute(routes: readonly Pick<WakeRoute, 'kind' | 'updatedAt'>[], attempts: readonly WakeAttempt[]): boolean;
32
32
  export declare function nextWakeAttemptNumber(attention: WakeAttention, opts?: {
33
33
  now?: number;
34
34
  env?: NodeJS.ProcessEnv;
35
- }): number;
35
+ }): Promise<number>;
36
36
  export declare function recordWakeAttempt(attempt: Omit<WakeAttempt, 'at'> & {
37
37
  at?: number;
38
- }, env?: NodeJS.ProcessEnv): WakeAttempt;
39
- export declare function recordRecoveredUnknown(attention: WakeAttention, lease: Pick<NotifyLease, 'attemptN' | 'routeKind'>, env?: NodeJS.ProcessEnv, at?: number): WakeAttempt | undefined;
38
+ }, env?: NodeJS.ProcessEnv): Promise<WakeAttempt>;
39
+ export declare function recordRecoveredUnknown(attention: WakeAttention, lease: Pick<NotifyLease, 'attemptN' | 'routeKind'>, env?: NodeJS.ProcessEnv, at?: number): Promise<WakeAttempt | undefined>;
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { withFileLockSync } from './file-lock.js';
4
+ import { withFileLock } from './file-lock.js';
5
5
  import { isWakeRouteKind, nameKey } from './model.js';
6
6
  import { canonicalSquarePath } from './registry.js';
7
7
  import { formatActivityId, parseActivityId } from './square-core.js';
@@ -12,8 +12,8 @@ const VALID_OUTCOMES = new Set(['accepted', 'unknown', 'failed']);
12
12
  export function wakeAttemptsPath(env = process.env) {
13
13
  return env.SQUARE_WAKE_ATTEMPTS || path.join(os.homedir(), '.square', 'wake-attempts.ndjsonl');
14
14
  }
15
- export function wakeAttentionKey(attention) {
16
- return JSON.stringify([canonicalSquarePath(attention.squarePath), formatActivityId(attention.actIndex), nameKey(attention.recipient)]);
15
+ export async function wakeAttentionKey(attention) {
16
+ return JSON.stringify([await canonicalSquarePath(attention.squarePath), formatActivityId(attention.actIndex), nameKey(attention.recipient)]);
17
17
  }
18
18
  function parseRow(raw, now) {
19
19
  let value;
@@ -39,10 +39,10 @@ function parseRow(raw, now) {
39
39
  return undefined;
40
40
  return row;
41
41
  }
42
- function readRowsFromFile(filePath, now) {
42
+ async function readRowsFromFile(filePath, now) {
43
43
  let raw;
44
44
  try {
45
- raw = fs.readFileSync(filePath, 'utf8');
45
+ raw = await fs.promises.readFile(filePath, 'utf8');
46
46
  }
47
47
  catch (error) {
48
48
  if (error.code === 'ENOENT')
@@ -51,25 +51,25 @@ function readRowsFromFile(filePath, now) {
51
51
  }
52
52
  return raw.split('\n').filter(Boolean).map((line) => parseRow(line, now)).filter((row) => row !== undefined);
53
53
  }
54
- function readRows(env, now) {
54
+ async function readRows(env, now) {
55
55
  return readRowsFromFile(wakeAttemptsPath(env), now);
56
56
  }
57
- function writeRows(filePath, rows) {
58
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
57
+ async function writeRows(filePath, rows) {
58
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
59
59
  const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
60
- fs.writeFileSync(temporary, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), {
60
+ await fs.promises.writeFile(temporary, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), {
61
61
  mode: 0o600,
62
62
  });
63
- fs.renameSync(temporary, filePath);
63
+ await fs.promises.rename(temporary, filePath);
64
64
  }
65
- function fromRow(row) {
65
+ async function fromRow(row) {
66
66
  const actIndex = parseActivityId(row.attention.act_id);
67
67
  if (actIndex === undefined)
68
68
  throw new Error(`Invalid wake activity id: ${row.attention.act_id}`);
69
69
  return {
70
70
  at: row.ts,
71
71
  attention: {
72
- squarePath: canonicalSquarePath(row.attention.square_path),
72
+ squarePath: await canonicalSquarePath(row.attention.square_path),
73
73
  actIndex,
74
74
  recipient: row.attention.recipient,
75
75
  },
@@ -81,18 +81,20 @@ function fromRow(row) {
81
81
  ...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }),
82
82
  };
83
83
  }
84
- export function readWakeAttempts(opts = {}) {
84
+ export async function readWakeAttempts(opts = {}) {
85
85
  const now = opts.now ?? Date.now();
86
- const expected = opts.attention === undefined ? undefined : wakeAttentionKey(opts.attention);
87
- return readRows(opts.env ?? process.env, now)
88
- .map(fromRow)
89
- .filter((attempt) => expected === undefined || wakeAttentionKey(attempt.attention) === expected);
86
+ const expected = opts.attention === undefined ? undefined : await wakeAttentionKey(opts.attention);
87
+ const attempts = await Promise.all((await readRows(opts.env ?? process.env, now)).map(fromRow));
88
+ if (expected === undefined)
89
+ return attempts;
90
+ const keys = await Promise.all(attempts.map((attempt) => wakeAttentionKey(attempt.attention)));
91
+ return attempts.filter((_, index) => keys[index] === expected);
90
92
  }
91
93
  export function terminalWakeEvidence(attempts) {
92
94
  return attempts.findLast((attempt) => attempt.outcome === 'accepted' || attempt.outcome === 'unknown');
93
95
  }
94
- export function terminalWakeAttempt(attention, opts = {}) {
95
- return terminalWakeEvidence(readWakeAttempts({ attention, ...opts }));
96
+ export async function terminalWakeAttempt(attention, opts = {}) {
97
+ return terminalWakeEvidence(await readWakeAttempts({ attention, ...opts }));
96
98
  }
97
99
  export function isWakeRouteAttemptable(route, attempts) {
98
100
  if (terminalWakeEvidence(attempts) !== undefined)
@@ -103,8 +105,8 @@ export function isWakeRouteAttemptable(route, attempts) {
103
105
  export function hasAttemptableWakeRoute(routes, attempts) {
104
106
  return routes.some((route) => isWakeRouteAttemptable(route, attempts));
105
107
  }
106
- export function nextWakeAttemptNumber(attention, opts = {}) {
107
- return readWakeAttempts({ attention, ...opts }).reduce((highest, attempt) => Math.max(highest, attempt.attemptN), 0) + 1;
108
+ export async function nextWakeAttemptNumber(attention, opts = {}) {
109
+ return (await readWakeAttempts({ attention, ...opts })).reduce((highest, attempt) => Math.max(highest, attempt.attemptN), 0) + 1;
108
110
  }
109
111
  function redact(value, secret) {
110
112
  if (typeof value === 'string') {
@@ -118,13 +120,13 @@ function redact(value, secret) {
118
120
  }
119
121
  return value;
120
122
  }
121
- function toRow(attempt, env) {
123
+ async function toRow(attempt, env) {
122
124
  const safe = redact(attempt, env.PASEO_PASSWORD);
123
125
  return {
124
126
  v: 1,
125
127
  ts: safe.at,
126
128
  attention: {
127
- square_path: canonicalSquarePath(safe.attention.squarePath),
129
+ square_path: await canonicalSquarePath(safe.attention.squarePath),
128
130
  act_id: formatActivityId(safe.attention.actIndex),
129
131
  recipient: safe.attention.recipient,
130
132
  },
@@ -136,7 +138,7 @@ function toRow(attempt, env) {
136
138
  ...(safe.diagnostic === undefined ? {} : { diagnostic: safe.diagnostic }),
137
139
  };
138
140
  }
139
- export function recordWakeAttempt(attempt, env = process.env) {
141
+ export async function recordWakeAttempt(attempt, env = process.env) {
140
142
  const value = { ...attempt, at: attempt.at ?? Date.now() };
141
143
  if (!isWakeRouteKind(value.routeKind))
142
144
  throw new Error('Wake attempts require a real adapter route kind.');
@@ -144,19 +146,25 @@ export function recordWakeAttempt(attempt, env = process.env) {
144
146
  throw new Error(`${value.outcome} wake attempts require a transport signature.`);
145
147
  }
146
148
  const file = wakeAttemptsPath(env);
147
- withFileLockSync(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
148
- writeRows(file, [...readRowsFromFile(file, value.at), toRow(value, env)]);
149
+ await withFileLock(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, async () => {
150
+ await writeRows(file, [...await readRowsFromFile(file, value.at), await toRow(value, env)]);
149
151
  });
150
152
  return value;
151
153
  }
152
- export function recordRecoveredUnknown(attention, lease, env = process.env, at = Date.now()) {
154
+ export async function recordRecoveredUnknown(attention, lease, env = process.env, at = Date.now()) {
153
155
  const routeKind = lease.routeKind;
154
156
  if (lease.attemptN === undefined || !isWakeRouteKind(routeKind))
155
157
  return undefined;
156
158
  const file = wakeAttemptsPath(env);
157
- return withFileLockSync(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
158
- const rows = readRowsFromFile(file, at);
159
- const attempts = rows.map(fromRow).filter((attempt) => wakeAttentionKey(attempt.attention) === wakeAttentionKey(attention));
159
+ return withFileLock(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, async () => {
160
+ const rows = await readRowsFromFile(file, at);
161
+ const expected = await wakeAttentionKey(attention);
162
+ const attempts = [];
163
+ for (const row of rows) {
164
+ const attempt = await fromRow(row);
165
+ if (await wakeAttentionKey(attempt.attention) === expected)
166
+ attempts.push(attempt);
167
+ }
160
168
  const terminal = terminalWakeEvidence(attempts);
161
169
  if (terminal !== undefined)
162
170
  return terminal;
@@ -169,7 +177,7 @@ export function recordRecoveredUnknown(attention, lease, env = process.env, at =
169
177
  attemptN: lease.attemptN,
170
178
  message: 'The notification worker ended after dispatch began; transport acceptance is unknown.',
171
179
  };
172
- writeRows(file, [...rows, toRow(value, env)]);
180
+ await writeRows(file, [...rows, await toRow(value, env)]);
173
181
  return value;
174
182
  });
175
183
  }
@@ -15,7 +15,7 @@ export interface WakeEvidenceProjection {
15
15
  }
16
16
  /** Capture the primary wake facts once and derive any number of eligibility decisions from them. */
17
17
  export declare function wakeEvidenceProjection(squarePath: string, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidenceProjection>;
18
- export declare function wakeEvidenceProjectionFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, delivery?: DeliveryModel): WakeEvidenceProjection;
18
+ export declare function wakeEvidenceProjectionFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, delivery?: DeliveryModel): Promise<WakeEvidenceProjection>;
19
19
  /** Project every wake decision from the same primary evidence. */
20
20
  export declare function wakeEvidence(squarePath: string, recipient: string, actIndex: number, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidence>;
21
21
  export declare function wakeIsEligible(evidence: WakeEvidence): boolean;
@@ -8,13 +8,13 @@ import { openSquare } from './square-file-adapter.js';
8
8
  import { closeOpenSquare } from './open-square.js';
9
9
  import { observationFor } from './runtime.js';
10
10
  import { entryPresentation } from './views.js';
11
- function attentionKey(squarePath, recipient, actIndex) {
12
- return JSON.stringify([canonicalSquarePath(squarePath), nameKey(recipient), actIndex]);
11
+ async function attentionKey(squarePath, recipient, actIndex) {
12
+ return JSON.stringify([await canonicalSquarePath(squarePath), nameKey(recipient), actIndex]);
13
13
  }
14
- function projectionFromState(squarePath, state, now, env, delivery = deriveDeliveryModel(state)) {
15
- const canonicalPath = canonicalSquarePath(squarePath);
14
+ async function projectionFromState(squarePath, state, now, env, delivery = deriveDeliveryModel(state)) {
15
+ const canonicalPath = await canonicalSquarePath(squarePath);
16
16
  const owners = new Map();
17
- for (const binding of readActiveBindings(now)) {
17
+ for (const binding of await readActiveBindings(now)) {
18
18
  if (binding.squarePath !== canonicalPath)
19
19
  continue;
20
20
  const key = nameKey(binding.name);
@@ -23,21 +23,21 @@ function projectionFromState(squarePath, state, now, env, delivery = deriveDeliv
23
23
  owners.set(key, recipientOwners);
24
24
  }
25
25
  const routesByOwner = new Map();
26
- for (const route of readWakeRoutes({ freshOnly: true, now, env })) {
26
+ for (const route of await readWakeRoutes({ freshOnly: true, now, env })) {
27
27
  const routes = routesByOwner.get(route.ownerId) ?? [];
28
28
  routes.push(route);
29
29
  routesByOwner.set(route.ownerId, routes);
30
30
  }
31
31
  const attemptsByAttention = new Map();
32
- for (const attempt of readWakeAttempts({ env, now })) {
33
- const key = attentionKey(attempt.attention.squarePath, attempt.attention.recipient, attempt.attention.actIndex);
32
+ for (const attempt of await readWakeAttempts({ env, now })) {
33
+ const key = await attentionKey(attempt.attention.squarePath, attempt.attention.recipient, attempt.attention.actIndex);
34
34
  const attempts = attemptsByAttention.get(key) ?? [];
35
35
  attempts.push(attempt);
36
36
  attemptsByAttention.set(key, attempts);
37
37
  }
38
38
  const presentedByAttention = new Map();
39
- for (const presented of readPresentedAttentions(env, now)) {
40
- const key = attentionKey(presented.squarePath, presented.name, presented.actIndex);
39
+ for (const presented of await readPresentedAttentions(env, now)) {
40
+ const key = await attentionKey(presented.squarePath, presented.name, presented.actIndex);
41
41
  const presentedOwners = presentedByAttention.get(key) ?? new Set();
42
42
  presentedOwners.add(presented.ownerId);
43
43
  presentedByAttention.set(key, presentedOwners);
@@ -45,7 +45,7 @@ function projectionFromState(squarePath, state, now, env, delivery = deriveDeliv
45
45
  return {
46
46
  evidence(recipient, actIndex) {
47
47
  const recipientOwners = owners.get(nameKey(recipient)) ?? new Set();
48
- const key = attentionKey(squarePath, recipient, actIndex);
48
+ const key = JSON.stringify([canonicalPath, nameKey(recipient), actIndex]);
49
49
  const attempts = attemptsByAttention.get(key) ?? [];
50
50
  const terminal = terminalWakeEvidence(attempts);
51
51
  const routes = [...recipientOwners].flatMap((ownerId) => routesByOwner.get(ownerId) ?? []);
@@ -1,7 +1,7 @@
1
1
  import type { WakeAdapter, WakeDispatchResult } from './delivery.js';
2
2
  import type { WakeRoute } from './model.js';
3
3
  export interface WakePortHooks {
4
- nextAttemptN(): number;
4
+ nextAttemptN(): number | Promise<number>;
5
5
  beforeSend(route: WakeRoute, attemptN: number): Promise<boolean>;
6
6
  record(route: WakeRoute, attemptN: number, result: Exclude<WakeDispatchResult, {
7
7
  outcome: 'cancelled' | 'unavailable';
package/dist/wake-port.js CHANGED
@@ -9,7 +9,7 @@ export class WakePort {
9
9
  const adapter = this.adapters.get(route.kind);
10
10
  if (adapter === undefined)
11
11
  continue;
12
- const attemptN = hooks.nextAttemptN();
12
+ const attemptN = await hooks.nextAttemptN();
13
13
  const result = await adapter.dispatch(route.address, typeof payload === 'function' ? payload(route) : payload, () => hooks.beforeSend(route, attemptN));
14
14
  if (result.outcome === 'cancelled')
15
15
  return result;
package/dist/watch.js CHANGED
@@ -87,7 +87,7 @@ async function finishWatchResult(square, squarePath, name, result, leaseId, idle
87
87
  }
88
88
  async function beginWatch(square, squarePath, name, opts) {
89
89
  const id = leaseId();
90
- const ownerId = localParticipantOwner(squarePath, name);
90
+ const ownerId = await localParticipantOwner(squarePath, name);
91
91
  return acquireWatchLease(square, name, id, opts, ownerId);
92
92
  }
93
93
  async function endWatch(square, name, id) {
@@ -4,6 +4,12 @@ import { waitForSessionPending } from '../dist/inbox.js';
4
4
  import { lookupSessionBindings } from '../dist/registry.js';
5
5
 
6
6
  const PI_SEND_TIMEOUT_MS = 5_000;
7
+ const DEFAULT_PI_BOUNDARY_TIMEOUT_MS = 2_000;
8
+
9
+ function piBoundaryTimeoutMs() {
10
+ const configured = Number.parseInt(process.env.SQUARE_PI_BOUNDARY_TIMEOUT_MS || '', 10);
11
+ return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PI_BOUNDARY_TIMEOUT_MS;
12
+ }
7
13
 
8
14
  export function pendingInbox(inbox) {
9
15
  return inbox.filter((item) => item.notifications?.length > 0);
@@ -32,7 +38,18 @@ export default function squarePiExtension(pi) {
32
38
  let settledWaiters = [];
33
39
  const handledPending = new Set();
34
40
  let retryAfterChange = false;
35
- const present = (deliver) => sessionId === undefined ? undefined : presentPendingAtBoundary(sessionId, deliver);
41
+ const present = (deliver, signal) => sessionId === undefined ? undefined : presentPendingAtBoundary(sessionId, deliver, undefined, undefined, signal);
42
+
43
+ const presentAtBoundary = (deliver) => {
44
+ const controller = new AbortController();
45
+ const timer = setTimeout(() => controller.abort(new Error('Pi boundary presentation timed out')), piBoundaryTimeoutMs());
46
+ const pending = present(deliver, controller.signal);
47
+ if (pending === undefined) {
48
+ clearTimeout(timer);
49
+ return undefined;
50
+ }
51
+ return pending.finally(() => clearTimeout(timer));
52
+ };
36
53
 
37
54
  const waitForSettled = (serial, signal) => {
38
55
  if (settledSerial !== serial) return Promise.resolve();
@@ -67,7 +84,7 @@ export default function squarePiExtension(pi) {
67
84
 
68
85
  const wake = async (piContext, token, signal) => {
69
86
  while (sessionId !== undefined && token === generation && !signal.aborted) {
70
- if (lookupSessionBindings(sessionId).length === 0) {
87
+ if ((await lookupSessionBindings(sessionId)).length === 0) {
71
88
  await pause(signal, 1_000);
72
89
  continue;
73
90
  }
@@ -103,6 +120,13 @@ export default function squarePiExtension(pi) {
103
120
  try {
104
121
  await Promise.race([
105
122
  send,
123
+ new Promise((_, reject) => {
124
+ if (signal.aborted) {
125
+ reject(signal.reason || new Error('Pi native injection aborted'));
126
+ return;
127
+ }
128
+ signal.addEventListener('abort', () => reject(signal.reason || new Error('Pi native injection aborted')), { once: true });
129
+ }),
106
130
  new Promise((_, reject) => {
107
131
  timer = setTimeout(() => reject(new Error('Pi native injection timed out')), PI_SEND_TIMEOUT_MS);
108
132
  }),
@@ -112,6 +136,9 @@ export default function squarePiExtension(pi) {
112
136
  }
113
137
  return true;
114
138
  },
139
+ undefined,
140
+ undefined,
141
+ signal,
115
142
  );
116
143
  if (delivered === true || delivered === undefined) {
117
144
  for (const key of keys) handledPending.add(key);
@@ -156,7 +183,7 @@ export default function squarePiExtension(pi) {
156
183
  return { message: { customType: 'square', content: context, display: true } };
157
184
  }
158
185
  if (presenting) return undefined;
159
- return present((context) => ({ message: { customType: 'square', content: context, display: true } }));
186
+ return await presentAtBoundary((context) => ({ message: { customType: 'square', content: context, display: true } }));
160
187
  } catch {
161
188
  return undefined;
162
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.27",
3
+ "version": "0.3.29",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,6 +52,10 @@
52
52
  "./paseo": {
53
53
  "types": "./dist/paseo.d.ts",
54
54
  "default": "./dist/paseo.js"
55
+ },
56
+ "./server": {
57
+ "types": "./dist/opencode.d.ts",
58
+ "default": "./dist/opencode.js"
55
59
  }
56
60
  },
57
61
  "main": "dist/index.js",
@@ -1,48 +0,0 @@
1
- import { presentPendingAtBoundary } from '../dist/boundary-presentation.js';
2
- import { automaticSessionEnd, automaticSessionStart } from '../dist/automatic-session.js';
3
-
4
- export default async function squareOpenCodePlugin() {
5
- const joining = new Map();
6
- return {
7
- event: async ({ event }) => {
8
- if (event.type === 'session.created' || event.type === 'session.updated') {
9
- const sessionID = event.properties?.sessionID;
10
- const cwd = event.properties?.info?.directory || process.cwd();
11
- if (sessionID) {
12
- try {
13
- const context = await automaticSessionStart('opencode', sessionID, cwd);
14
- if (context) joining.set(sessionID, context);
15
- } catch { /* startup remains bounded */ }
16
- }
17
- } else if (event.type === 'session.deleted') {
18
- const sessionID = event.properties?.sessionID;
19
- const cwd = event.properties?.info?.directory || process.cwd();
20
- if (sessionID) {
21
- joining.delete(sessionID);
22
- await automaticSessionEnd('opencode', sessionID, cwd);
23
- }
24
- }
25
- },
26
- 'shell.env': async (input, output) => {
27
- if (input.sessionID) output.env.OPENCODE_SESSION_ID = input.sessionID;
28
- },
29
-
30
- 'tool.execute.after': async (input, output) => {
31
- try {
32
- const joined = joining.get(input.sessionID);
33
- if (joined) {
34
- joining.delete(input.sessionID);
35
- output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${joined}`;
36
- }
37
- await presentPendingAtBoundary(
38
- input.sessionID,
39
- (context) => {
40
- output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${context}`;
41
- }
42
- );
43
- } catch {
44
- // A failed admission remains available at a later boundary.
45
- }
46
- },
47
- };
48
- }