@astrosheep/square 0.3.42 → 0.3.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.42",
3
+ "version": "0.3.43",
4
4
  "description": "Native Claude Code turn-boundary delivery for Square participants",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.42",
3
+ "version": "0.3.43",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,5 +1,5 @@
1
1
  import type { DirectedNotificationRoute } from './model.js';
2
- export declare const ATTENTION_BODY_MAX = 100;
2
+ export declare const ATTENTION_BODY_MAX = 200;
3
3
  export interface AttentionPreview {
4
4
  squarePath: string;
5
5
  actIndex: number;
@@ -2,7 +2,7 @@ import { homedir } from 'node:os';
2
2
  import { notificationMessageId } from './delivery.js';
3
3
  import { participantIdentity } from './presentation.js';
4
4
  import { formatActivityId } from './square-core.js';
5
- export const ATTENTION_BODY_MAX = 100;
5
+ export const ATTENTION_BODY_MAX = 200;
6
6
  export function previewAttentionBody(body) {
7
7
  const compact = body.replace(/\r\n/g, '\n');
8
8
  if (compact.length <= ATTENTION_BODY_MAX)
@@ -2,23 +2,31 @@ import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { openSquare } from './square-file-adapter.js';
4
4
  import { closeOpenSquare } from './open-square.js';
5
- import { openParticipant, Square } from './square-wiring.js';
5
+ import { Square } from './square-wiring.js';
6
6
  import { entryPresentation } from './views.js';
7
7
  import { automaticParticipant } from './participant-identity.js';
8
8
  import { SquareError } from './model.js';
9
9
  import { createHostLedgerPort } from './host-ledger-file-adapter.js';
10
10
  import { projectSessionBindings } from './square-projections.js';
11
- import { claimSessionParticipant, releaseSessionParticipant } from './registry.js';
12
- import { publishWakeRoute, retireWakeRouteFromArtifact, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
11
+ import { claimSessionParticipant, readParticipantOwner, releaseSessionParticipant } from './registry.js';
12
+ import { publishWakeRoute, retireWakeRoutesForSessionFromArtifact, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
13
13
  export { automaticParticipant } from './participant-identity.js';
14
14
  const providerEnv = {
15
15
  codex: 'CODEX_THREAD_ID',
16
16
  claude: 'CLAUDE_CODE_SESSION_ID',
17
17
  opencode: 'OPENCODE_SESSION_ID',
18
- pi: 'PI_SESSION_ID',
18
+ pi: 'SQUARE_PI_SESSION_ID',
19
19
  };
20
20
  function operationEnv(provider, sessionId, env) {
21
- return { ...env, [providerEnv[provider]]: sessionId };
21
+ return {
22
+ ...env,
23
+ CLAUDE_CODE_SESSION_ID: '',
24
+ CLAUDE_CODE_CHILD_SESSION: '',
25
+ CODEX_THREAD_ID: '',
26
+ OPENCODE_SESSION_ID: '',
27
+ SQUARE_PI_SESSION_ID: '',
28
+ [providerEnv[provider]]: sessionId,
29
+ };
22
30
  }
23
31
  function hostLedgerForEnv(env) {
24
32
  const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
@@ -82,13 +90,20 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
82
90
  if (route !== undefined) {
83
91
  const publisher = await openSquare(squarePath, { hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv });
84
92
  try {
85
- await publishWakeRoute(publisher.artifact, route, { at: Date.now() });
93
+ await publishWakeRoute(publisher.artifact, { ...route, ...(claim?.epoch === undefined || claim.epoch <= 0 ? {} : { epoch: claim.epoch }) }, { at: Date.now(), requireCurrentSession: true });
86
94
  }
87
95
  finally {
88
96
  await closeOpenSquare(publisher);
89
97
  }
90
98
  }
91
- await hostLedgerForEnv(env).ensurePresence({ location: squarePath, participant: name, session: sessionId, channel: provider === 'claude' ? 'claude-code' : provider, updatedAt: Date.now() }, 'user');
99
+ await hostLedgerForEnv(env).ensurePresence({
100
+ location: squarePath,
101
+ participant: name,
102
+ session: sessionId,
103
+ channel: provider === 'claude' ? 'claude-code' : provider,
104
+ updatedAt: Date.now(),
105
+ ...(claim?.epoch === undefined || claim.epoch <= 0 ? {} : { epoch: claim.epoch }),
106
+ }, 'user');
92
107
  await square.reconcileBinding();
93
108
  return undefined;
94
109
  }
@@ -103,6 +118,8 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
103
118
  }
104
119
  export async function automaticSessionEnd(provider, sessionId, cwd, env = process.env) {
105
120
  const squarePath = publicSquarePath(cwd);
121
+ if (!await squareExists(squarePath))
122
+ return;
106
123
  const scopedEnv = operationEnv(provider, sessionId, env);
107
124
  const hostLedger = hostLedgerForEnv(scopedEnv);
108
125
  const probe = await openSquare(squarePath, { hostLedger, env: scopedEnv });
@@ -112,32 +129,30 @@ export async function automaticSessionEnd(provider, sessionId, cwd, env = proces
112
129
  location: probe.location,
113
130
  scopes: ['user', 'local'],
114
131
  }))[0];
132
+ const expectedEpoch = binding === undefined
133
+ ? undefined
134
+ : (await readParticipantOwner(squarePath, binding.participant, scopedEnv))?.epoch;
115
135
  await closeOpenSquare(probe);
116
- if (binding === undefined || !await squareExists(squarePath))
117
- return;
118
- const reader = await openSquare(squarePath);
119
- const joined = await entryPresentation(reader, binding.participant).finally(() => closeOpenSquare(reader));
120
- if (!joined.joined)
121
- return;
122
- const opened = await openParticipant({ path: squarePath, hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv }, binding.participant);
136
+ const square = await Square.at({ path: squarePath, hostLedger, env: scopedEnv });
123
137
  try {
124
- await opened.participant.done();
125
- const reconciler = await openSquare(squarePath);
126
- try {
127
- await hostLedger.reconcileBinding({ artifact: reconciler.artifact });
128
- }
129
- finally {
130
- await closeOpenSquare(reconciler);
138
+ if (binding !== undefined) {
139
+ const reader = await openSquare(squarePath);
140
+ const joined = await entryPresentation(reader, binding.participant).finally(() => closeOpenSquare(reader));
141
+ if (joined.joined) {
142
+ const ended = await square.endOwnedSession(binding.participant, sessionId, expectedEpoch);
143
+ if (ended !== null)
144
+ await square.reconcileBinding();
145
+ }
131
146
  }
132
147
  const cleanup = await openSquare(squarePath);
133
148
  try {
134
- await retireWakeRouteFromArtifact(cleanup.artifact, { location: squarePath, participant: binding.participant, sessionId });
149
+ await retireWakeRoutesForSessionFromArtifact(cleanup.artifact, { location: squarePath, sessionId });
135
150
  }
136
151
  finally {
137
152
  await closeOpenSquare(cleanup);
138
153
  }
139
154
  }
140
155
  finally {
141
- await opened.close();
156
+ await square.close();
142
157
  }
143
158
  }
@@ -147,5 +147,5 @@ export async function presentPendingAtBoundary(sessionId, present, lookup = sess
147
147
  const ledgerRoot = env.SQUARE_HOST_LEDGER_USER
148
148
  ?? (env.SQUARE_REGISTRY === undefined ? path.join(os.homedir(), '.square', 'host-ledger') : path.dirname(env.SQUARE_REGISTRY));
149
149
  const ownerKey = createHash('sha256').update(sessionId).digest('hex');
150
- return withFileLock(path.join(ledgerRoot, `presentation-boundary-${ownerKey}.lock`), { retryMs: 10, staleMs: 300_000, signal }, () => presentPendingAtBoundaryUnlocked(sessionId, present, lookup, env, signal));
150
+ return withFileLock(path.join(ledgerRoot, `presentation-boundary-${ownerKey}.lock`), { retryMs: 10, signal }, () => presentPendingAtBoundaryUnlocked(sessionId, present, lookup, env, signal));
151
151
  }
@@ -63,7 +63,7 @@ export async function recordCodexBoundary(threadId, event, env = process.env) {
63
63
  if (!threadId)
64
64
  return;
65
65
  const filePath = statePath(env);
66
- await withFileLock(lockPath(filePath), { retryMs: 10, staleMs: 30_000 }, async () => {
66
+ await withFileLock(lockPath(filePath), { retryMs: 10 }, async () => {
67
67
  const value = await readFile(filePath);
68
68
  value.nextSequence += 1;
69
69
  const current = value.threads[threadId] ?? { lastStop: 0, lastNonStop: 0 };
@@ -77,7 +77,7 @@ export async function clearCodexBoundary(threadId, env = process.env) {
77
77
  if (!threadId)
78
78
  return;
79
79
  const filePath = statePath(env);
80
- await withFileLock(lockPath(filePath), { retryMs: 10, staleMs: 30_000 }, async () => {
80
+ await withFileLock(lockPath(filePath), { retryMs: 10 }, async () => {
81
81
  const value = await readFile(filePath);
82
82
  if (!(threadId in value.threads))
83
83
  return;
@@ -1,11 +1,11 @@
1
1
  import { type SquareState } from './model.js';
2
- import type { HostLedgerPort, PresenceRecord, SquareArtifactPort, DeliverPendingInput, DeliveryResult, ObserveSquareInput, ReconcileBindingInput, SquareObservation } from './ports.js';
2
+ import type { HostLedgerPort, PresenceRecord, PresentationEvidenceProjection, SquareArtifactPort, DeliverPendingInput, DeliveryResult, ObserveSquareInput, ReconcileBindingInput, SquareObservation } from './ports.js';
3
3
  import { deriveDeliveryModel } from './delivery.js';
4
4
  import { type WakeAttempt } from './square-projections.js';
5
5
  export declare function observeSquare(input: ObserveSquareInput): Promise<SquareObservation>;
6
6
  export declare function reconcileBinding(input: ReconcileBindingInput): Promise<import("./host-ledger.js").ReconcileBindingResult>;
7
7
  export declare function deliverPending(input: DeliverPendingInput): Promise<DeliveryResult>;
8
- export declare function selectPendingWakeActivities(state: SquareState, routes: readonly PresenceRecord[], attempts: readonly WakeAttempt[], now: number, graceMs: number, limit: number, delivery?: import("./delivery.js").DeliveryModel): number[];
8
+ export declare function selectPendingWakeActivities(state: SquareState, routes: readonly PresenceRecord[], attempts: readonly WakeAttempt[], now: number, graceMs: number, limit: number, delivery?: import("./delivery.js").DeliveryModel, presentations?: readonly PresentationEvidenceProjection[]): number[];
9
9
  export declare function sweepPending(input: {
10
10
  readonly artifact: SquareArtifactPort;
11
11
  readonly hostLedger: HostLedgerPort;
@@ -2,15 +2,15 @@ import { randomUUID } from 'node:crypto';
2
2
  import { formatActivityId, parseActivityId } from './square-core.js';
3
3
  import { nameKey } from './model.js';
4
4
  import { deriveDeliveryModel } from './delivery.js';
5
- import { isWakeRouteAttemptable } from './square-projections.js';
5
+ import { currentSessionBindings, isWakeRouteAttemptable, presentationSuppressesWake, projectPresentationEvidence } from './square-projections.js';
6
6
  import { retireWakeRouteFromArtifact } from './routes.js';
7
- async function attemptWakeWithin(transport, request, timeoutMs) {
7
+ async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
8
8
  let timer;
9
9
  const timeout = new Promise((resolve) => {
10
10
  timer = setTimeout(() => resolve({ outcome: 'unknown', diagnostic: 'transport timeout' }), timeoutMs);
11
11
  });
12
12
  try {
13
- return await Promise.race([transport.attempt(request, timeoutMs), timeout]);
13
+ return await Promise.race([transport.attempt(request, timeoutMs, beforeSend), timeout]);
14
14
  }
15
15
  finally {
16
16
  if (timer !== undefined)
@@ -52,6 +52,14 @@ export async function deliverPending(input) {
52
52
  route: { kind: route.kind, address: route.address },
53
53
  updatedAt: route.updatedAt,
54
54
  }));
55
+ const liveRoutes = currentSessionBindings(routes.filter((route) => observation.bindings.some((binding) => nameKey(binding.participant) === nameKey(route.participant)
56
+ && binding.sessionId === route.session
57
+ && binding.location === route.location)));
58
+ let presentations = [];
59
+ try {
60
+ presentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, now: input.now });
61
+ }
62
+ catch { /* capability is handled by the route-level wake checks */ }
55
63
  let attempted = 0;
56
64
  let accepted = 0;
57
65
  let failed = 0;
@@ -64,21 +72,25 @@ export async function deliverPending(input) {
64
72
  if (requested === undefined || requested !== notification.item.index)
65
73
  continue;
66
74
  }
67
- const candidates = routes.filter((route) => nameKey(route.participant) === nameKey(membership.recipient));
68
- if (candidates.length === 0) {
69
- notCapable += 1;
70
- continue;
71
- }
75
+ const candidates = liveRoutes.filter((route) => nameKey(route.participant) === nameKey(membership.recipient));
72
76
  let acceptedForAttention = false;
77
+ let failedForAttention = false;
78
+ let unknownForAttention = false;
79
+ let notCapableForAttention = false;
73
80
  try {
74
- const prior = await input.hostLedger.listWakeAttempts({ attention: { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient }, now: input.now });
81
+ const prior = await input.hostLedger.listWakeAttempts({ attention: { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient }, now: Date.now() });
75
82
  acceptedForAttention = prior.some((attempt) => attempt.outcome === 'accepted');
76
83
  }
77
84
  catch { /* capability is handled by the route-level probe */ }
78
85
  if (acceptedForAttention)
79
86
  continue;
87
+ if (candidates.length === 0)
88
+ notCapableForAttention = true;
80
89
  for (const route of candidates) {
81
90
  const activity = formatActivityId(notification.item.index);
91
+ const presented = presentations.filter((row) => row.activity === activity && row.participant.toLocaleLowerCase() === membership.recipient.toLocaleLowerCase() && row.sessionId === route.session && presentationSuppressesWake([row]));
92
+ if (presented.length > 0)
93
+ continue;
82
94
  const attention = { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient };
83
95
  const leaseMs = input.timeoutMs ?? 5000;
84
96
  const requestRoute = { location: route.location, participant: route.participant, sessionId: route.session, channel: route.channel, kind: route.route.kind, address: { ...route.route.address }, updatedAt: route.updatedAt ?? 0 };
@@ -86,97 +98,174 @@ export async function deliverPending(input) {
86
98
  try {
87
99
  const probe = await input.transport.probe(requestRoute);
88
100
  if (probe === false || (typeof probe === 'object' && probe.outcome === 'not-capable')) {
89
- notCapable += 1;
101
+ notCapableForAttention = true;
90
102
  continue;
91
103
  }
92
104
  }
93
105
  catch {
94
- notCapable += 1;
106
+ notCapableForAttention = true;
95
107
  continue;
96
108
  }
97
109
  }
98
110
  let leaseId = randomUUID();
99
111
  let lease;
100
112
  try {
101
- lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session, at: input.now });
113
+ lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session });
102
114
  }
103
115
  catch {
104
- notCapable += 1;
116
+ notCapableForAttention = true;
105
117
  continue;
106
118
  }
107
119
  if (lease.type === 'ambiguous') {
108
- await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: 'unknown', routeKind: lease.lease.routeKind ?? route.route.kind, attemptN: lease.lease.attemptN ?? 1, signature: 'worker_interrupted_during_dispatch', message: 'The notification worker ended after dispatch began; transport acceptance is unknown.', at: input.now });
109
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId: lease.lease.leaseId, session: route.session, at: input.now });
110
- continue;
120
+ let recovered = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: lease.lease.leaseId });
121
+ if (recovered.status === 'busy' && recovered.record.claimToken !== undefined) {
122
+ await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken: recovered.record.claimToken });
123
+ recovered = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: lease.lease.leaseId });
124
+ }
125
+ if (recovered.status === 'acquired') {
126
+ await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: 'unknown', routeKind: lease.lease.routeKind ?? route.route.kind, attemptN: lease.lease.attemptN ?? 1, signature: 'worker_interrupted_during_dispatch', message: 'The notification worker ended after dispatch began; transport acceptance is unknown.', claimToken: recovered.claimToken });
127
+ }
128
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId: lease.lease.leaseId, session: route.session });
129
+ break;
111
130
  }
131
+ if (lease.type === 'busy')
132
+ break;
112
133
  if (lease.type !== 'acquired')
113
134
  continue;
135
+ try {
136
+ const completed = await input.hostLedger.listWakeAttempts({ attention, now: Date.now() });
137
+ if (completed.some((attempt) => attempt.outcome === 'accepted')) {
138
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
139
+ acceptedForAttention = true;
140
+ break;
141
+ }
142
+ }
143
+ catch { /* continue with the evidence claim */ }
114
144
  let attempts;
115
145
  try {
116
- attempts = await input.hostLedger.listWakeAttempts({ attention, session: route.session, now: input.now });
146
+ attempts = await input.hostLedger.listWakeAttempts({ attention, now: Date.now() });
117
147
  }
118
148
  catch {
119
- notCapable += 1;
120
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now }).catch(() => undefined);
149
+ notCapableForAttention = true;
150
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
121
151
  continue;
122
152
  }
123
- if (attempts.some((attempt) => attempt.routeKind === route.route.kind && attempt.outcome === 'unknown')) {
124
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
125
- continue;
153
+ if (attempts.some((attempt) => attempt.outcome === 'unknown')) {
154
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
155
+ break;
126
156
  }
127
- const attemptN = attempts.reduce((highest, record) => Math.max(highest, record.attemptN ?? 0), 0) + 1;
157
+ const sessionAttempts = attempts.filter((attempt) => attempt.session === route.session);
158
+ const attemptN = sessionAttempts.reduce((highest, record) => Math.max(highest, record.attemptN ?? 0), 0) + 1;
128
159
  const request = { location: input.location, participant: membership.recipient, activity, actor: notification.item.actor, route: requestRoute };
129
160
  let outcome;
130
- const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, now: input.now });
161
+ const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: leaseId });
131
162
  if (claim.status !== 'acquired') {
132
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
163
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
133
164
  if (claim.status === 'degraded')
134
- notCapable += 1;
165
+ notCapableForAttention = true;
135
166
  continue;
136
167
  }
137
- attempted += 1;
138
- const dispatching = await input.hostLedger.transitionWakeDispatch({ attention, leaseId, phase: 'dispatching', leaseMs, routeKind: route.route.kind, attemptN, session: route.session, at: input.now });
168
+ const claimToken = claim.claimToken;
169
+ const dispatching = await input.hostLedger.transitionWakeDispatch({ attention, leaseId, phase: 'dispatching', leaseMs, routeKind: route.route.kind, attemptN, session: route.session });
139
170
  if (!dispatching) {
140
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
171
+ await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken }).catch(() => undefined);
172
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
141
173
  continue;
142
174
  }
175
+ let current;
143
176
  try {
144
- outcome = await attemptWakeWithin(input.transport, request, leaseMs);
177
+ current = await observeSquare({ artifact: input.artifact, hostLedger: input.hostLedger, location: input.location, now: input.now });
178
+ }
179
+ catch {
180
+ current = { ...observation, pending: [], bindings: [] };
181
+ }
182
+ const stillPending = current.pending.some((entry) => nameKey(entry.recipient) === nameKey(membership.recipient)
183
+ && entry.notifications.some((entryNotification) => entryNotification.item.index === notification.item.index));
184
+ const stillBound = current.bindings.some((binding) => nameKey(binding.participant) === nameKey(route.participant)
185
+ && binding.sessionId === route.session
186
+ && binding.location === route.location);
187
+ const stillPublished = (current.state.routes ?? []).some((published) => published.location === route.location
188
+ && nameKey(published.participant) === nameKey(route.participant)
189
+ && published.sessionId === route.session
190
+ && published.kind === route.route.kind
191
+ && JSON.stringify(published.address) === JSON.stringify(route.route.address));
192
+ if (!stillPending || !stillBound || !stillPublished) {
193
+ await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken }).catch(() => undefined);
194
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
195
+ continue;
196
+ }
197
+ let latestPresentations = [];
198
+ try {
199
+ latestPresentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, sessionId: route.session, activity, now: input.now });
200
+ }
201
+ catch { /* capability is handled by the transport path */ }
202
+ if (presentationSuppressesWake(latestPresentations)) {
203
+ await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken }).catch(() => undefined);
204
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
205
+ continue;
206
+ }
207
+ let suppressedDuringSend = false;
208
+ const beforeSend = async () => {
209
+ let finalPresentations = [];
210
+ try {
211
+ finalPresentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, sessionId: route.session, activity, now: Date.now() });
212
+ }
213
+ catch { /* capability is handled by the transport path */ }
214
+ suppressedDuringSend = presentationSuppressesWake(finalPresentations);
215
+ return !suppressedDuringSend;
216
+ };
217
+ try {
218
+ outcome = await attemptWakeWithin(input.transport, request, leaseMs, beforeSend);
145
219
  }
146
220
  catch (error) {
147
221
  outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
148
222
  }
223
+ if (suppressedDuringSend) {
224
+ await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken }).catch(() => undefined);
225
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
226
+ continue;
227
+ }
228
+ attempted += 1;
149
229
  if (outcome.outcome === 'not-capable') {
150
- await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', now: input.now }).catch(() => undefined);
151
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now }).catch(() => undefined);
152
- notCapable += 1;
230
+ await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken }).catch(() => undefined);
231
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
232
+ notCapableForAttention = true;
153
233
  continue;
154
234
  }
155
235
  if (outcome.outcome === 'failed' && outcome.unavailable) {
156
236
  if (outcome.routeStale === true)
157
237
  await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.session });
158
- await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', now: input.now });
159
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
160
- failed += 1;
238
+ await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken });
239
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
240
+ failedForAttention = true;
161
241
  continue;
162
242
  }
163
- await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: outcome.outcome, routeKind: route.route.kind, attemptN: outcome.attemptN ?? attemptN, ...(outcome.outcome === 'accepted' && outcome.signature === undefined ? {} : outcome.outcome === 'accepted' ? { signature: outcome.signature } : outcome.outcome === 'failed' ? { message: outcome.message } : { diagnostic: outcome.diagnostic }), at: input.now });
164
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
243
+ await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: outcome.outcome, routeKind: route.route.kind, attemptN: outcome.attemptN ?? attemptN, ...(outcome.outcome === 'accepted' && outcome.signature === undefined ? {} : outcome.outcome === 'accepted' ? { signature: outcome.signature } : outcome.outcome === 'failed' ? { message: outcome.message } : { diagnostic: outcome.diagnostic }), claimToken });
244
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
165
245
  if (outcome.outcome === 'accepted') {
166
- accepted += 1;
167
246
  acceptedForAttention = true;
168
247
  break;
169
248
  }
170
249
  if (outcome.outcome === 'failed')
171
- failed += 1;
172
- else
173
- unknown += 1;
250
+ failedForAttention = true;
251
+ else {
252
+ unknownForAttention = true;
253
+ break;
254
+ }
174
255
  }
256
+ if (acceptedForAttention)
257
+ accepted += 1;
258
+ else if (unknownForAttention)
259
+ unknown += 1;
260
+ else if (failedForAttention)
261
+ failed += 1;
262
+ else if (notCapableForAttention)
263
+ notCapable += 1;
175
264
  }
176
265
  }
177
266
  return { attempted, accepted, failed, unknown, notCapable };
178
267
  }
179
- export function selectPendingWakeActivities(state, routes, attempts, now, graceMs, limit, delivery = deriveDeliveryModel(state)) {
268
+ export function selectPendingWakeActivities(state, routes, attempts, now, graceMs, limit, delivery = deriveDeliveryModel(state), presentations = []) {
180
269
  const selected = new Set();
181
270
  for (const membership of delivery.joinedRecipients()) {
182
271
  for (const notification of delivery.pendingFor(membership)) {
@@ -187,6 +276,9 @@ export function selectPendingWakeActivities(state, routes, attempts, now, graceM
187
276
  const eligible = routes.some((binding) => {
188
277
  if (binding.route === undefined || nameKey(binding.participant) !== nameKey(membership))
189
278
  return false;
279
+ const presented = presentations.filter((row) => row.activity === formatActivityId(notification.item.index) && row.participant.toLocaleLowerCase() === membership.toLocaleLowerCase() && row.sessionId === binding.session && presentationSuppressesWake([row]));
280
+ if (presented.length > 0)
281
+ return false;
190
282
  const matching = attempts.filter((attempt) => attempt.session === binding.session && nameKey(attempt.attention.recipient) === nameKey(membership) && attempt.attention.actIndex === notification.item.index);
191
283
  return isWakeRouteAttemptable({ kind: binding.route.kind, updatedAt: binding.updatedAt ?? 0 }, matching);
192
284
  });
@@ -201,7 +293,7 @@ export async function sweepPending(input) {
201
293
  return sweepPendingFromState({ ...input, state });
202
294
  }
203
295
  export async function sweepPendingFromState(input) {
204
- const bindings = (input.state.routes ?? []).map((route) => ({ location: input.location, participant: route.participant, session: route.sessionId, channel: route.channel, route: { kind: route.kind, address: route.address }, updatedAt: route.updatedAt }));
296
+ const bindings = currentSessionBindings((input.state.routes ?? []).map((route) => ({ location: input.location, participant: route.participant, session: route.sessionId, channel: route.channel, route: { kind: route.kind, address: route.address }, updatedAt: route.updatedAt })));
205
297
  let records = [];
206
298
  try {
207
299
  records = await input.hostLedger.listWakeAttempts({ now: input.now });
@@ -209,6 +301,13 @@ export async function sweepPendingFromState(input) {
209
301
  catch {
210
302
  records = [];
211
303
  }
304
+ let presentations = [];
305
+ try {
306
+ presentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, now: input.now });
307
+ }
308
+ catch {
309
+ presentations = [];
310
+ }
212
311
  const attempts = records.flatMap((record) => {
213
312
  const index = parseActivityId(record.activity);
214
313
  if (index === undefined || record.routeKind === undefined || typeof record.attemptN !== 'number')
@@ -216,5 +315,5 @@ export async function sweepPendingFromState(input) {
216
315
  return [{ at: record.at ?? input.now, attention: { squarePath: record.location, actIndex: index, recipient: record.participant }, routeKind: record.routeKind, outcome: record.outcome, attemptN: record.attemptN, ...(record.session === undefined ? {} : { session: record.session }) }];
217
316
  });
218
317
  const delivery = input.deriveDelivery?.(input.state) ?? deriveDeliveryModel(input.state);
219
- return selectPendingWakeActivities(input.state, bindings, attempts, input.now, input.graceMs, input.limit, delivery);
318
+ return selectPendingWakeActivities(input.state, bindings, attempts, input.now, input.graceMs, input.limit, delivery, presentations);
220
319
  }
@@ -1,6 +1,10 @@
1
1
  export interface FileLockOptions {
2
2
  retryMs: number;
3
- staleMs: number;
4
3
  signal?: AbortSignal;
4
+ createParent?: boolean;
5
+ }
6
+ export declare class FileLockError extends Error {
7
+ readonly code: "not_a_database";
8
+ constructor(lockPath: string, cause?: unknown);
5
9
  }
6
10
  export declare function withFileLock<T>(lockPath: string, options: FileLockOptions, fn: () => T | Promise<T>): Promise<T>;