@astrosheep/square 0.3.30 → 0.3.32

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 (84) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/hooks/hooks.json +0 -3
  3. package/claude-plugin/skills/square/SKILL.md +5 -4
  4. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  5. package/dist/activity.js +8 -3
  6. package/dist/artifact.js +10 -51
  7. package/dist/automatic-session.js +49 -14
  8. package/dist/boundary-presentation.d.ts +1 -1
  9. package/dist/boundary-presentation.js +58 -14
  10. package/dist/catch-decisions.d.ts +17 -0
  11. package/dist/catch-decisions.js +53 -0
  12. package/dist/claude-hook.d.ts +2 -1
  13. package/dist/claude-hook.js +5 -2
  14. package/dist/cli/context.js +10 -2
  15. package/dist/cli/observation-commands.d.ts +5 -1
  16. package/dist/cli/observation-commands.js +68 -33
  17. package/dist/cli/square-commands.js +18 -12
  18. package/dist/codex-hook.d.ts +2 -1
  19. package/dist/codex-hook.js +5 -2
  20. package/dist/codex-queue.js +1 -1
  21. package/dist/decisions.js +2 -2
  22. package/dist/delivery-health.d.ts +1 -1
  23. package/dist/delivery-operations.d.ts +25 -0
  24. package/dist/delivery-operations.js +207 -0
  25. package/dist/delivery.d.ts +1 -0
  26. package/dist/help.js +1 -1
  27. package/dist/host-ledger-file-adapter.d.ts +34 -0
  28. package/dist/host-ledger-file-adapter.js +156 -0
  29. package/dist/host-ledger.d.ts +160 -0
  30. package/dist/host-ledger.js +1 -0
  31. package/dist/inbox.d.ts +2 -2
  32. package/dist/inbox.js +22 -14
  33. package/dist/index.d.ts +4 -1
  34. package/dist/index.js +1 -0
  35. package/dist/landing.d.ts +18 -14
  36. package/dist/landing.js +29 -113
  37. package/dist/model.d.ts +9 -17
  38. package/dist/model.js +2 -2
  39. package/dist/notifications.d.ts +12 -10
  40. package/dist/notifications.js +122 -190
  41. package/dist/open-square.d.ts +5 -4
  42. package/dist/open-square.js +1 -1
  43. package/dist/paseo-delivery.js +2 -0
  44. package/dist/ports.d.ts +138 -0
  45. package/dist/ports.js +1 -0
  46. package/dist/presence.d.ts +1 -2
  47. package/dist/presence.js +13 -46
  48. package/dist/presentation-operations.d.ts +3 -0
  49. package/dist/presentation-operations.js +51 -0
  50. package/dist/presentation.d.ts +2 -2
  51. package/dist/presentation.js +11 -7
  52. package/dist/presented.d.ts +1 -12
  53. package/dist/presented.js +6 -75
  54. package/dist/registry.d.ts +10 -10
  55. package/dist/registry.js +47 -113
  56. package/dist/routes.d.ts +27 -30
  57. package/dist/routes.js +88 -151
  58. package/dist/runtime.d.ts +1 -1
  59. package/dist/runtime.js +3 -4
  60. package/dist/square-actions.d.ts +33 -0
  61. package/dist/square-actions.js +204 -0
  62. package/dist/square-facade.d.ts +10 -7
  63. package/dist/square-file-adapter.d.ts +5 -3
  64. package/dist/square-file-adapter.js +23 -5
  65. package/dist/square-projections.d.ts +68 -0
  66. package/dist/square-projections.js +92 -0
  67. package/dist/square-storage.d.ts +2 -2
  68. package/dist/square-storage.js +2 -2
  69. package/dist/square-wiring.d.ts +3 -3
  70. package/dist/square-wiring.js +44 -29
  71. package/dist/views.d.ts +8 -2
  72. package/dist/views.js +28 -24
  73. package/dist/wake-attempts.d.ts +29 -22
  74. package/dist/wake-attempts.js +36 -119
  75. package/dist/wake-evidence.d.ts +6 -18
  76. package/dist/wake-evidence.js +17 -80
  77. package/dist/wake-port.js +1 -1
  78. package/dist/wakes.d.ts +2 -16
  79. package/dist/wakes.js +7 -27
  80. package/dist/watch.js +2 -3
  81. package/extensions/square-pi.js +7 -2
  82. package/package.json +1 -1
  83. package/skills/brainstorm/SKILL.md +2 -2
  84. package/skills/square/SKILL.md +5 -4
@@ -1,25 +1,17 @@
1
- import { spawn } from 'node:child_process';
2
- import { randomUUID } from 'node:crypto';
3
1
  import { setTimeout as sleep } from 'node:timers/promises';
4
- import { fileURLToPath } from 'node:url';
5
- import { deriveDeliveryModel, leaseOwnsNotification, planActNotifications, } from './delivery.js';
6
- import { sessionInbox } from './inbox.js';
7
- import { hasPresentedAttention } from './presented.js';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { planActNotifications, } from './delivery.js';
8
5
  import { SquareError } from './model.js';
9
6
  import { SLEEP_MS, matchesMentionTarget } from './runtime.js';
10
- import { parseActivityId } from './square-core.js';
11
- import { displayAttentionPath, renderAttentionPreview } from './attention-presentation.js';
12
- import { lookupParticipant } from './registry.js';
13
- import { retireWakeRoute } from './routes.js';
7
+ import { formatActivityId, parseActivityId } from './square-core.js';
8
+ import { displayAttentionPath } from './attention-presentation.js';
14
9
  import { openSquare } from './square-file-adapter.js';
15
- import { markNotificationNotified } from './square-wiring.js';
16
10
  import { closeOpenSquare } from './open-square.js';
17
- import { entryPresentation, notificationDelivered, notificationForAct, pendingDeliveriesFromState, resolveParticipant } from './views.js';
18
- import { claimNotificationLease, releaseNotificationLease, transitionNotificationLease } from './wakes.js';
19
- import { nextWakeAttemptNumber, recordRecoveredUnknown, recordWakeAttempt, } from './wake-attempts.js';
20
- import { wakeEvidence, wakeEvidenceProjectionFromState, wakeIsEligible } from './wake-evidence.js';
21
- import { WakePort } from './wake-port.js';
22
- const NOTIFY_LEASE_MS = 5 * 60 * 1000;
11
+ import { notificationDelivered, resolveParticipant } from './views.js';
12
+ import { deliverPending, sweepPending, sweepPendingFromState } from './delivery-operations.js';
13
+ import { projectPresentationEvidence } from './square-projections.js';
14
+ import { createHostLedgerPort } from './host-ledger-file-adapter.js';
23
15
  export { planActNotifications, matchesMentionTarget };
24
16
  export { notificationMessageId } from './delivery.js';
25
17
  export function wakeGraceMs(env = process.env) {
@@ -36,48 +28,14 @@ function wakeLabel(kind) {
36
28
  return 'codex-queue';
37
29
  return kind;
38
30
  }
39
- function renderWakePayload(request, body, kind) {
31
+ function renderWakePayload(request) {
40
32
  return [
41
- `<system-reminder source="square" wake="${wakeLabel(kind)}">`,
42
- `square: ${displayAttentionPath(request.squarePath)}`,
43
- renderAttentionPreview({
44
- squarePath: request.squarePath,
45
- actIndex: request.actIndex,
46
- recipient: request.recipient,
47
- actor: request.actor,
48
- route: request.route,
49
- body,
50
- compact: true,
51
- }),
33
+ `<system-reminder source="square" wake="${wakeLabel(request.route.kind)}">`,
34
+ `square: ${displayAttentionPath(request.location)}`,
35
+ `attention: ${request.activity} for ${request.participant}`,
52
36
  '</system-reminder>',
53
37
  ].join('\n');
54
38
  }
55
- async function waitForCatch(route, request, body) {
56
- const binding = (await lookupParticipant(request.squarePath, request.recipient))
57
- .find((item) => item.ownerId === route.ownerId);
58
- const activeCatch = binding && (await sessionInbox(binding.sessionId))
59
- .find((item) => item.name === request.recipient)?.catchLease;
60
- if (!activeCatch || !leaseOwnsNotification(activeCatch, {
61
- actor: request.actor,
62
- body,
63
- route: request.route,
64
- recipient: request.recipient,
65
- }))
66
- return false;
67
- const deadline = Date.now() + 180_000;
68
- while (Date.now() < deadline) {
69
- if (await hasDeliveredNotification(request.squarePath, request.recipient, request.actIndex))
70
- return true;
71
- const currentBinding = (await lookupParticipant(request.squarePath, request.recipient))
72
- .find((item) => item.ownerId === route.ownerId);
73
- const lease = currentBinding && (await sessionInbox(currentBinding.sessionId))
74
- .find((item) => item.name === request.recipient)?.catchLease;
75
- if (!lease || lease.expiresAt <= Date.now())
76
- return false;
77
- await sleep(Math.min(250, lease.expiresAt - Date.now()));
78
- }
79
- return false;
80
- }
81
39
  function notificationIndex(ref) {
82
40
  if (typeof ref === 'number')
83
41
  return ref;
@@ -101,7 +59,11 @@ export async function hasAttentionNotification(squarePath, name, ref, env = proc
101
59
  try {
102
60
  const recipient = (await resolveParticipant(square, name)).name;
103
61
  const index = notificationIndex(ref);
104
- return await notificationDelivered(square, recipient, index) || await hasPresentedAttention(squarePath, recipient, index, env);
62
+ if (await notificationDelivered(square, recipient, index))
63
+ return true;
64
+ const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
65
+ const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? root, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root, readableScopes: ['user'], writableScope: 'user' });
66
+ return (await projectPresentationEvidence({ hostLedger, location: squarePath, participant: recipient, activity: formatActivityId(index), now: Date.now() })).some((row) => row.outcome === 'presented');
105
67
  }
106
68
  finally {
107
69
  await closeOpenSquare(square);
@@ -116,7 +78,7 @@ export async function waitForDeliveredNotification(squarePath, name, ref, opts =
116
78
  }
117
79
  return false;
118
80
  }
119
- async function defaultWakeAdapters() {
81
+ export async function defaultWakeAdapters() {
120
82
  const adapters = [];
121
83
  try {
122
84
  const { CodexQueueAdapter } = await import('./codex-queue.js');
@@ -134,164 +96,134 @@ async function defaultWakeAdapters() {
134
96
  }
135
97
  return adapters;
136
98
  }
137
- async function claimNotifyLease(square, recipient, actIndex) {
138
- const leaseId = randomUUID();
139
- return claimNotificationLease(square, recipient, actIndex, leaseId, NOTIFY_LEASE_MS);
99
+ export async function createDefaultWakeTransport(hostLedger, clock, env = process.env) {
100
+ const adapters = await defaultWakeAdapters();
101
+ return createWakeTransport(env.SQUARE_DISABLE_PASEO_WAKE === '1' ? adapters.filter((adapter) => adapter.kind !== 'paseo') : adapters, hostLedger, clock);
140
102
  }
141
- async function transitionNotifyLease(square, recipient, actIndex, leaseId, phase, routeKind, attemptN) {
142
- return transitionNotificationLease(square, recipient, actIndex, leaseId, phase, NOTIFY_LEASE_MS, routeKind, attemptN);
143
- }
144
- async function releaseNotifyLease(square, recipient, actIndex, leaseId) {
145
- await releaseNotificationLease(square, recipient, actIndex, leaseId);
103
+ export function createWakeTransport(adapters, hostLedger, clock) {
104
+ return {
105
+ probe: async (route) => {
106
+ const adapter = adapters.find((candidate) => candidate.kind === route.kind);
107
+ if (adapter === undefined)
108
+ return { outcome: 'not-capable', diagnostic: `no adapter for ${route.kind}` };
109
+ const probe = adapter.probe;
110
+ if (probe === undefined)
111
+ return true;
112
+ try {
113
+ return await probe.call(adapter, route.address);
114
+ }
115
+ catch (error) {
116
+ return { outcome: 'not-capable', diagnostic: error instanceof Error ? error.message : String(error) };
117
+ }
118
+ },
119
+ attempt: async (request, _timeoutMs) => {
120
+ const adapter = adapters.find((candidate) => candidate.kind === request.route.kind);
121
+ if (adapter === undefined)
122
+ return { outcome: 'not-capable', diagnostic: `no adapter for ${request.route.kind}` };
123
+ try {
124
+ const result = await adapter.dispatch(request.route.address, renderWakePayload(request), async () => true);
125
+ if (result.outcome === 'accepted')
126
+ return { outcome: 'accepted' };
127
+ if (result.outcome === 'failed')
128
+ return { outcome: 'failed', message: result.message };
129
+ if (result.outcome === 'unavailable')
130
+ return { outcome: 'failed', message: result.message, unavailable: true, ...(result.retainRoute === true ? { retainRoute: true } : {}), ...(result.routeStale === true ? { routeStale: true } : {}) };
131
+ if (result.outcome === 'unknown')
132
+ return { outcome: 'unknown', diagnostic: result.message };
133
+ return { outcome: 'unknown', diagnostic: 'wake dispatch cancelled' };
134
+ }
135
+ catch (error) {
136
+ return { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
137
+ }
138
+ },
139
+ };
146
140
  }
147
- async function processNotification(squarePath, notification, opts) {
141
+ export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
148
142
  const env = opts.env ?? process.env;
149
143
  const now = opts.now ?? Date.now;
150
- const attention = {
151
- squarePath,
152
- actIndex: notification.item.index,
153
- recipient: notification.recipient,
154
- };
155
- const initialAt = now();
156
- if (!wakeIsEligible(await wakeEvidence(squarePath, notification.recipient, notification.item.index, initialAt, env)))
157
- return;
158
- const square = await openSquare(squarePath, { clock: now });
159
- const claim = await claimNotifyLease(square, notification.recipient, notification.item.index);
160
- if (claim.type === 'busy' || claim.type === 'delivered') {
161
- await closeOpenSquare(square);
162
- return;
163
- }
164
- if (claim.type === 'ambiguous') {
165
- const recovered = await recordRecoveredUnknown(attention, claim.lease, env);
166
- if (recovered !== undefined) {
167
- await releaseNotifyLease(square, notification.recipient, notification.item.index, claim.lease.leaseId);
144
+ const ledgerRoot = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
145
+ const hostLedger = createHostLedgerPort({
146
+ userPath: env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot,
147
+ localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot,
148
+ });
149
+ const square = await openSquare(squarePath, { clock: now, hostLedger, env });
150
+ try {
151
+ const adapters = opts.adapters ?? await defaultWakeAdapters();
152
+ const transport = createWakeTransport(adapters, hostLedger, now);
153
+ try {
154
+ return await deliverPending({ artifact: square.artifact, hostLedger, transport, location: squarePath, activity: actIndex, timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: now() });
168
155
  }
156
+ catch {
157
+ return { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
158
+ }
159
+ }
160
+ finally {
169
161
  await closeOpenSquare(square);
170
- return;
171
162
  }
172
- const { leaseId } = claim;
173
- let releaseLease = true;
163
+ }
164
+ /** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares. */
165
+ export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAdapters) {
166
+ const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
167
+ const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? root, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root, readableScopes: ['user'], writableScope: 'user' });
168
+ let indexed = [];
174
169
  try {
175
- const dispatchAt = now();
176
- const evidence = await wakeEvidence(squarePath, notification.recipient, notification.item.index, dispatchAt, env);
177
- if (!wakeIsEligible(evidence))
178
- return;
179
- const port = new WakePort(opts.adapters ?? await defaultWakeAdapters());
180
- const request = {
181
- squarePath,
182
- actIndex: notification.item.index,
183
- recipient: notification.recipient,
184
- actor: notification.item.actor,
185
- route: notification.route,
186
- };
187
- await port.dispatch(evidence.attemptableRoutes, (route) => renderWakePayload(request, notification.item.body, route.kind), {
188
- nextAttemptN: async () => nextWakeAttemptNumber(attention, { env, now: now() }),
189
- beforeSend: async (route, attemptN) => {
190
- if (await waitForCatch(route, request, notification.item.body))
191
- return false;
192
- const currentAt = now();
193
- if (!(await entryPresentation(square, notification.recipient)).joined)
194
- return false;
195
- const current = await wakeEvidence(squarePath, notification.recipient, notification.item.index, currentAt, env);
196
- if (!wakeIsEligible(current))
197
- return false;
198
- if (!current.attemptableRoutes.some((candidate) => candidate.ownerId === route.ownerId && candidate.kind === route.kind && candidate.sessionId === route.sessionId))
199
- return false;
200
- const dispatching = await transitionNotifyLease(square, notification.recipient, notification.item.index, leaseId, 'dispatching', route.kind, attemptN);
201
- if (dispatching)
202
- releaseLease = false;
203
- return dispatching;
204
- },
205
- record: async (route, attemptN, outcome) => {
206
- if (outcome.outcome === 'failed') {
207
- await transitionNotifyLease(square, notification.recipient, notification.item.index, leaseId, 'claimed');
208
- releaseLease = true;
209
- }
210
- await recordWakeAttempt({
211
- attention,
212
- routeKind: route.kind,
213
- outcome: outcome.outcome,
214
- attemptN,
215
- at: now(),
216
- ...('signature' in outcome ? { signature: outcome.signature } : {}),
217
- ...('message' in outcome ? { message: outcome.message } : {}),
218
- ...('diagnostic' in outcome && outcome.diagnostic !== undefined ? { diagnostic: outcome.diagnostic } : {}),
219
- }, env);
220
- if (outcome.outcome === 'accepted') {
221
- await markNotificationNotified(square, notification.recipient, notification.item.index, route.ownerId, now());
222
- }
223
- if (outcome.outcome !== 'failed')
224
- releaseLease = true;
225
- },
226
- invalidate: async (route) => {
227
- await retireWakeRoute(route, { env, at: now() });
228
- },
229
- });
170
+ indexed = await hostLedger.listPresence({ scopes: ['user'], now: Date.now() });
230
171
  }
231
- finally {
232
- if (releaseLease) {
233
- await releaseNotifyLease(square, notification.recipient, notification.item.index, leaseId);
172
+ catch { /* capability is best effort */ }
173
+ const paths = new Set(indexed.map((binding) => binding.location));
174
+ try {
175
+ for (const entry of await fs.promises.readdir(path.join(cwd, '.square'))) {
176
+ if (entry.endsWith('.square'))
177
+ paths.add(path.join(cwd, '.square', entry));
234
178
  }
235
- await closeOpenSquare(square);
236
179
  }
237
- }
238
- export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
239
- const square = await openSquare(squarePath);
240
- const notifications = await notificationForAct(square, actIndex).finally(() => closeOpenSquare(square));
241
- await Promise.all(notifications.map((notification) => processNotification(squarePath, notification, opts)));
242
- }
243
- function launchWorker(workerPath, args) {
244
- const child = spawn(process.execPath, [workerPath, ...args], { detached: true, stdio: 'ignore', env: process.env });
245
- child.unref();
246
- }
247
- export function wakeNotifierForSquare(squarePath, env = process.env) {
248
- return {
249
- wake(recipients, activity) {
250
- if (env.SQUARE_DISABLE_PASEO_WAKE === '1' || recipients.length === 0)
251
- return;
252
- const actIndex = parseActivityId(activity.id);
253
- if (actIndex === undefined)
254
- return;
255
- launchWorker(fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url)), ['--location', squarePath, '--act-index', String(actIndex)]);
256
- },
257
- };
258
- }
259
- /** Select sweep candidates from one frozen snapshot and one delivery replay. */
260
- export async function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery = deriveDeliveryModel) {
261
- const delivery = deriveDelivery(state);
262
- const pending = pendingDeliveriesFromState(state, delivery);
263
- const evidence = await wakeEvidenceProjectionFromState(squarePath, state, now, env, delivery);
264
- const indexes = new Set();
265
- for (const recipient of pending) {
266
- for (const note of recipient.notifications) {
267
- if (now - note.item.at <= wakeGraceMs(env))
268
- continue;
269
- if (!wakeIsEligible(evidence.evidence(recipient.recipient, note.item.index)))
270
- continue;
271
- indexes.add(note.item.index);
180
+ catch { /* no local square directory */ }
181
+ const adapters = suppliedAdapters ?? await defaultWakeAdapters();
182
+ for (const squarePath of paths) {
183
+ try {
184
+ const square = await openSquare(squarePath, { hostLedger, env });
185
+ try {
186
+ const snapshot = await square.artifact.read();
187
+ await hostLedger.reconcileBinding({ artifact: square.artifact, scopes: ['user'], now: Date.now() }).catch(() => undefined);
188
+ const limit = Number.parseInt(env.SQUARE_NOTIFY_SWEEP_LIMIT ?? '8', 10);
189
+ const graceMs = 0;
190
+ const selected = await sweepPending({ artifact: square.artifact, hostLedger, location: squarePath, now: Date.now(), graceMs, limit: Number.isFinite(limit) && limit > 0 ? limit : 8 }).catch(() => []);
191
+ 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);
194
+ }
195
+ finally {
196
+ await closeOpenSquare(square);
197
+ }
272
198
  }
199
+ catch { /* stale index entries are ignored by the hook */ }
273
200
  }
274
- return [...indexes].sort((left, right) => left - right).slice(0, Math.max(0, limit));
275
201
  }
276
- /** Reconsider old pending attention at a bounded action boundary using the existing worker. */
202
+ /** Select sweep candidates from one frozen snapshot and one delivery replay. */
203
+ export async function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery) {
204
+ const ledger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER, writableScope: 'user', readableScopes: ['user'] });
205
+ return sweepPendingFromState({ state, hostLedger: ledger, location: squarePath, now, graceMs: wakeGraceMs(env), limit, deriveDelivery });
206
+ }
207
+ /** Select old pending attention at a bounded action boundary for an explicit executor. */
277
208
  export async function sweepPendingNotifications(squarePath, opts = {}) {
278
209
  const env = opts.env ?? process.env;
279
210
  if (env.SQUARE_DISABLE_PASEO_WAKE === '1')
280
211
  return [];
281
212
  const now = opts.now ?? Date.now();
282
213
  const limit = opts.limit ?? 8;
283
- const square = await openSquare(squarePath, { clock: () => now });
284
- let state;
214
+ const ledgerRoot = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
215
+ const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot });
216
+ const square = await openSquare(squarePath, { clock: () => now, hostLedger, env });
217
+ let selected;
285
218
  try {
286
- ({ state } = await entryPresentation(square, ''));
219
+ selected = await sweepPending({ artifact: square.artifact, hostLedger, location: squarePath, now, graceMs: wakeGraceMs(env), limit });
287
220
  }
288
221
  finally {
289
222
  await closeOpenSquare(square);
290
223
  }
291
- const selected = await pendingNotificationSweepFromState(squarePath, state, now, env, limit);
292
- const workerPath = fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url));
293
- for (const actIndex of selected) {
294
- (opts.launchWorker ?? launchWorker)(workerPath, ['--location', squarePath, '--act-index', String(actIndex)]);
224
+ if (opts.dispatchCandidate !== undefined) {
225
+ for (const actIndex of selected)
226
+ await opts.dispatchCandidate(actIndex);
295
227
  }
296
228
  return selected;
297
229
  }
@@ -1,11 +1,12 @@
1
- import type { StateCell } from './state-cell.js';
2
- import type { WakeNotifier } from './square-facade.js';
1
+ import type { HostLedgerPort, SquareArtifactPort, WakeTransportPort } from './ports.js';
3
2
  /** Private binding assembled by storage and consumed by the four concerns. */
4
3
  export interface OpenSquare {
5
- readonly cell: StateCell;
4
+ readonly artifact: SquareArtifactPort;
6
5
  readonly clock: () => number;
7
- readonly notifier?: WakeNotifier;
8
6
  readonly location: string;
7
+ readonly hostLedger?: HostLedgerPort;
8
+ readonly wakeTransport?: WakeTransportPort;
9
+ readonly env?: NodeJS.ProcessEnv;
9
10
  }
10
11
  /** Package-private lifecycle boundary for bound squares. */
11
12
  export declare function closeOpenSquare(square: OpenSquare): Promise<void>;
@@ -1,4 +1,4 @@
1
1
  /** Package-private lifecycle boundary for bound squares. */
2
2
  export function closeOpenSquare(square) {
3
- return square.cell.close();
3
+ return square.artifact.close();
4
4
  }
@@ -37,6 +37,7 @@ export class PaseoAdapter {
37
37
  outcome: 'unavailable',
38
38
  signature: 'invalid_address',
39
39
  message: 'Paseo route has no agent id.',
40
+ routeStale: true,
40
41
  diagnostic: diagnostic('selection', address, 'invalid_address'),
41
42
  };
42
43
  }
@@ -58,6 +59,7 @@ export class PaseoAdapter {
58
59
  message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle.',
59
60
  diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : 'not_idle'),
60
61
  ...(agent === undefined ? {} : { retainRoute: true }),
62
+ ...(agent === undefined ? { routeStale: true } : {}),
61
63
  };
62
64
  }
63
65
  if (!(await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent))) {
@@ -0,0 +1,138 @@
1
+ import type { InboxNotification, SquareState, StoredAct, WakeRoute, WatchLease } from './model.js';
2
+ import type { PlannedNotification } from './delivery.js';
3
+ export type { ClaimResult, EvidenceClaim, EvidenceRelease, EvidenceGc, EvidenceLookup, EvidenceRecord, HostLedgerPort, HostLedgerScope, PresenceKey, PresenceLookup, PresenceRecord, PresenceChannel, PresenceResult, ReconcileBindingInput as HostLedgerReconcileBindingInput, ReconcileBindingResult, } from './host-ledger.js';
4
+ import type { HostLedgerPort } from './host-ledger.js';
5
+ /** Atomic artifact access. Never exposes framing, compression, locks, or storage schema. */
6
+ export interface SquareArtifactPort {
7
+ read(): Promise<{
8
+ state: SquareState;
9
+ version: number;
10
+ }>;
11
+ transact<R>(fn: (state: SquareState, version: number) => {
12
+ state?: SquareState;
13
+ result: R;
14
+ }): Promise<R>;
15
+ changed(sinceVersion: number, timeoutMs: number): Promise<boolean>;
16
+ close(): Promise<void>;
17
+ }
18
+ /** Capability-neutral wake transport. Unused by this Contract's activity operations. */
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
+ }>;
25
+ attempt(request: WakeRequest, timeoutMs: number): Promise<WakeOutcome>;
26
+ /** Optional route retirement supplied by the concrete executor adapter. */
27
+ invalidate?(request: WakeRequest): Promise<void>;
28
+ }
29
+ export interface WakeRequest {
30
+ readonly location: string;
31
+ readonly participant: string;
32
+ readonly activity: string;
33
+ readonly route: WakeRoute;
34
+ }
35
+ /** Executor-neutral presentation capability; intentionally separate from wake transport. */
36
+ export interface PresentationSinkPort {
37
+ present(activity: StoredAct): void | Promise<void>;
38
+ }
39
+ /** Stable projection of a host binding; storage rows never cross this boundary. */
40
+ export interface SessionBindingProjection {
41
+ readonly location: string;
42
+ readonly participant: string;
43
+ readonly sessionId: string;
44
+ readonly channel: import('./host-ledger.js').PresenceChannel;
45
+ readonly route?: WakeRoute;
46
+ readonly updatedAt: number;
47
+ }
48
+ /** Pending boundary presentation projected for one bound session. */
49
+ export interface PresentationProjection {
50
+ readonly binding: SessionBindingProjection;
51
+ readonly joined: boolean;
52
+ readonly notifications: readonly InboxNotification[];
53
+ readonly catchLease?: WatchLease;
54
+ }
55
+ /** Read-only presentation evidence projection for diagnostics and migration callers. */
56
+ export interface PresentationEvidenceProjection {
57
+ readonly location: string;
58
+ readonly participant: string;
59
+ readonly sessionId: string;
60
+ readonly activity: string;
61
+ readonly outcome: string;
62
+ readonly at?: number;
63
+ }
64
+ export type WakeOutcome = {
65
+ readonly outcome: 'accepted';
66
+ readonly signature?: string;
67
+ readonly attemptN?: number;
68
+ } | {
69
+ readonly outcome: 'failed';
70
+ readonly message?: string;
71
+ readonly attemptN?: number;
72
+ readonly unavailable?: boolean;
73
+ readonly retainRoute?: boolean;
74
+ readonly routeStale?: boolean;
75
+ } | {
76
+ readonly outcome: 'not-capable';
77
+ readonly diagnostic?: string;
78
+ } | {
79
+ readonly outcome: 'unknown';
80
+ readonly diagnostic?: string;
81
+ readonly attemptN?: number;
82
+ };
83
+ export interface SquareObservation {
84
+ readonly location?: string;
85
+ readonly version: number;
86
+ readonly state: SquareState;
87
+ readonly pending: readonly {
88
+ readonly recipient: string;
89
+ readonly notifications: readonly PlannedNotification[];
90
+ }[];
91
+ readonly bindings: readonly SessionBindingProjection[];
92
+ }
93
+ export interface ObserveSquareInput {
94
+ readonly artifact: SquareArtifactPort;
95
+ readonly hostLedger?: HostLedgerPort;
96
+ readonly location?: string;
97
+ readonly now?: number;
98
+ }
99
+ export interface ReconcileBindingInput {
100
+ readonly artifact: SquareArtifactPort;
101
+ readonly hostLedger: HostLedgerPort;
102
+ readonly location?: string;
103
+ readonly scopes?: readonly import('./host-ledger.js').HostLedgerScope[];
104
+ readonly now?: number;
105
+ }
106
+ export interface DeliveryResult {
107
+ readonly attempted: number;
108
+ readonly accepted: number;
109
+ readonly failed: number;
110
+ readonly unknown: number;
111
+ readonly notCapable: number;
112
+ }
113
+ export interface DeliverPendingInput {
114
+ readonly artifact: SquareArtifactPort;
115
+ readonly hostLedger: HostLedgerPort;
116
+ readonly transport: WakeTransportPort;
117
+ readonly location: string;
118
+ readonly timeoutMs?: number;
119
+ readonly now?: number;
120
+ readonly activity?: string | number;
121
+ }
122
+ export interface PresentationResult {
123
+ readonly presented: boolean;
124
+ readonly activity?: StoredAct;
125
+ }
126
+ export interface PresentPendingInput {
127
+ readonly artifact: SquareArtifactPort;
128
+ readonly location: string;
129
+ readonly participant: string;
130
+ readonly activity: string | number;
131
+ readonly sink: PresentationSinkPort;
132
+ readonly hostLedger?: HostLedgerPort;
133
+ readonly session?: string;
134
+ readonly now?: number;
135
+ readonly timeoutMs?: number;
136
+ /** Boundary previews that were clipped remain pending for a later full presentation. */
137
+ readonly markSeen?: boolean;
138
+ }
package/dist/ports.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -3,5 +3,4 @@ import type { OpenSquare } from './open-square.js';
3
3
  import type { CatchOptions, CatchResult } from './square-facade.js';
4
4
  export declare function catchUp(square: OpenSquare, name: string, options?: CatchOptions, deriveDelivery?: (state: import('./model.js').SquareState) => DeliveryModel): Promise<CatchResult>;
5
5
  /** Commit seen only for complete, actually rendered boundary bodies. */
6
- export declare function markBoundarySeen(squarePath: string, name: string, ownerId: string | undefined, actIndexes: readonly number[], at?: number): Promise<void>;
7
- export declare function markNotificationNotified(square: OpenSquare, name: string, actIndex: number, ownerId: string | undefined, at?: number): Promise<void>;
6
+ export declare function markBoundarySeen(squarePath: string, name: string, actIndexes: readonly number[], at?: number): Promise<void>;