@astrosheep/square 0.3.31 → 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,5 +1,6 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
2
  import path from 'node:path';
3
+ import fs from 'node:fs';
3
4
  import { planActNotifications, } from './delivery.js';
4
5
  import { SquareError } from './model.js';
5
6
  import { SLEEP_MS, matchesMentionTarget } from './runtime.js';
@@ -13,6 +14,7 @@ import { projectPresentationEvidence } from './square-projections.js';
13
14
  import { createHostLedgerPort } from './host-ledger-file-adapter.js';
14
15
  export { planActNotifications, matchesMentionTarget };
15
16
  export { notificationMessageId } from './delivery.js';
17
+ export const PRIVILEGED_HOOK_BUDGET_MS = 3000;
16
18
  export function wakeGraceMs(env = process.env) {
17
19
  const value = Number.parseInt(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? '5000', 10);
18
20
  if (!Number.isFinite(value) || value <= 0) {
@@ -77,7 +79,7 @@ export async function waitForDeliveredNotification(squarePath, name, ref, opts =
77
79
  }
78
80
  return false;
79
81
  }
80
- async function defaultWakeAdapters() {
82
+ export async function defaultWakeAdapters() {
81
83
  const adapters = [];
82
84
  try {
83
85
  const { CodexQueueAdapter } = await import('./codex-queue.js');
@@ -95,20 +97,38 @@ async function defaultWakeAdapters() {
95
97
  }
96
98
  return adapters;
97
99
  }
98
- function createWakeTransport(adapters, hostLedger, clock) {
100
+ export async function createDefaultWakeTransport(hostLedger, clock, env = process.env) {
101
+ const adapters = await defaultWakeAdapters();
102
+ return createWakeTransport(env.SQUARE_DISABLE_PASEO_WAKE === '1' ? adapters.filter((adapter) => adapter.kind !== 'paseo') : adapters, hostLedger, clock);
103
+ }
104
+ export function createWakeTransport(adapters, hostLedger, clock) {
99
105
  return {
100
- attempt: async (request, _timeoutMs) => {
106
+ probe: async (route) => {
107
+ const adapter = adapters.find((candidate) => candidate.kind === route.kind);
108
+ if (adapter === undefined)
109
+ return { outcome: 'not-capable', diagnostic: `no adapter for ${route.kind}` };
110
+ const probe = adapter.probe;
111
+ if (probe === undefined)
112
+ return true;
113
+ try {
114
+ return await probe.call(adapter, route.address);
115
+ }
116
+ catch (error) {
117
+ return { outcome: 'not-capable', diagnostic: error instanceof Error ? error.message : String(error) };
118
+ }
119
+ },
120
+ attempt: async (request, timeoutMs) => {
101
121
  const adapter = adapters.find((candidate) => candidate.kind === request.route.kind);
102
122
  if (adapter === undefined)
103
- return { outcome: 'failed', message: 'wake adapter unavailable' };
123
+ return { outcome: 'not-capable', diagnostic: `no adapter for ${request.route.kind}` };
104
124
  try {
105
- 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);
106
126
  if (result.outcome === 'accepted')
107
127
  return { outcome: 'accepted' };
108
128
  if (result.outcome === 'failed')
109
129
  return { outcome: 'failed', message: result.message };
110
130
  if (result.outcome === 'unavailable')
111
- return { outcome: 'failed', message: result.message, unavailable: true };
131
+ return { outcome: 'failed', message: result.message, unavailable: true, ...(result.retainRoute === true ? { retainRoute: true } : {}), ...(result.routeStale === true ? { routeStale: true } : {}) };
112
132
  if (result.outcome === 'unknown')
113
133
  return { outcome: 'unknown', diagnostic: result.message };
114
134
  return { outcome: 'unknown', diagnostic: 'wake dispatch cancelled' };
@@ -117,15 +137,6 @@ function createWakeTransport(adapters, hostLedger, clock) {
117
137
  return { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
118
138
  }
119
139
  },
120
- invalidate: async (request) => {
121
- await hostLedger.ensurePresence({
122
- location: request.route.location,
123
- participant: request.route.participant,
124
- session: request.route.sessionId,
125
- channel: request.route.channel,
126
- updatedAt: clock(),
127
- }, 'user');
128
- },
129
140
  };
130
141
  }
131
142
  export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
@@ -140,12 +151,69 @@ export async function processActNotificationsOnce(squarePath, actIndex, opts = {
140
151
  try {
141
152
  const adapters = opts.adapters ?? await defaultWakeAdapters();
142
153
  const transport = createWakeTransport(adapters, hostLedger, now);
143
- return await deliverPending({ artifact: square.artifact, hostLedger, transport, location: squarePath, activity: actIndex, timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: now() });
154
+ try {
155
+ return await deliverPending({ artifact: square.artifact, hostLedger, transport, location: squarePath, activity: actIndex, timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: now() });
156
+ }
157
+ catch {
158
+ return { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
159
+ }
144
160
  }
145
161
  finally {
146
162
  await closeOpenSquare(square);
147
163
  }
148
164
  }
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;
170
+ const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
171
+ const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? root, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root, readableScopes: ['user'], writableScope: 'user' });
172
+ let indexed = [];
173
+ try {
174
+ indexed = await hostLedger.listPresence({ scopes: ['user'], now: Date.now() });
175
+ }
176
+ catch { /* capability is best effort */ }
177
+ const paths = new Set(indexed.map((binding) => binding.location));
178
+ try {
179
+ for (const entry of await fs.promises.readdir(path.join(cwd, '.square'))) {
180
+ if (entry.endsWith('.square'))
181
+ paths.add(path.join(cwd, '.square', entry));
182
+ }
183
+ }
184
+ catch { /* no local square directory */ }
185
+ if (remainingMs() === 0)
186
+ return;
187
+ const adapters = suppliedAdapters ?? await defaultWakeAdapters();
188
+ for (const squarePath of paths) {
189
+ if (remainingMs() === 0)
190
+ break;
191
+ try {
192
+ const square = await openSquare(squarePath, { hostLedger, env });
193
+ try {
194
+ await hostLedger.reconcileBinding({ artifact: square.artifact, scopes: ['user'], now: Date.now() }).catch(() => undefined);
195
+ if (remainingMs() === 0)
196
+ break;
197
+ const limit = Number.parseInt(env.SQUARE_NOTIFY_SWEEP_LIMIT ?? '8', 10);
198
+ const graceMs = 0;
199
+ const selected = await sweepPending({ artifact: square.artifact, hostLedger, location: squarePath, now: Date.now(), graceMs, limit: Number.isFinite(limit) && limit > 0 ? limit : 8 }).catch(() => []);
200
+ const transport = createWakeTransport(adapters, hostLedger, Date.now);
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
+ }
209
+ }
210
+ finally {
211
+ await closeOpenSquare(square);
212
+ }
213
+ }
214
+ catch { /* stale index entries are ignored by the hook */ }
215
+ }
216
+ }
149
217
  /** Select sweep candidates from one frozen snapshot and one delivery replay. */
150
218
  export async function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery) {
151
219
  const ledger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER, writableScope: 'user', readableScopes: ['user'] });
@@ -1,10 +1,11 @@
1
- import type { HostLedgerPort, SquareArtifactPort } from './ports.js';
1
+ import type { HostLedgerPort, SquareArtifactPort, WakeTransportPort } from './ports.js';
2
2
  /** Private binding assembled by storage and consumed by the four concerns. */
3
3
  export interface OpenSquare {
4
4
  readonly artifact: SquareArtifactPort;
5
5
  readonly clock: () => number;
6
6
  readonly location: string;
7
7
  readonly hostLedger?: HostLedgerPort;
8
+ readonly wakeTransport?: WakeTransportPort;
8
9
  readonly env?: NodeJS.ProcessEnv;
9
10
  }
10
11
  /** Package-private lifecycle boundary for bound squares. */
@@ -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,17 +30,29 @@ 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 {
37
45
  outcome: 'unavailable',
38
46
  signature: 'invalid_address',
39
47
  message: 'Paseo route has no agent id.',
48
+ routeStale: true,
40
49
  diagnostic: diagnostic('selection', address, 'invalid_address'),
41
50
  };
42
51
  }
43
- 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);
44
56
  if (discovery.error && discovery.agents.length === 0) {
45
57
  return {
46
58
  outcome: 'unavailable',
@@ -58,9 +70,13 @@ export class PaseoAdapter {
58
70
  message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle.',
59
71
  diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : 'not_idle'),
60
72
  ...(agent === undefined ? {} : { retainRoute: true }),
73
+ ...(agent === undefined ? { routeStale: true } : {}),
61
74
  };
62
75
  }
63
- 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))) {
64
80
  return {
65
81
  outcome: 'unavailable',
66
82
  signature: 'boundary_unavailable',
@@ -71,8 +87,11 @@ export class PaseoAdapter {
71
87
  }
72
88
  if (!(await beforeSend()))
73
89
  return { outcome: 'cancelled' };
90
+ remaining = remainingMs();
91
+ if (remaining === 0)
92
+ return budgetUnavailable();
74
93
  try {
75
- (this.opts.sendWake ?? sendPaseoWake)({ agentId, prompt: payload });
94
+ (this.opts.sendWake ?? sendPaseoWake)({ agentId, prompt: payload }, { timeoutMs: remaining });
76
95
  return { outcome: 'accepted' };
77
96
  }
78
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/dist/ports.d.ts CHANGED
@@ -17,6 +17,11 @@ export interface SquareArtifactPort {
17
17
  }
18
18
  /** Capability-neutral wake transport. Unused by this Contract's activity operations. */
19
19
  export interface WakeTransportPort {
20
+ /** Capability check that performs no external wake and writes no evidence. */
21
+ probe?(route: WakeRoute): Promise<boolean | {
22
+ readonly outcome: 'not-capable';
23
+ readonly diagnostic?: string;
24
+ }>;
20
25
  attempt(request: WakeRequest, timeoutMs: number): Promise<WakeOutcome>;
21
26
  /** Optional route retirement supplied by the concrete executor adapter. */
22
27
  invalidate?(request: WakeRequest): Promise<void>;
@@ -65,6 +70,11 @@ export type WakeOutcome = {
65
70
  readonly message?: string;
66
71
  readonly attemptN?: number;
67
72
  readonly unavailable?: boolean;
73
+ readonly retainRoute?: boolean;
74
+ readonly routeStale?: boolean;
75
+ } | {
76
+ readonly outcome: 'not-capable';
77
+ readonly diagnostic?: string;
68
78
  } | {
69
79
  readonly outcome: 'unknown';
70
80
  readonly diagnostic?: string;
@@ -98,6 +108,7 @@ export interface DeliveryResult {
98
108
  readonly accepted: number;
99
109
  readonly failed: number;
100
110
  readonly unknown: number;
111
+ readonly notCapable: number;
101
112
  }
102
113
  export interface DeliverPendingInput {
103
114
  readonly artifact: SquareArtifactPort;
package/dist/presence.js CHANGED
@@ -5,7 +5,7 @@ import { recordObservation } from './runtime.js';
5
5
  import { catchUp as actionCatchUp } from './square-actions.js';
6
6
  function operationContext(square) {
7
7
  return 'artifact' in square
8
- ? { artifact: square.artifact, clock: square.clock }
8
+ ? { artifact: square.artifact, clock: square.clock, location: square.location, hostLedger: square.hostLedger, wakeTransport: square.wakeTransport, env: square.env }
9
9
  : { artifact: square.cell, clock: square.clock };
10
10
  }
11
11
  export async function catchUp(square, name, options = {}, deriveDelivery = deriveDeliveryModel) {
package/dist/registry.js CHANGED
@@ -26,7 +26,7 @@ async function writePresence(sessionId, name, squarePath, options, done, scope =
26
26
  return; const env = options.env ?? process.env; const channel = options.channel ?? 'unknown'; const port = ledger(env, scope); const location = await canonicalSquarePath(squarePath); if (done)
27
27
  await port.removePresence({ location, participant: name, session: sessionId, channel });
28
28
  else
29
- await port.ensurePresence({ location, participant: name, session: sessionId, channel, route: options.route, updatedAt: options.at ?? Date.now() }); }
29
+ await port.ensurePresence({ location, participant: name, session: sessionId, channel, updatedAt: options.at ?? Date.now() }); }
30
30
  export function recordJoin(sessionId, name, squarePath, options = {}) { return writePresence(sessionId, name, squarePath, options, false); }
31
31
  export async function recordDone(sessionId, name, squarePath, options = {}) {
32
32
  await writePresence(sessionId, name, squarePath, options, true);
@@ -64,17 +64,16 @@ function addLocalSession(identities, sessionId, channel, child, paseoAgentId) {
64
64
  export function localSessionIdentities(env = process.env) { const paseoAgentId = env.PASEO_AGENT_ID?.trim() || undefined; const identities = []; for (const source of LOCAL_SESSION_SOURCES)
65
65
  addLocalSession(identities, env[source.variable]?.trim(), source.channel, source.child !== undefined && env[source.child] === '1', paseoAgentId); addLocalSession(identities, paseoAgentId, 'paseo', false, paseoAgentId); return identities; }
66
66
  export function hasAutomaticDeliveryIdentity(env = process.env) { return localSessionIdentities(env).length > 0; }
67
- function callableRoute(sessionId, channel, env) { if (channel === 'codex' && env.CODEX_THREAD_ID?.trim() === sessionId)
68
- return { kind: 'codex-queue', address: { threadId: sessionId } }; if (channel === 'paseo' && env.PASEO_AGENT_ID?.trim() === sessionId)
69
- return { kind: 'paseo', address: { agentId: sessionId } }; return undefined; }
70
- export async function recordLocalJoin(name, squarePath, env = process.env) { const at = Date.now(); const identities = localSessionIdentities(env); const current = await lookupParticipant(squarePath, name, at, env); for (const binding of current)
71
- await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }); for (const identity of identities)
72
- await recordJoin(identity.sessionId, name, squarePath, { ...identity, at, env }); }
73
- export async function recordLocalDone(name, squarePath, env = process.env) { const at = Date.now(); const current = await lookupParticipant(squarePath, name, at, env); for (const binding of current)
67
+ export async function recordLocalJoin(name, squarePath, env = process.env) { const at = Date.now(); const identities = localSessionIdentities(env); const current = await lookupParticipant(squarePath, name, at, env); for (const identity of identities) {
68
+ for (const binding of current.filter((item) => item.sessionId === identity.sessionId))
69
+ await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env });
70
+ await recordJoin(identity.sessionId, name, squarePath, { ...identity, at, env });
71
+ } }
72
+ export async function recordLocalDone(name, squarePath, env = process.env) { const at = Date.now(); const identities = new Set(localSessionIdentities(env).map((identity) => identity.sessionId)); const current = (await lookupParticipant(squarePath, name, at, env)).filter((binding) => identities.has(binding.sessionId)); for (const binding of current)
74
73
  await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }); }
75
- export async function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) { const at = Date.now(); const current = await lookupParticipant(squarePath, name, at, env); for (const binding of current) {
74
+ export async function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) { const at = Date.now(); const current = (await lookupParticipant(squarePath, name, at, env)).filter((binding) => binding.sessionId === sessionId); for (const binding of current) {
76
75
  await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env });
77
76
  await writePresence(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }, true, 'user');
78
- } const route = callableRoute(sessionId, channel, env); await recordJoin(sessionId, name, squarePath, { channel, at, env }); await writePresence(sessionId, name, squarePath, { channel, route, at, env }, false, 'user'); return sessionId; }
77
+ } await recordJoin(sessionId, name, squarePath, { channel, at, env }); await writePresence(sessionId, name, squarePath, { channel, at, env }, false, 'user'); return sessionId; }
79
78
  export async function recordSessionDone(sessionId, name, squarePath, channel, env = process.env) { const canonicalPath = await canonicalSquarePath(squarePath); const binding = (await lookupSessionBindings(sessionId, Date.now(), env)).find((item) => item.squarePath === canonicalPath && sameName(item.name, name) && item.channel === channel); if (binding === undefined)
80
79
  return false; const options = { channel, at: Date.now(), env }; await recordDone(sessionId, binding.name, binding.squarePath, options); await writePresence(sessionId, binding.name, binding.squarePath, options, true, 'user'); return true; }
package/dist/routes.d.ts CHANGED
@@ -1,7 +1,26 @@
1
- import { type WakeRoute } from './model.js';
1
+ import { type WakeRoute, type WakeRouteKind } from './model.js';
2
2
  export { WAKE_ROUTE_KINDS } from './model.js';
3
3
  export type { WakeRoute, WakeRouteKind } from './model.js';
4
4
  export declare const ROUTE_FRESH_MS: number;
5
+ export type WakeBoundaryProvider = 'codex' | 'claude' | 'opencode' | 'pi' | 'paseo';
6
+ export interface WakeBoundary {
7
+ readonly location: string;
8
+ readonly participant: string;
9
+ readonly sessionId: string;
10
+ readonly provider: WakeBoundaryProvider;
11
+ }
12
+ export interface WakeRouteCapabilities {
13
+ readonly canUse: (kind: WakeRouteKind, address: Readonly<Record<string, string>>) => boolean;
14
+ }
15
+ export declare function defaultWakeRouteCapabilities(hostLedger?: import('./host-ledger.js').HostLedgerPort): Promise<WakeRouteCapabilities>;
16
+ /** Pure, ordered route precedence. Capability predicates own adapter and scope checks. */
17
+ export declare function selectPrimaryWakeRoute(input: {
18
+ readonly boundary: WakeBoundary;
19
+ readonly env: NodeJS.ProcessEnv;
20
+ readonly capabilities: WakeRouteCapabilities;
21
+ }): Omit<WakeRoute, 'updatedAt'> | undefined;
22
+ export declare function routeIdentityKey(route: Pick<WakeRoute, 'location' | 'participant' | 'sessionId'>, location?: string): string;
23
+ export declare function resolvePrimaryWakeRoute(boundary: WakeBoundary, env: NodeJS.ProcessEnv, capabilities: WakeRouteCapabilities): Omit<WakeRoute, 'updatedAt'> | undefined;
5
24
  export declare function readWakeRoutes(opts?: {
6
25
  location?: string;
7
26
  participant?: string;
@@ -14,7 +33,12 @@ export declare function upsertWakeRoute(route: Omit<WakeRoute, 'updatedAt'>, opt
14
33
  at?: number;
15
34
  env?: NodeJS.ProcessEnv;
16
35
  }): Promise<void>;
36
+ export declare function publishWakeRoute(artifact: import('./ports.js').SquareArtifactPort, route: Omit<WakeRoute, 'updatedAt'>, opts?: {
37
+ at?: number;
38
+ }): Promise<void>;
17
39
  export declare function retireWakeRoute(route: WakeRoute, opts?: {
18
40
  at?: number;
19
41
  env?: NodeJS.ProcessEnv;
20
42
  }): Promise<void>;
43
+ export declare function retireWakeRouteFromArtifact(artifact: import('./ports.js').SquareArtifactPort, route: Pick<WakeRoute, 'location' | 'participant' | 'sessionId'>): Promise<void>;
44
+ export declare function canonicalRouteLocation(location: string): Promise<string>;
package/dist/routes.js CHANGED
@@ -1,9 +1,110 @@
1
- import os from 'node:os';
1
+ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { createHostLedgerPort } from './host-ledger-file-adapter.js';
3
+ import { nameKey } from './model.js';
4
+ import { openSquare } from './square-file-adapter.js';
5
+ import { closeOpenSquare } from './open-square.js';
4
6
  export { WAKE_ROUTE_KINDS } from './model.js';
5
7
  export const ROUTE_FRESH_MS = 24 * 60 * 60 * 1000;
6
- function ledger(env) { return createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? path.join(os.homedir(), '.square', 'host-ledger'), writableScope: 'user', readableScopes: ['user'] }); }
7
- export async function readWakeRoutes(opts = {}) { const now = opts.now ?? Date.now(); const records = await ledger(opts.env ?? process.env).listPresence({ location: opts.location, participant: opts.participant, session: opts.sessionId, now, scopes: ['user'] }); return records.flatMap((record) => record.route === undefined || (opts.freshOnly === true && now - (record.updatedAt ?? 0) >= ROUTE_FRESH_MS) ? [] : [{ location: record.location, participant: record.participant, sessionId: record.session, channel: record.channel, kind: record.route.kind, address: { ...record.route.address }, updatedAt: record.updatedAt ?? 0 }]); }
8
- export async function upsertWakeRoute(route, opts = {}) { await ledger(opts.env ?? process.env).ensurePresence({ location: route.location, participant: route.participant, session: route.sessionId, channel: route.channel, route: { kind: route.kind, address: route.address }, updatedAt: opts.at ?? Date.now() }); }
9
- export async function retireWakeRoute(route, opts = {}) { await ledger(opts.env ?? process.env).ensurePresence({ location: route.location, participant: route.participant, session: route.sessionId, channel: route.channel, updatedAt: opts.at ?? Date.now() }); }
8
+ export async function defaultWakeRouteCapabilities(hostLedger) {
9
+ let userCapable = hostLedger !== undefined;
10
+ if (hostLedger !== undefined) {
11
+ try {
12
+ await hostLedger.listPresence({ scopes: ['user'], now: Date.now() });
13
+ }
14
+ catch {
15
+ userCapable = false;
16
+ }
17
+ }
18
+ const available = new Set();
19
+ try {
20
+ const { CodexQueueAdapter } = await import('./codex-queue.js');
21
+ available.add(new CodexQueueAdapter().kind);
22
+ }
23
+ catch { /* optional */ }
24
+ try {
25
+ const { PaseoAdapter } = await import('./paseo-delivery.js');
26
+ available.add(new PaseoAdapter().kind);
27
+ }
28
+ catch { /* optional */ }
29
+ return { canUse: (kind, address) => userCapable && available.has(kind) && Object.values(address).every((value) => value.trim() !== '') };
30
+ }
31
+ function nativeCandidate(boundary) {
32
+ if (boundary.provider === 'codex')
33
+ return { kind: 'codex-queue', address: { threadId: boundary.sessionId } };
34
+ if (boundary.provider === 'claude')
35
+ return { kind: 'claude-native', address: { sessionId: boundary.sessionId } };
36
+ if (boundary.provider === 'opencode')
37
+ return { kind: 'opencode-server', address: { sessionId: boundary.sessionId } };
38
+ if (boundary.provider === 'pi')
39
+ return { kind: 'pi-extension', address: { sessionId: boundary.sessionId } };
40
+ return undefined;
41
+ }
42
+ /** Pure, ordered route precedence. Capability predicates own adapter and scope checks. */
43
+ export function selectPrimaryWakeRoute(input) {
44
+ const { boundary, env, capabilities } = input;
45
+ const paseoAgentId = env.PASEO_AGENT_ID?.trim();
46
+ const candidates = [];
47
+ if (paseoAgentId)
48
+ candidates.push({ kind: 'paseo', address: { agentId: paseoAgentId } });
49
+ const native = nativeCandidate(boundary);
50
+ if (native)
51
+ candidates.push(native);
52
+ const chosen = candidates.find((candidate) => Object.values(candidate.address).every((value) => value.trim() !== '') && capabilities.canUse(candidate.kind, candidate.address));
53
+ return chosen === undefined ? undefined : { location: boundary.location, participant: boundary.participant, sessionId: boundary.sessionId, channel: boundary.provider === 'paseo' ? 'paseo' : boundary.provider === 'claude' ? 'claude-code' : boundary.provider, ...chosen };
54
+ }
55
+ export function routeIdentityKey(route, location = route.location) {
56
+ return JSON.stringify([location, nameKey(route.participant), route.sessionId]);
57
+ }
58
+ export function resolvePrimaryWakeRoute(boundary, env, capabilities) {
59
+ return selectPrimaryWakeRoute({ boundary, env, capabilities });
60
+ }
61
+ async function withArtifact(location, fn) {
62
+ if (location === undefined)
63
+ return undefined;
64
+ try {
65
+ await fs.promises.access(location);
66
+ const square = await openSquare(location);
67
+ try {
68
+ return await fn(square);
69
+ }
70
+ finally {
71
+ await closeOpenSquare(square);
72
+ }
73
+ }
74
+ catch {
75
+ return undefined;
76
+ }
77
+ }
78
+ export async function readWakeRoutes(opts = {}) {
79
+ const now = opts.now ?? Date.now();
80
+ const canonicalLocation = opts.location === undefined ? undefined : await canonicalRouteLocation(opts.location);
81
+ const routes = await withArtifact(canonicalLocation, async (square) => (await square.artifact.read()).state.routes ?? []) ?? [];
82
+ const filtered = routes.filter((route) => (opts.participant === undefined || nameKey(route.participant) === nameKey(opts.participant)) && (opts.sessionId === undefined || route.sessionId === opts.sessionId) && (!opts.freshOnly || now - route.updatedAt < ROUTE_FRESH_MS));
83
+ const canonicalized = await Promise.all(filtered.map(async (route) => ({ ...route, location: await canonicalRouteLocation(route.location), address: { ...route.address } })));
84
+ return canonicalLocation === undefined ? canonicalized : canonicalized.filter((route) => route.location === canonicalLocation);
85
+ }
86
+ export async function upsertWakeRoute(route, opts = {}) {
87
+ const location = await canonicalRouteLocation(route.location);
88
+ await withArtifact(location, async (square) => publishWakeRoute(square.artifact, { ...route, location }, opts));
89
+ }
90
+ export async function publishWakeRoute(artifact, route, opts = {}) {
91
+ const location = await canonicalRouteLocation(route.location);
92
+ await artifact.transact((state) => ({ state: { ...state, routes: [...(state.routes ?? []).filter((item) => routeIdentityKey(item) !== routeIdentityKey({ ...route, location })), { ...route, location, participant: route.participant, updatedAt: opts.at ?? Date.now() }] }, result: undefined }));
93
+ }
94
+ export async function retireWakeRoute(route, opts = {}) {
95
+ const location = await canonicalRouteLocation(route.location);
96
+ await withArtifact(location, async (square) => retireWakeRouteFromArtifact(square.artifact, { ...route, location }));
97
+ }
98
+ export async function retireWakeRouteFromArtifact(artifact, route) {
99
+ const location = await canonicalRouteLocation(route.location);
100
+ await artifact.transact((state) => ({ state: { ...state, routes: (state.routes ?? []).filter((item) => routeIdentityKey(item) !== routeIdentityKey({ ...route, location })) }, result: undefined }));
101
+ }
102
+ export async function canonicalRouteLocation(location) {
103
+ const absolute = path.resolve(location);
104
+ try {
105
+ return await fs.promises.realpath(absolute);
106
+ }
107
+ catch {
108
+ return absolute;
109
+ }
110
+ }
@@ -8,6 +8,7 @@ export interface OperationContext {
8
8
  readonly clock: () => number;
9
9
  readonly location?: string;
10
10
  readonly hostLedger?: HostLedgerPort;
11
+ readonly wakeTransport?: import('./ports.js').WakeTransportPort;
11
12
  readonly env?: NodeJS.ProcessEnv;
12
13
  }
13
14
  export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, project?: (state: SquareState) => CatchProjection): Promise<CatchResult>;
@@ -2,7 +2,9 @@ import { extractMentions, formatActivityId, parseActivityId } from './square-cor
2
2
  import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, decideAct, decideImplicitJoin, decideJoin } from './decisions.js';
3
3
  import { SquareError } from './model.js';
4
4
  import { participantIdentity } from './participant-identity.js';
5
+ import { deliverPending } from './delivery-operations.js';
5
6
  import { decideCatch } from './catch-decisions.js';
7
+ import { publishWakeRoute, retireWakeRouteFromArtifact, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
6
8
  function processIdentity(env) {
7
9
  const choices = [
8
10
  [env.CLAUDE_CODE_SESSION_ID, 'claude-code'], [env.CODEX_THREAD_ID, 'codex'],
@@ -11,6 +13,25 @@ function processIdentity(env) {
11
13
  const found = choices.find(([session]) => session?.trim());
12
14
  return found === undefined ? { session: `process:${process.pid}`, channel: 'unknown' } : { session: found[0].trim(), channel: found[1] };
13
15
  }
16
+ async function publishIdentityRoute(context, participant) {
17
+ if (context.location === undefined || context.location === 'memory')
18
+ return;
19
+ const identity = processIdentity(context.env ?? process.env);
20
+ if (context.hostLedger === undefined)
21
+ return;
22
+ const provider = identity.channel === 'claude-code' ? 'claude' : identity.channel === 'opencode' ? 'opencode' : identity.channel === 'pi' ? 'pi' : identity.channel === 'paseo' ? 'paseo' : 'codex';
23
+ const capabilities = await defaultWakeRouteCapabilities(context.hostLedger);
24
+ const route = await resolvePrimaryWakeRoute({ location: context.location, participant, sessionId: identity.session, provider }, context.env ?? process.env, capabilities);
25
+ if (route === undefined)
26
+ return;
27
+ await publishWakeRoute(context.artifact, route, { at: context.clock() }).catch(() => undefined);
28
+ }
29
+ async function retireIdentityRoute(context, participant) {
30
+ if (context.location === undefined || context.location === 'memory')
31
+ return;
32
+ const identity = processIdentity(context.env ?? process.env);
33
+ await retireWakeRouteFromArtifact(context.artifact, { location: context.location, participant, sessionId: identity.session }).catch(() => undefined);
34
+ }
14
35
  /** Presence is best effort and runs only after the artifact mutation commits. */
15
36
  async function ensureLocalPresence(context, participant) {
16
37
  if (context.hostLedger === undefined || context.location === undefined || context.location === 'memory')
@@ -48,6 +69,7 @@ export async function catchUp(square, name, options = {}, project) {
48
69
  return { ...(decision.changed ? { state } : {}), result: { version, decision } };
49
70
  });
50
71
  await ensureLocalPresence(square, name);
72
+ await publishIdentityRoute(square, name);
51
73
  if (attempt.decision.delivered.length > 0 || idle === 0) {
52
74
  return {
53
75
  activities: attempt.decision.delivered.map((activity) => exposeCaught(activity, attempt.decision.perceptions.get(activity.index) ?? 'full')),
@@ -103,6 +125,7 @@ export async function join(square, name) {
103
125
  return { state, result: { name: decision.joinedName, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
104
126
  });
105
127
  await ensureLocalPresence(square, committed.name);
128
+ await publishIdentityRoute(square, committed.name);
106
129
  return { name: committed.name, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
107
130
  }
108
131
  export async function implicitJoin(square, name) {
@@ -114,6 +137,10 @@ export async function implicitJoin(square, name) {
114
137
  return { state, result: { name: decision.joinedName, state: decision.state, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
115
138
  });
116
139
  await ensureLocalPresence(square, committed.name);
140
+ if (committed.state === 'done')
141
+ await retireIdentityRoute(square, committed.name);
142
+ else
143
+ await publishIdentityRoute(square, committed.name);
117
144
  return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
118
145
  }
119
146
  export async function express(square, name, body, options = {}) {
@@ -139,7 +166,15 @@ export async function express(square, name, body, options = {}) {
139
166
  return { state, result: { stored } };
140
167
  });
141
168
  await ensureLocalPresence(square, name);
142
- return { activity: exposeActivity(committed.stored) };
169
+ await publishIdentityRoute(square, name);
170
+ let delivery;
171
+ if (square.wakeTransport !== undefined && square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory') {
172
+ delivery = await deliverPending({ artifact: square.artifact, hostLedger: square.hostLedger, transport: square.wakeTransport, location: square.location, activity: committed.stored.index, now }).catch(() => ({ attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 }));
173
+ }
174
+ else {
175
+ delivery = { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
176
+ }
177
+ return { activity: exposeActivity(committed.stored), delivery };
143
178
  }
144
179
  async function landListenerChange(square, verb, actor, target) {
145
180
  const now = square.clock();
@@ -160,6 +195,8 @@ async function landCore(square, verb, actor, body = '') {
160
195
  const act = verb === 'done' ? coreDone(state, actor, body, now) : verb === 'hold' ? coreHold(state, actor, body, now) : coreResume(state, actor, now);
161
196
  return { state, result: committedActivity(storeActs(state, [act]), verb) };
162
197
  });
198
+ if (verb === 'done')
199
+ await retireIdentityRoute(square, actor);
163
200
  return { activity: exposeActivity(stored) };
164
201
  }
165
202
  export function done(square, name, body = '') { return landCore(square, 'done', name, body); }
@@ -1,5 +1,5 @@
1
1
  import type { ActivityId, Perception, Reach } from './square-core.js';
2
- import type { HostLedgerPort } from './ports.js';
2
+ import type { HostLedgerPort, WakeTransportPort, DeliveryResult } from './ports.js';
3
3
  export interface Activity {
4
4
  readonly id: ActivityId;
5
5
  readonly at: number;
@@ -20,6 +20,7 @@ export interface ExpressOptions {
20
20
  }
21
21
  export interface ExpressResult {
22
22
  readonly activity: Activity;
23
+ readonly delivery?: DeliveryResult;
23
24
  }
24
25
  export interface ListenerChangeResult {
25
26
  readonly activity: Activity | null;
@@ -68,6 +69,7 @@ export type SquareSource = {
68
69
  export interface OpenOptions {
69
70
  clock?: () => number;
70
71
  hostLedger?: HostLedgerPort;
72
+ wakeTransport?: WakeTransportPort;
71
73
  env?: NodeJS.ProcessEnv;
72
74
  }
73
75
  export interface SquareAtInput extends SquareSource, OpenOptions {
@@ -78,6 +80,7 @@ export interface SquareBuildInput extends SquareSource {
78
80
  throttlePerMinute?: number;
79
81
  clock?: () => number;
80
82
  hostLedger?: HostLedgerPort;
83
+ wakeTransport?: WakeTransportPort;
81
84
  env?: NodeJS.ProcessEnv;
82
85
  }
83
86
  export interface Participant {
@@ -11,9 +11,10 @@ export interface SquareBuildOptions {
11
11
  throttlePerMinute?: number;
12
12
  clock?: () => number;
13
13
  hostLedger?: HostLedgerPort;
14
+ wakeTransport?: import('./ports.js').WakeTransportPort;
14
15
  env?: NodeJS.ProcessEnv;
15
16
  }
16
- export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'env'>): Promise<OpenSquare>;
17
+ export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env'>): Promise<OpenSquare>;
17
18
  export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
18
19
  export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
19
20
  export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;