@astrosheep/square 0.3.32 → 0.3.33

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.32",
3
+ "version": "0.3.33",
4
4
  "description": "Native Claude Code turn-boundary delivery for Square participants",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.32",
3
+ "version": "0.3.33",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,7 +1,7 @@
1
1
  import { presentPendingAtBoundary } from './boundary-presentation.js';
2
2
  import { sessionInbox } from './inbox.js';
3
3
  import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
4
- import { sweepPrivilegedPending } from './notifications.js';
4
+ import { PRIVILEGED_HOOK_BUDGET_MS, sweepPrivilegedPending } from './notifications.js';
5
5
  export async function runClaudeHookAsync(inputText, env = process.env) {
6
6
  let input;
7
7
  try {
@@ -36,12 +36,13 @@ export async function runClaudeHookAsync(inputText, env = process.env) {
36
36
  return runClaudeHook(inputText, env);
37
37
  }
38
38
  export async function claudeHookResponse(input, lookup = sessionInbox, env = process.env, deliveryAdapters) {
39
+ const sweepDeadline = Date.now() + PRIVILEGED_HOOK_BUDGET_MS;
39
40
  if (typeof input.session_id !== 'string' || input.session_id === '')
40
41
  return undefined;
41
42
  if (input.hook_event_name !== 'PostToolBatch')
42
43
  return undefined;
43
44
  const response = await presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: context } }), lookup, env);
44
- await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters).catch(() => undefined);
45
+ await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters, sweepDeadline).catch(() => undefined);
45
46
  return response;
46
47
  }
47
48
  export async function runClaudeHook(inputText, env = process.env) {
@@ -2,12 +2,13 @@ import { presentPendingAtBoundary } from './boundary-presentation.js';
2
2
  import { sessionInbox } from './inbox.js';
3
3
  import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
4
4
  import { clearCodexBoundary, recordCodexBoundary } from './codex-boundary-state.js';
5
- import { sweepPrivilegedPending } from './notifications.js';
5
+ import { PRIVILEGED_HOOK_BUDGET_MS, sweepPrivilegedPending } from './notifications.js';
6
6
  const CODEX_HOOK_EVENTS = {
7
7
  PostToolUse: 'PostToolUse',
8
8
  Stop: 'Stop',
9
9
  };
10
10
  export async function codexHookResponse(input, lookup = sessionInbox, env = process.env, deliveryAdapters) {
11
+ const sweepDeadline = Date.now() + PRIVILEGED_HOOK_BUDGET_MS;
11
12
  if (typeof input.session_id !== 'string' || input.session_id === '')
12
13
  return undefined;
13
14
  if (typeof input.hook_event_name !== 'string')
@@ -19,7 +20,7 @@ export async function codexHookResponse(input, lookup = sessionInbox, env = proc
19
20
  const response = await presentPendingAtBoundary(input.session_id, (context) => hookEventName === 'Stop'
20
21
  ? { systemMessage: context }
21
22
  : { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }, lookup, env);
22
- await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters).catch(() => undefined);
23
+ await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters, sweepDeadline).catch(() => undefined);
23
24
  return response;
24
25
  }
25
26
  export async function runCodexHook(inputText, env = process.env) {
@@ -22,6 +22,6 @@ export declare class CodexQueueAdapter implements WakeAdapter {
22
22
  private readonly opts;
23
23
  readonly kind: "codex-queue";
24
24
  constructor(opts?: CodexQueueAdapterOptions);
25
- dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>): Promise<WakeDispatchResult>;
25
+ dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number): Promise<WakeDispatchResult>;
26
26
  }
27
27
  export type { WakeRoute };
@@ -33,7 +33,9 @@ export class CodexQueueAdapter {
33
33
  constructor(opts = {}) {
34
34
  this.opts = opts;
35
35
  }
36
- async dispatch(address, payload, beforeSend) {
36
+ async dispatch(address, payload, beforeSend, timeoutMs = 5000) {
37
+ const deadline = Date.now() + timeoutMs;
38
+ const remainingMs = () => Math.max(0, deadline - Date.now());
37
39
  const threadId = address.threadId?.trim();
38
40
  if (!threadId) {
39
41
  return { outcome: 'unavailable', signature: 'invalid_address', message: 'Codex route has no thread id.', routeStale: true };
@@ -57,8 +59,12 @@ export class CodexQueueAdapter {
57
59
  retainRoute: true,
58
60
  };
59
61
  }
62
+ const remaining = remainingMs();
63
+ if (remaining === 0) {
64
+ return { outcome: 'unavailable', signature: 'dispatch_budget_exhausted', message: 'The wake dispatch budget elapsed before queueing.', retainRoute: true };
65
+ }
60
66
  try {
61
- (this.opts.sendQueue ?? sendCodexQueue)({ threadId, message: payload }, { env });
67
+ (this.opts.sendQueue ?? sendCodexQueue)({ threadId, message: payload }, { env, timeoutMs: remaining });
62
68
  return { outcome: 'accepted' };
63
69
  }
64
70
  catch (error) {
@@ -4,6 +4,19 @@ import { nameKey } from './model.js';
4
4
  import { deriveDeliveryModel } from './delivery.js';
5
5
  import { isWakeRouteAttemptable } from './square-projections.js';
6
6
  import { retireWakeRouteFromArtifact } from './routes.js';
7
+ async function attemptWakeWithin(transport, request, timeoutMs) {
8
+ let timer;
9
+ const timeout = new Promise((resolve) => {
10
+ timer = setTimeout(() => resolve({ outcome: 'unknown', diagnostic: 'transport timeout' }), timeoutMs);
11
+ });
12
+ try {
13
+ return await Promise.race([transport.attempt(request, timeoutMs), timeout]);
14
+ }
15
+ finally {
16
+ if (timer !== undefined)
17
+ clearTimeout(timer);
18
+ }
19
+ }
7
20
  export async function observeSquare(input) {
8
21
  const snapshot = await input.artifact.read();
9
22
  const delivery = deriveDeliveryModel(snapshot.state);
@@ -128,7 +141,7 @@ export async function deliverPending(input) {
128
141
  continue;
129
142
  }
130
143
  try {
131
- outcome = await Promise.race([input.transport.attempt(request, leaseMs), new Promise((resolve) => setTimeout(() => resolve({ outcome: 'unknown', diagnostic: 'transport timeout' }), leaseMs))]);
144
+ outcome = await attemptWakeWithin(input.transport, request, leaseMs);
132
145
  }
133
146
  catch (error) {
134
147
  outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
@@ -41,7 +41,7 @@ export type WakeDispatchResult = {
41
41
  };
42
42
  export interface WakeAdapter {
43
43
  readonly kind: WakeRouteKind;
44
- dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>): Promise<WakeDispatchResult>;
44
+ dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number): Promise<WakeDispatchResult>;
45
45
  }
46
46
  export interface RoutedNotification {
47
47
  actor: string;
@@ -6,6 +6,7 @@ import type { WakeTransportPort } from './ports.js';
6
6
  export type { PlannedNotification } from './delivery.js';
7
7
  export { planActNotifications, matchesMentionTarget };
8
8
  export { notificationMessageId } from './delivery.js';
9
+ export declare const PRIVILEGED_HOOK_BUDGET_MS = 3000;
9
10
  export declare function wakeGraceMs(env?: NodeJS.ProcessEnv): number;
10
11
  export declare function hasDeliveredNotification(squarePath: string, name: string, ref: number | ActivityId): Promise<boolean>;
11
12
  export declare function hasAttentionNotification(squarePath: string, name: string, ref: number | ActivityId, env?: NodeJS.ProcessEnv): Promise<boolean>;
@@ -21,8 +22,8 @@ export declare function defaultWakeAdapters(): Promise<WakeAdapter[]>;
21
22
  export declare function createDefaultWakeTransport(hostLedger: import('./host-ledger.js').HostLedgerPort, clock: () => number, env?: NodeJS.ProcessEnv): Promise<WakeTransportPort>;
22
23
  export declare function createWakeTransport(adapters: readonly WakeAdapter[], hostLedger: import('./host-ledger.js').HostLedgerPort, clock: () => number): WakeTransportPort;
23
24
  export declare function processActNotificationsOnce(squarePath: string, actIndex: number, opts?: ProcessNotificationOptions): Promise<import("./ports.js").DeliveryResult>;
24
- /** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares. */
25
- export declare function sweepPrivilegedPending(cwd: string, env?: NodeJS.ProcessEnv, suppliedAdapters?: WakeAdapter[]): Promise<void>;
25
+ /** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares within one boundary budget. */
26
+ export declare function sweepPrivilegedPending(cwd: string, env?: NodeJS.ProcessEnv, suppliedAdapters?: WakeAdapter[], deadline?: number): Promise<void>;
26
27
  export interface SweepPendingNotificationsOptions {
27
28
  env?: NodeJS.ProcessEnv;
28
29
  now?: number;
@@ -14,6 +14,7 @@ import { projectPresentationEvidence } from './square-projections.js';
14
14
  import { createHostLedgerPort } from './host-ledger-file-adapter.js';
15
15
  export { planActNotifications, matchesMentionTarget };
16
16
  export { notificationMessageId } from './delivery.js';
17
+ export const PRIVILEGED_HOOK_BUDGET_MS = 3000;
17
18
  export function wakeGraceMs(env = process.env) {
18
19
  const value = Number.parseInt(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? '5000', 10);
19
20
  if (!Number.isFinite(value) || value <= 0) {
@@ -116,12 +117,12 @@ export function createWakeTransport(adapters, hostLedger, clock) {
116
117
  return { outcome: 'not-capable', diagnostic: error instanceof Error ? error.message : String(error) };
117
118
  }
118
119
  },
119
- attempt: async (request, _timeoutMs) => {
120
+ attempt: async (request, timeoutMs) => {
120
121
  const adapter = adapters.find((candidate) => candidate.kind === request.route.kind);
121
122
  if (adapter === undefined)
122
123
  return { outcome: 'not-capable', diagnostic: `no adapter for ${request.route.kind}` };
123
124
  try {
124
- const result = await adapter.dispatch(request.route.address, renderWakePayload(request), async () => true);
125
+ const result = await adapter.dispatch(request.route.address, renderWakePayload(request), async () => true, timeoutMs);
125
126
  if (result.outcome === 'accepted')
126
127
  return { outcome: 'accepted' };
127
128
  if (result.outcome === 'failed')
@@ -161,8 +162,11 @@ export async function processActNotificationsOnce(squarePath, actIndex, opts = {
161
162
  await closeOpenSquare(square);
162
163
  }
163
164
  }
164
- /** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares. */
165
- export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAdapters) {
165
+ /** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares within one boundary budget. */
166
+ export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAdapters, deadline = Date.now() + PRIVILEGED_HOOK_BUDGET_MS) {
167
+ const remainingMs = () => Math.max(0, deadline - Date.now());
168
+ if (remainingMs() === 0)
169
+ return;
166
170
  const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
167
171
  const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? root, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root, readableScopes: ['user'], writableScope: 'user' });
168
172
  let indexed = [];
@@ -178,19 +182,30 @@ export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAda
178
182
  }
179
183
  }
180
184
  catch { /* no local square directory */ }
185
+ if (remainingMs() === 0)
186
+ return;
181
187
  const adapters = suppliedAdapters ?? await defaultWakeAdapters();
182
188
  for (const squarePath of paths) {
189
+ if (remainingMs() === 0)
190
+ break;
183
191
  try {
184
192
  const square = await openSquare(squarePath, { hostLedger, env });
185
193
  try {
186
- const snapshot = await square.artifact.read();
187
194
  await hostLedger.reconcileBinding({ artifact: square.artifact, scopes: ['user'], now: Date.now() }).catch(() => undefined);
195
+ if (remainingMs() === 0)
196
+ break;
188
197
  const limit = Number.parseInt(env.SQUARE_NOTIFY_SWEEP_LIMIT ?? '8', 10);
189
198
  const graceMs = 0;
190
199
  const selected = await sweepPending({ artifact: square.artifact, hostLedger, location: squarePath, now: Date.now(), graceMs, limit: Number.isFinite(limit) && limit > 0 ? limit : 8 }).catch(() => []);
191
200
  const transport = createWakeTransport(adapters, hostLedger, Date.now);
192
- for (const actIndex of selected)
193
- await deliverPending({ artifact: square.artifact, hostLedger, transport, location: squarePath, activity: actIndex, timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: Date.now() }).catch(() => undefined);
201
+ for (const actIndex of selected) {
202
+ const remaining = remainingMs();
203
+ if (remaining === 0)
204
+ break;
205
+ const configured = Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000);
206
+ const timeoutMs = Math.max(1, Math.min(Number.isFinite(configured) && configured > 0 ? configured : 5000, remaining));
207
+ await deliverPending({ artifact: square.artifact, hostLedger, transport, location: squarePath, activity: actIndex, timeoutMs, now: Date.now() }).catch(() => undefined);
208
+ }
194
209
  }
195
210
  finally {
196
211
  await closeOpenSquare(square);
@@ -10,5 +10,5 @@ export declare class PaseoAdapter implements WakeAdapter {
10
10
  private readonly opts;
11
11
  readonly kind: "paseo";
12
12
  constructor(opts?: PaseoAdapterOptions);
13
- dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>): Promise<WakeDispatchResult>;
13
+ dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number): Promise<WakeDispatchResult>;
14
14
  }
@@ -30,7 +30,15 @@ export class PaseoAdapter {
30
30
  constructor(opts = {}) {
31
31
  this.opts = opts;
32
32
  }
33
- async dispatch(address, payload, beforeSend) {
33
+ async dispatch(address, payload, beforeSend, timeoutMs = 5000) {
34
+ const deadline = Date.now() + timeoutMs;
35
+ const remainingMs = () => Math.max(0, deadline - Date.now());
36
+ const budgetUnavailable = () => ({
37
+ outcome: 'unavailable',
38
+ signature: 'dispatch_budget_exhausted',
39
+ message: 'The wake dispatch budget elapsed before Paseo accepted the wake.',
40
+ retainRoute: true,
41
+ });
34
42
  const agentId = address.agentId?.trim();
35
43
  if (!agentId) {
36
44
  return {
@@ -41,7 +49,10 @@ export class PaseoAdapter {
41
49
  diagnostic: diagnostic('selection', address, 'invalid_address'),
42
50
  };
43
51
  }
44
- const discovery = (this.opts.discover ?? discoverPaseoAgents)();
52
+ let remaining = remainingMs();
53
+ if (remaining === 0)
54
+ return budgetUnavailable();
55
+ const discovery = (this.opts.discover ?? discoverPaseoAgents)(remaining);
45
56
  if (discovery.error && discovery.agents.length === 0) {
46
57
  return {
47
58
  outcome: 'unavailable',
@@ -62,7 +73,10 @@ export class PaseoAdapter {
62
73
  ...(agent === undefined ? { routeStale: true } : {}),
63
74
  };
64
75
  }
65
- if (!(await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent))) {
76
+ remaining = remainingMs();
77
+ if (remaining === 0)
78
+ return budgetUnavailable();
79
+ if (!(await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent, remaining))) {
66
80
  return {
67
81
  outcome: 'unavailable',
68
82
  signature: 'boundary_unavailable',
@@ -73,8 +87,11 @@ export class PaseoAdapter {
73
87
  }
74
88
  if (!(await beforeSend()))
75
89
  return { outcome: 'cancelled' };
90
+ remaining = remainingMs();
91
+ if (remaining === 0)
92
+ return budgetUnavailable();
76
93
  try {
77
- (this.opts.sendWake ?? sendPaseoWake)({ agentId, prompt: payload });
94
+ (this.opts.sendWake ?? sendPaseoWake)({ agentId, prompt: payload }, { timeoutMs: remaining });
78
95
  return { outcome: 'accepted' };
79
96
  }
80
97
  catch (error) {
@@ -8,4 +8,4 @@ export declare function discoverPaseoAgents(timeoutMs?: number): {
8
8
  agents: PaseoAgent[];
9
9
  error?: string;
10
10
  };
11
- export declare function waitForPaseoWakeBoundary(agent: Pick<PaseoAgent, 'id' | 'status'>): Promise<boolean>;
11
+ export declare function waitForPaseoWakeBoundary(agent: Pick<PaseoAgent, 'id' | 'status'>, timeoutMs?: number): Promise<boolean>;
@@ -22,10 +22,10 @@ export function discoverPaseoAgents(timeoutMs = 5000) {
22
22
  return { agents: [], error: error instanceof Error ? error.message : String(error) };
23
23
  }
24
24
  }
25
- export async function waitForPaseoWakeBoundary(agent) {
25
+ export async function waitForPaseoWakeBoundary(agent, timeoutMs = 30_000) {
26
26
  if (agent.status === 'idle')
27
27
  return true;
28
28
  if (agent.status !== 'running')
29
29
  return false;
30
- return waitForPaseoToolBoundary(agent.id);
30
+ return waitForPaseoToolBoundary(agent.id, { timeoutMs });
31
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.32",
3
+ "version": "0.3.33",
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": {