@astrosheep/square 0.3.29 → 0.3.31

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 (77) 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 +8 -7
  4. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  5. package/dist/activity.js +7 -3
  6. package/dist/artifact.js +7 -50
  7. package/dist/automatic-session.js +31 -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 +1 -1
  13. package/dist/cli/context.js +10 -2
  14. package/dist/cli/observation-commands.d.ts +5 -1
  15. package/dist/cli/observation-commands.js +68 -33
  16. package/dist/cli/square-commands.js +17 -10
  17. package/dist/codex-hook.d.ts +1 -1
  18. package/dist/decisions.js +2 -2
  19. package/dist/delivery-health.d.ts +1 -1
  20. package/dist/delivery-operations.d.ts +25 -0
  21. package/dist/delivery-operations.js +124 -0
  22. package/dist/help.js +1 -1
  23. package/dist/host-ledger-file-adapter.d.ts +34 -0
  24. package/dist/host-ledger-file-adapter.js +165 -0
  25. package/dist/host-ledger.d.ts +160 -0
  26. package/dist/host-ledger.js +1 -0
  27. package/dist/inbox.d.ts +2 -2
  28. package/dist/inbox.js +22 -14
  29. package/dist/index.d.ts +4 -1
  30. package/dist/index.js +1 -0
  31. package/dist/landing.d.ts +18 -14
  32. package/dist/landing.js +29 -113
  33. package/dist/model.d.ts +6 -17
  34. package/dist/notifications.d.ts +6 -10
  35. package/dist/notifications.js +71 -192
  36. package/dist/open-square.d.ts +4 -4
  37. package/dist/open-square.js +1 -1
  38. package/dist/ports.d.ts +127 -0
  39. package/dist/ports.js +1 -0
  40. package/dist/presence.d.ts +1 -2
  41. package/dist/presence.js +13 -46
  42. package/dist/presentation-operations.d.ts +3 -0
  43. package/dist/presentation-operations.js +51 -0
  44. package/dist/presentation.d.ts +2 -2
  45. package/dist/presentation.js +11 -7
  46. package/dist/presented.d.ts +1 -12
  47. package/dist/presented.js +6 -75
  48. package/dist/registry.d.ts +10 -10
  49. package/dist/registry.js +48 -113
  50. package/dist/routes.d.ts +6 -33
  51. package/dist/routes.js +6 -170
  52. package/dist/runtime.d.ts +1 -1
  53. package/dist/runtime.js +3 -4
  54. package/dist/square-actions.d.ts +32 -0
  55. package/dist/square-actions.js +167 -0
  56. package/dist/square-facade.d.ts +7 -7
  57. package/dist/square-file-adapter.d.ts +4 -3
  58. package/dist/square-file-adapter.js +22 -5
  59. package/dist/square-projections.d.ts +68 -0
  60. package/dist/square-projections.js +87 -0
  61. package/dist/square-storage.d.ts +2 -2
  62. package/dist/square-storage.js +14 -9
  63. package/dist/square-wiring.d.ts +3 -3
  64. package/dist/square-wiring.js +44 -29
  65. package/dist/views.d.ts +8 -2
  66. package/dist/views.js +28 -24
  67. package/dist/wake-attempts.d.ts +29 -22
  68. package/dist/wake-attempts.js +36 -119
  69. package/dist/wake-evidence.d.ts +6 -18
  70. package/dist/wake-evidence.js +17 -80
  71. package/dist/wakes.d.ts +2 -16
  72. package/dist/wakes.js +7 -27
  73. package/dist/watch.js +2 -3
  74. package/extensions/square-pi.js +7 -2
  75. package/package.json +1 -1
  76. package/skills/brainstorm/SKILL.md +2 -2
  77. package/skills/square/SKILL.md +8 -7
package/dist/model.d.ts CHANGED
@@ -5,8 +5,10 @@ export declare const WAKE_ROUTE_KINDS: readonly ["opencode-server", "codex-queue
5
5
  export type WakeRouteKind = typeof WAKE_ROUTE_KINDS[number];
6
6
  export declare function isWakeRouteKind(value: unknown): value is WakeRouteKind;
7
7
  export interface WakeRoute {
8
- ownerId: string;
8
+ location: string;
9
+ participant: string;
9
10
  sessionId: string;
11
+ channel: string;
10
12
  kind: WakeRouteKind;
11
13
  address: Record<string, string>;
12
14
  updatedAt: number;
@@ -40,11 +42,10 @@ export type StoredAct = Act & {
40
42
  at: number;
41
43
  };
42
44
  export type StoredActHead = StoredAct extends infer T ? T extends StoredAct ? Omit<T, 'body' | 'through' | 'index'> : never : never;
43
- export type ObservationState = 'notified' | 'seen';
45
+ export type ObservationState = 'seen';
44
46
  export interface ActivityObservation {
45
47
  state: ObservationState;
46
48
  at: number;
47
- ownerId?: string;
48
49
  }
49
50
  export interface WatchLeaseFilter {
50
51
  participants?: string[];
@@ -52,8 +53,6 @@ export interface WatchLeaseFilter {
52
53
  }
53
54
  export interface WatchLease {
54
55
  leaseId: string;
55
- /** Machine-local owner that may claim automatic delivery for this lease. */
56
- ownerId?: string;
57
56
  heartbeatAt: number;
58
57
  expiresAt: number;
59
58
  filter?: WatchLeaseFilter;
@@ -69,22 +68,13 @@ export interface InboxNotification {
69
68
  export interface InboxMembership {
70
69
  name: string;
71
70
  squarePath: string;
72
- ownerId?: string;
73
71
  notifications: InboxNotification[];
74
72
  catchLease?: WatchLease;
75
73
  }
76
- export interface NotifyLease {
77
- leaseId: string;
78
- expiresAt: number;
79
- phase: 'claimed' | 'dispatching';
80
- attemptN?: number;
81
- routeKind?: WakeRouteKind;
82
- }
83
74
  export interface SquareRuntimeState {
84
75
  nextActIndex: number;
85
76
  observations: Record<string, Record<string, ActivityObservation>>;
86
77
  leases: Record<string, WatchLease>;
87
- notifyLeases: Record<string, NotifyLease>;
88
78
  }
89
79
  export interface SquareState {
90
80
  hardCap: HardCap;
@@ -97,12 +87,12 @@ export interface SquareState {
97
87
  export interface ActivitiesOptions {
98
88
  lastN?: number | null;
99
89
  participants?: string[];
100
- /** Exclusive upper bound on act.at (ms). */
101
- before?: number;
102
90
  /** Exclusive lower bound on act.at (ms). Alias of --since. */
103
91
  after?: number;
104
92
  /** Exclusive lower bound on stable act index (--after act/<index>). */
105
93
  afterIndex?: number;
94
+ /** Exclusive upper bound on stable act index (--before act/<index>). */
95
+ beforeIndex?: number;
106
96
  /** Center act indexes for context windows (--at). */
107
97
  atIndexes?: number[];
108
98
  beforeContext?: number;
@@ -111,7 +101,6 @@ export interface ActivitiesOptions {
111
101
  /** Undelivered mention/bell items for viewer only (read-only; requires viewer). */
112
102
  pending?: boolean;
113
103
  viewer?: string;
114
- full?: boolean;
115
104
  grep?: string;
116
105
  fixed?: string;
117
106
  order?: 'asc' | 'desc';
@@ -1,8 +1,7 @@
1
- import { planActNotifications, type DeliveryModel, type WakeAdapter } from './delivery.js';
1
+ import { planActNotifications, type WakeAdapter } from './delivery.js';
2
2
  import { type SquareState } from './model.js';
3
3
  import { matchesMentionTarget } from './runtime.js';
4
4
  import { type ActivityId } from './square-core.js';
5
- import type { WakeNotifier } from './square-facade.js';
6
5
  export type { PlannedNotification } from './delivery.js';
7
6
  export { planActNotifications, matchesMentionTarget };
8
7
  export { notificationMessageId } from './delivery.js';
@@ -17,17 +16,14 @@ interface ProcessNotificationOptions {
17
16
  env?: NodeJS.ProcessEnv;
18
17
  now?: () => number;
19
18
  }
20
- export declare function processActNotificationsOnce(squarePath: string, actIndex: number, opts?: ProcessNotificationOptions): Promise<void>;
21
- interface WorkerLaunchOptions {
22
- launchWorker?: (workerPath: string, args: string[]) => void;
19
+ export declare function processActNotificationsOnce(squarePath: string, actIndex: number, opts?: ProcessNotificationOptions): Promise<import("./ports.js").DeliveryResult>;
20
+ export interface SweepPendingNotificationsOptions {
23
21
  env?: NodeJS.ProcessEnv;
24
- }
25
- export declare function wakeNotifierForSquare(squarePath: string, env?: NodeJS.ProcessEnv): WakeNotifier;
26
- export interface SweepPendingNotificationsOptions extends WorkerLaunchOptions {
27
22
  now?: number;
28
23
  limit?: number;
24
+ dispatchCandidate?: (actIndex: number) => void | Promise<void>;
29
25
  }
30
26
  /** Select sweep candidates from one frozen snapshot and one delivery replay. */
31
- export declare function pendingNotificationSweepFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, limit: number, deriveDelivery?: (snapshot: SquareState) => DeliveryModel): Promise<number[]>;
32
- /** Reconsider old pending attention at a bounded action boundary using the existing worker. */
27
+ export declare function pendingNotificationSweepFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, limit: number, deriveDelivery?: (snapshot: import('./model.js').SquareState) => ReturnType<typeof import('./delivery.js').deriveDeliveryModel>): Promise<number[]>;
28
+ /** Select old pending attention at a bounded action boundary for an explicit executor. */
33
29
  export declare function sweepPendingNotifications(squarePath: string, opts?: SweepPendingNotificationsOptions): Promise<number[]>;
@@ -1,25 +1,16 @@
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 { planActNotifications, } from './delivery.js';
8
4
  import { SquareError } from './model.js';
9
5
  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';
6
+ import { formatActivityId, parseActivityId } from './square-core.js';
7
+ import { displayAttentionPath } from './attention-presentation.js';
14
8
  import { openSquare } from './square-file-adapter.js';
15
- import { markNotificationNotified } from './square-wiring.js';
16
9
  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;
10
+ import { notificationDelivered, resolveParticipant } from './views.js';
11
+ import { deliverPending, sweepPending, sweepPendingFromState } from './delivery-operations.js';
12
+ import { projectPresentationEvidence } from './square-projections.js';
13
+ import { createHostLedgerPort } from './host-ledger-file-adapter.js';
23
14
  export { planActNotifications, matchesMentionTarget };
24
15
  export { notificationMessageId } from './delivery.js';
25
16
  export function wakeGraceMs(env = process.env) {
@@ -36,48 +27,14 @@ function wakeLabel(kind) {
36
27
  return 'codex-queue';
37
28
  return kind;
38
29
  }
39
- function renderWakePayload(request, body, kind) {
30
+ function renderWakePayload(request) {
40
31
  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
- }),
32
+ `<system-reminder source="square" wake="${wakeLabel(request.route.kind)}">`,
33
+ `square: ${displayAttentionPath(request.location)}`,
34
+ `attention: ${request.activity} for ${request.participant}`,
52
35
  '</system-reminder>',
53
36
  ].join('\n');
54
37
  }
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
38
  function notificationIndex(ref) {
82
39
  if (typeof ref === 'number')
83
40
  return ref;
@@ -101,7 +58,11 @@ export async function hasAttentionNotification(squarePath, name, ref, env = proc
101
58
  try {
102
59
  const recipient = (await resolveParticipant(square, name)).name;
103
60
  const index = notificationIndex(ref);
104
- return await notificationDelivered(square, recipient, index) || await hasPresentedAttention(squarePath, recipient, index, env);
61
+ if (await notificationDelivered(square, recipient, index))
62
+ return true;
63
+ const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
64
+ const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? root, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root, readableScopes: ['user'], writableScope: 'user' });
65
+ return (await projectPresentationEvidence({ hostLedger, location: squarePath, participant: recipient, activity: formatActivityId(index), now: Date.now() })).some((row) => row.outcome === 'presented');
105
66
  }
106
67
  finally {
107
68
  await closeOpenSquare(square);
@@ -134,164 +95,82 @@ async function defaultWakeAdapters() {
134
95
  }
135
96
  return adapters;
136
97
  }
137
- async function claimNotifyLease(square, recipient, actIndex) {
138
- const leaseId = randomUUID();
139
- return claimNotificationLease(square, recipient, actIndex, leaseId, NOTIFY_LEASE_MS);
140
- }
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);
98
+ function createWakeTransport(adapters, hostLedger, clock) {
99
+ return {
100
+ attempt: async (request, _timeoutMs) => {
101
+ const adapter = adapters.find((candidate) => candidate.kind === request.route.kind);
102
+ if (adapter === undefined)
103
+ return { outcome: 'failed', message: 'wake adapter unavailable' };
104
+ try {
105
+ const result = await adapter.dispatch(request.route.address, renderWakePayload(request), async () => true);
106
+ if (result.outcome === 'accepted')
107
+ return { outcome: 'accepted' };
108
+ if (result.outcome === 'failed')
109
+ return { outcome: 'failed', message: result.message };
110
+ if (result.outcome === 'unavailable')
111
+ return { outcome: 'failed', message: result.message, unavailable: true };
112
+ if (result.outcome === 'unknown')
113
+ return { outcome: 'unknown', diagnostic: result.message };
114
+ return { outcome: 'unknown', diagnostic: 'wake dispatch cancelled' };
115
+ }
116
+ catch (error) {
117
+ return { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
118
+ }
119
+ },
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
+ };
146
130
  }
147
- async function processNotification(squarePath, notification, opts) {
131
+ export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
148
132
  const env = opts.env ?? process.env;
149
133
  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);
168
- }
169
- await closeOpenSquare(square);
170
- return;
171
- }
172
- const { leaseId } = claim;
173
- let releaseLease = true;
134
+ const ledgerRoot = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
135
+ const hostLedger = createHostLedgerPort({
136
+ userPath: env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot,
137
+ localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot,
138
+ });
139
+ const square = await openSquare(squarePath, { clock: now, hostLedger, env });
174
140
  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
- });
141
+ const adapters = opts.adapters ?? await defaultWakeAdapters();
142
+ 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() });
230
144
  }
231
145
  finally {
232
- if (releaseLease) {
233
- await releaseNotifyLease(square, notification.recipient, notification.item.index, leaseId);
234
- }
235
146
  await closeOpenSquare(square);
236
147
  }
237
148
  }
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
149
  /** 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);
272
- }
273
- }
274
- return [...indexes].sort((left, right) => left - right).slice(0, Math.max(0, limit));
150
+ export async function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery) {
151
+ const ledger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER, writableScope: 'user', readableScopes: ['user'] });
152
+ return sweepPendingFromState({ state, hostLedger: ledger, location: squarePath, now, graceMs: wakeGraceMs(env), limit, deriveDelivery });
275
153
  }
276
- /** Reconsider old pending attention at a bounded action boundary using the existing worker. */
154
+ /** Select old pending attention at a bounded action boundary for an explicit executor. */
277
155
  export async function sweepPendingNotifications(squarePath, opts = {}) {
278
156
  const env = opts.env ?? process.env;
279
157
  if (env.SQUARE_DISABLE_PASEO_WAKE === '1')
280
158
  return [];
281
159
  const now = opts.now ?? Date.now();
282
160
  const limit = opts.limit ?? 8;
283
- const square = await openSquare(squarePath, { clock: () => now });
284
- let state;
161
+ const ledgerRoot = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
162
+ const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot });
163
+ const square = await openSquare(squarePath, { clock: () => now, hostLedger, env });
164
+ let selected;
285
165
  try {
286
- ({ state } = await entryPresentation(square, ''));
166
+ selected = await sweepPending({ artifact: square.artifact, hostLedger, location: squarePath, now, graceMs: wakeGraceMs(env), limit });
287
167
  }
288
168
  finally {
289
169
  await closeOpenSquare(square);
290
170
  }
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)]);
171
+ if (opts.dispatchCandidate !== undefined) {
172
+ for (const actIndex of selected)
173
+ await opts.dispatchCandidate(actIndex);
295
174
  }
296
175
  return selected;
297
176
  }
@@ -1,11 +1,11 @@
1
- import type { StateCell } from './state-cell.js';
2
- import type { WakeNotifier } from './square-facade.js';
1
+ import type { HostLedgerPort, SquareArtifactPort } 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 env?: NodeJS.ProcessEnv;
9
9
  }
10
10
  /** Package-private lifecycle boundary for bound squares. */
11
11
  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
  }
@@ -0,0 +1,127 @@
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
+ attempt(request: WakeRequest, timeoutMs: number): Promise<WakeOutcome>;
21
+ /** Optional route retirement supplied by the concrete executor adapter. */
22
+ invalidate?(request: WakeRequest): Promise<void>;
23
+ }
24
+ export interface WakeRequest {
25
+ readonly location: string;
26
+ readonly participant: string;
27
+ readonly activity: string;
28
+ readonly route: WakeRoute;
29
+ }
30
+ /** Executor-neutral presentation capability; intentionally separate from wake transport. */
31
+ export interface PresentationSinkPort {
32
+ present(activity: StoredAct): void | Promise<void>;
33
+ }
34
+ /** Stable projection of a host binding; storage rows never cross this boundary. */
35
+ export interface SessionBindingProjection {
36
+ readonly location: string;
37
+ readonly participant: string;
38
+ readonly sessionId: string;
39
+ readonly channel: import('./host-ledger.js').PresenceChannel;
40
+ readonly route?: WakeRoute;
41
+ readonly updatedAt: number;
42
+ }
43
+ /** Pending boundary presentation projected for one bound session. */
44
+ export interface PresentationProjection {
45
+ readonly binding: SessionBindingProjection;
46
+ readonly joined: boolean;
47
+ readonly notifications: readonly InboxNotification[];
48
+ readonly catchLease?: WatchLease;
49
+ }
50
+ /** Read-only presentation evidence projection for diagnostics and migration callers. */
51
+ export interface PresentationEvidenceProjection {
52
+ readonly location: string;
53
+ readonly participant: string;
54
+ readonly sessionId: string;
55
+ readonly activity: string;
56
+ readonly outcome: string;
57
+ readonly at?: number;
58
+ }
59
+ export type WakeOutcome = {
60
+ readonly outcome: 'accepted';
61
+ readonly signature?: string;
62
+ readonly attemptN?: number;
63
+ } | {
64
+ readonly outcome: 'failed';
65
+ readonly message?: string;
66
+ readonly attemptN?: number;
67
+ readonly unavailable?: boolean;
68
+ } | {
69
+ readonly outcome: 'unknown';
70
+ readonly diagnostic?: string;
71
+ readonly attemptN?: number;
72
+ };
73
+ export interface SquareObservation {
74
+ readonly location?: string;
75
+ readonly version: number;
76
+ readonly state: SquareState;
77
+ readonly pending: readonly {
78
+ readonly recipient: string;
79
+ readonly notifications: readonly PlannedNotification[];
80
+ }[];
81
+ readonly bindings: readonly SessionBindingProjection[];
82
+ }
83
+ export interface ObserveSquareInput {
84
+ readonly artifact: SquareArtifactPort;
85
+ readonly hostLedger?: HostLedgerPort;
86
+ readonly location?: string;
87
+ readonly now?: number;
88
+ }
89
+ export interface ReconcileBindingInput {
90
+ readonly artifact: SquareArtifactPort;
91
+ readonly hostLedger: HostLedgerPort;
92
+ readonly location?: string;
93
+ readonly scopes?: readonly import('./host-ledger.js').HostLedgerScope[];
94
+ readonly now?: number;
95
+ }
96
+ export interface DeliveryResult {
97
+ readonly attempted: number;
98
+ readonly accepted: number;
99
+ readonly failed: number;
100
+ readonly unknown: number;
101
+ }
102
+ export interface DeliverPendingInput {
103
+ readonly artifact: SquareArtifactPort;
104
+ readonly hostLedger: HostLedgerPort;
105
+ readonly transport: WakeTransportPort;
106
+ readonly location: string;
107
+ readonly timeoutMs?: number;
108
+ readonly now?: number;
109
+ readonly activity?: string | number;
110
+ }
111
+ export interface PresentationResult {
112
+ readonly presented: boolean;
113
+ readonly activity?: StoredAct;
114
+ }
115
+ export interface PresentPendingInput {
116
+ readonly artifact: SquareArtifactPort;
117
+ readonly location: string;
118
+ readonly participant: string;
119
+ readonly activity: string | number;
120
+ readonly sink: PresentationSinkPort;
121
+ readonly hostLedger?: HostLedgerPort;
122
+ readonly session?: string;
123
+ readonly now?: number;
124
+ readonly timeoutMs?: number;
125
+ /** Boundary previews that were clipped remain pending for a later full presentation. */
126
+ readonly markSeen?: boolean;
127
+ }
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>;