@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
@@ -4,7 +4,7 @@ import { sessionInbox } from '../inbox.js';
4
4
  import { sweepPendingNotifications } from '../notifications.js';
5
5
  import { cmdListSquares } from '../list.js';
6
6
  import { parseActivityId, sameName } from '../model.js';
7
- import { commandPrefix, participantCommandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEvent, renderPresenceAnchor, withPathOutput, } from '../presentation.js';
7
+ import { commandPrefix, participantCommandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEvent, renderPresenceAnchor, withPathOutput, quoteShell, } from '../presentation.js';
8
8
  import { actId, nowMs } from '../runtime.js';
9
9
  import { cmdStream, cmdStreamNdjson } from '../stream.js';
10
10
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
@@ -114,20 +114,39 @@ function parseTimestamp(value, flag) {
114
114
  fail(`Invalid ${flag} timestamp: ${value}`);
115
115
  return timestamp;
116
116
  }
117
+ function historyContinuationArgs(argv) {
118
+ const result = [];
119
+ for (let index = 0; index < argv.length; index++) {
120
+ const flag = argv[index];
121
+ if (flag === '--limit' || flag === '--before' || flag === '--after') {
122
+ index += 1;
123
+ continue;
124
+ }
125
+ result.push(flag);
126
+ if (flag === '--from' || flag === '--since' || flag === '--at' || flag === '-B' || flag === '-A' || flag === '-C' || flag === '--mention' || flag === '--grep' || flag === '--fixed' || flag === '--order' || flag === '--format') {
127
+ const value = argv[index + 1];
128
+ if (value !== undefined) {
129
+ result.push(value);
130
+ index += 1;
131
+ }
132
+ }
133
+ }
134
+ return result;
135
+ }
117
136
  function parseHistory(argv, context) {
118
137
  const squarePath = requireSquarePath(context);
119
138
  const viewer = context.name;
120
139
  let lastN = 10;
121
140
  let lastNExplicit = false;
122
- let before;
123
141
  let after;
124
142
  let afterIndex;
143
+ let beforeIndex;
125
144
  const atIndexes = [];
126
145
  let beforeContext;
127
146
  let afterContext;
128
147
  let mention;
129
148
  let pending = false;
130
- let full = false;
149
+ let noTruncate = false;
131
150
  let grep;
132
151
  let fixed;
133
152
  let order;
@@ -148,18 +167,10 @@ function parseHistory(argv, context) {
148
167
  lastNExplicit = true;
149
168
  index += 1;
150
169
  }
151
- else if (flag === '--all') {
152
- lastN = null;
153
- lastNExplicit = true;
154
- }
155
170
  else if (flag === '--from') {
156
171
  participants.push(...parseNameList(requireValue(argv, index, flag), flag));
157
172
  index += 1;
158
173
  }
159
- else if (flag === '--until') {
160
- before = parseTimestamp(requireValue(argv, index, flag), flag);
161
- index += 1;
162
- }
163
174
  else if (flag === '--since') {
164
175
  after = parseTimestamp(requireValue(argv, index, flag), flag);
165
176
  index += 1;
@@ -168,6 +179,10 @@ function parseHistory(argv, context) {
168
179
  afterIndex = parseActRef(requireValue(argv, index, flag), flag);
169
180
  index += 1;
170
181
  }
182
+ else if (flag === '--before') {
183
+ beforeIndex = parseActRef(requireValue(argv, index, flag), flag);
184
+ index += 1;
185
+ }
171
186
  else if (flag === '--at') {
172
187
  const values = requireValue(argv, index, flag).split(',');
173
188
  if (values.some((value) => value === ''))
@@ -189,8 +204,8 @@ function parseHistory(argv, context) {
189
204
  afterContext = context;
190
205
  index += 1;
191
206
  }
192
- else if (flag === '--full')
193
- full = true;
207
+ else if (flag === '--no-truncate')
208
+ noTruncate = true;
194
209
  else if (flag === '--mention') {
195
210
  mention = requireValue(argv, index, flag);
196
211
  index += 1;
@@ -227,29 +242,37 @@ function parseHistory(argv, context) {
227
242
  fail('--grep and --fixed cannot be combined.');
228
243
  if (grep === '' || fixed === '')
229
244
  fail('--grep and --fixed require non-empty text.');
245
+ if (beforeIndex !== undefined && afterIndex !== undefined)
246
+ fail('--before and --after cannot be combined.');
230
247
  if (!lastNExplicit && (atIndexes.length > 0 || pending))
231
248
  lastN = null;
232
249
  return {
233
250
  lastN,
234
251
  participants,
235
- before,
236
252
  after,
237
253
  afterIndex,
254
+ beforeIndex,
238
255
  atIndexes: atIndexes.length === 0 ? undefined : atIndexes,
239
256
  beforeContext,
240
257
  afterContext,
241
258
  mention,
242
259
  pending,
243
260
  viewer,
244
- full,
261
+ noTruncate,
245
262
  grep,
246
263
  fixed,
247
264
  order,
248
265
  format,
249
266
  json,
267
+ continuationArgs: historyContinuationArgs(argv),
250
268
  };
251
269
  }
252
- function renderFields(sayNumbers, item, fields) {
270
+ function historyContinuationCommand(options, squarePath, direction, index) {
271
+ const prefix = options.viewer === undefined ? commandPrefix(squarePath) : participantCommandPrefix(squarePath, options.viewer);
272
+ const args = [...(options.continuationArgs ?? []), direction, actId(index), '--limit', String(options.lastN ?? 10)];
273
+ return `${prefix} history ${args.map((arg) => arg.startsWith('-') || /^act\/\d+$/.test(arg) || /^\d+$/.test(arg) ? arg : quoteShell(arg)).join(' ')}`;
274
+ }
275
+ function renderFields(sayNumbers, item, fields, perception) {
253
276
  return fields.map((field) => {
254
277
  switch (field) {
255
278
  case 'id': return actId(item.index);
@@ -258,14 +281,14 @@ function renderFields(sayNumbers, item, fields) {
258
281
  case 'ts':
259
282
  case 'at': return formatTimestamp(item.at);
260
283
  case 'kind': return item.kind;
261
- case 'body': return 'body' in item && typeof item.body === 'string' ? item.body.replace(/\s+/g, ' ').trim() : '';
284
+ case 'body': return perception === 'presence' ? '' : 'body' in item && typeof item.body === 'string' ? item.body.replace(/\s+/g, ' ').trim() : '';
262
285
  case 'number': return item.kind === 'say' ? String(sayNumbers[item.index]) : '';
263
286
  case 'reply': return item.kind === 'say' && item.reply !== undefined ? actId(item.reply) : '';
264
287
  default: return '';
265
288
  }
266
289
  }).join('\t');
267
290
  }
268
- function jsonLine(sayNumbers, item) {
291
+ function jsonLine(sayNumbers, item, perception) {
269
292
  const act = item;
270
293
  return JSON.stringify({
271
294
  id: actId(item.index),
@@ -274,15 +297,15 @@ function jsonLine(sayNumbers, item) {
274
297
  author: act.actor ?? null,
275
298
  at: act.at,
276
299
  ts: formatTimestamp(act.at),
277
- body: 'body' in act && typeof act.body === 'string' ? act.body : '',
300
+ body: perception === 'presence' ? '' : 'body' in act && typeof act.body === 'string' ? act.body : '',
278
301
  number: act.kind === 'say' ? sayNumbers[act.index] : null,
279
302
  reach: act.kind === 'say' ? act.reach ?? null : null,
280
303
  reply: act.kind === 'say' && act.reply !== undefined ? actId(act.reply) : null,
281
304
  });
282
305
  }
283
- function renderHistoryProjection(projection, visible, full, squarePath, viewer, mode) {
306
+ function renderHistoryProjection(projection, visible, noTruncate, squarePath, viewer, mode) {
284
307
  const shown = visible.filter((activity) => activity.kind === 'say' || activity.kind === 'done');
285
- const preview = full ? undefined : 200;
308
+ const preview = noTruncate ? undefined : 200;
286
309
  const chunks = [];
287
310
  for (const activity of shown) {
288
311
  const options = {
@@ -302,7 +325,7 @@ function renderHistoryProjection(projection, visible, full, squarePath, viewer,
302
325
  if (chunks.length === 0)
303
326
  return 'latest\n ○ no public activity in this view';
304
327
  if (preview !== undefined && shown.some((activity) => activity.kind === 'say' && activity.body.length > preview && (mode === 'archive' || activity.perception === 'full'))) {
305
- chunks.push(`» ${commandPrefix(squarePath)} history --full`);
328
+ chunks.push(`» ${commandPrefix(squarePath)} history --no-truncate`);
306
329
  }
307
330
  return chunks.join('\n\n');
308
331
  }
@@ -312,29 +335,41 @@ export const historyCommand = {
312
335
  const squarePath = requireSquarePath(context);
313
336
  const square = await openSquare(squarePath, { clock: nowMs });
314
337
  try {
315
- const projection = await historyPresentation(square, options);
338
+ // Keep the projection chronological; pagination chooses a stable edge,
339
+ // then --order only changes how the selected page is displayed.
340
+ const projection = await historyPresentation(square, { ...options, order: 'asc' });
316
341
  let events = [...projection.activities];
317
342
  const searching = options.grep !== undefined || options.fixed !== undefined;
318
343
  const totalMatches = searching ? events.length : 0;
319
344
  if (options.lastN != null) {
320
- events = options.order === 'desc'
345
+ events = options.afterIndex !== undefined
321
346
  ? events.slice(0, options.lastN)
322
347
  : events.slice(-options.lastN);
323
348
  }
349
+ if (options.order === 'desc')
350
+ events.reverse();
324
351
  if (options.json)
325
- return events.map((item) => jsonLine(projection.sayNumbers, item)).join('\n') + (events.length > 0 ? '\n' : '');
352
+ return events.map((item) => jsonLine(projection.sayNumbers, item, item.perception)).join('\n') + (events.length > 0 ? '\n' : '');
326
353
  if (options.format !== undefined && options.format.length > 0) {
327
- return events.map((item) => renderFields(projection.sayNumbers, item, options.format)).join('\n') + (events.length > 0 ? '\n' : '');
354
+ return events.map((item) => renderFields(projection.sayNumbers, item, options.format, item.perception)).join('\n') + (events.length > 0 ? '\n' : '');
328
355
  }
329
356
  const pattern = options.grep ?? options.fixed;
330
357
  const anonymous = options.viewer === undefined;
331
- const archive = (options.atIndexes !== undefined && options.atIndexes.length > 0)
332
- || (options.lastN == null && options.full === true)
333
- || anonymous;
358
+ const archive = anonymous;
334
359
  const output = pattern === undefined || pattern === ''
335
- ? renderHistoryProjection(projection, events, options.full === true || anonymous, squarePath, options.viewer ?? '', archive ? 'archive' : 'ambient')
336
- : renderGrepActivitiesView(events, totalMatches, options.full, squarePath, pattern, options.fixed !== undefined);
337
- return withPathOutput(squarePath, output, { participantCount: projection.participantCount });
360
+ ? renderHistoryProjection(projection, events, options.noTruncate === true, squarePath, options.viewer ?? '', archive ? 'archive' : 'ambient')
361
+ : renderGrepActivitiesView(events, totalMatches, options.noTruncate, squarePath, pattern, options.fixed !== undefined, (item) => projection.activities.find((candidate) => candidate.index === item.index)?.perception ?? 'full');
362
+ const publicEvents = events.filter((item) => item.kind === 'say' || item.kind === 'done');
363
+ const allPublic = projection.activities.filter((item) => item.kind === 'say' || item.kind === 'done');
364
+ const pageMin = publicEvents.length === 0 ? undefined : Math.min(...publicEvents.map((item) => item.index));
365
+ const pageMax = publicEvents.length === 0 ? undefined : Math.max(...publicEvents.map((item) => item.index));
366
+ const hasMore = options.lastN != null && publicEvents.length > 0 && (options.afterIndex !== undefined
367
+ ? allPublic.some((item) => item.index > (pageMax ?? options.afterIndex))
368
+ : allPublic.some((item) => item.index < (pageMin ?? Infinity)));
369
+ const cursorDirection = options.afterIndex !== undefined ? '--after' : '--before';
370
+ const cursorIndex = cursorDirection === '--after' ? Math.max(...publicEvents.map((item) => item.index)) : Math.min(...publicEvents.map((item) => item.index));
371
+ const continuation = hasMore ? `\n\n» ${historyContinuationCommand(options, squarePath, cursorDirection, cursorIndex)}` : '';
372
+ return withPathOutput(squarePath, output + continuation, { participantCount: projection.participantCount });
338
373
  }
339
374
  finally {
340
375
  await closeOpenSquare(square);
@@ -428,7 +463,7 @@ export const statusCommand = {
428
463
  : [` ${visible.replace(/\n/g, '\n ')}`];
429
464
  if (visible.includes('more chars') && result.latestAct !== undefined) {
430
465
  const prefix = context.name === undefined ? commandPrefix(squarePath) : participantCommandPrefix(squarePath, context.name);
431
- latest.push(`» ${prefix} history --at ${actId(result.latestAct)} -C 2 --full`);
466
+ latest.push(`» ${prefix} history --at ${actId(result.latestAct)} -C 2 --no-truncate`);
432
467
  }
433
468
  const output = [
434
469
  `${result.activeCount} active · ${result.doneCount} done · cap ${cap} · throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
@@ -1,8 +1,10 @@
1
1
  import { cmdActivity } from '../activity.js';
2
2
  import { formatActivityId, formatHardCap, parseActivityId, } from '../model.js';
3
3
  import { participantCommandPrefix, participantIdentity, renderEventCli, renderAmbientEvent, withPathOutput, } from '../presentation.js';
4
- import { hasAutomaticDeliveryIdentity, localParticipantOwner, recordLocalDone, recordLocalJoin, } from '../registry.js';
5
- import { sweepPendingNotifications, wakeNotifierForSquare } from '../notifications.js';
4
+ import { hasAutomaticDeliveryIdentity, } from '../registry.js';
5
+ import { createHostLedgerPort } from '../host-ledger-file-adapter.js';
6
+ import { projectLocalParticipantBinding, sessionIdsFromEnvironment } from '../square-projections.js';
7
+ import { sweepPendingNotifications } from '../notifications.js';
6
8
  import { nowMs } from '../runtime.js';
7
9
  import { createSquare, openSquare } from '../square-file-adapter.js';
8
10
  import { closeOpenSquare } from '../open-square.js';
@@ -84,13 +86,18 @@ export const joinCommand = {
84
86
  const beforeSquare = await openSquare(squarePath, { clock: nowMs });
85
87
  const before = await entryPresentation(beforeSquare, intent.name, intent.lastN);
86
88
  await closeOpenSquare(beforeSquare);
87
- const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
89
+ const square = await Square.at({ path: squarePath, clock: nowMs });
88
90
  try {
91
+ const reconnect = before.joined
92
+ && await projectLocalParticipantBinding({
93
+ hostLedger: createHostLedgerPort(),
94
+ location: squarePath,
95
+ participant: intent.name,
96
+ sessionIds: sessionIdsFromEnvironment(),
97
+ }) !== undefined;
89
98
  const participant = await square.join(intent.name);
90
99
  const joinedName = participant.name;
91
100
  const isRejoin = before.joined;
92
- const reconnect = isRejoin
93
- && await localParticipantOwner(squarePath, joinedName) !== undefined;
94
101
  if (isRejoin && !intent.kick && !reconnect) {
95
102
  fail([
96
103
  `✕ ${participantIdentity(joinedName)} shoos you out of the square`,
@@ -102,7 +109,7 @@ export const joinCommand = {
102
109
  const afterSquare = await openSquare(squarePath, { clock: nowMs });
103
110
  const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
104
111
  await closeOpenSquare(afterSquare);
105
- await recordLocalJoin(joinedName, squarePath);
112
+ await square.reconcileBinding();
106
113
  await sweepPendingNotifications(squarePath);
107
114
  const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
108
115
  now: nowMs(),
@@ -275,12 +282,12 @@ export const doneCommand = {
275
282
  async execute(intent, context) {
276
283
  const squarePath = requireSquarePath(context);
277
284
  const body = (await resolveBody(intent.body ?? (process.stdin.isTTY ? '' : '-'))).replace(/\r\n/g, '\n').trim();
278
- const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
285
+ const square = await Square.at({ path: squarePath, clock: nowMs });
279
286
  const participant = await square.join(intent.name);
280
287
  const result = await participant.done(body);
288
+ await square.reconcileBinding();
281
289
  await square.close();
282
290
  const name = result.activity.actor;
283
- await recordLocalDone(name, squarePath);
284
291
  const presentation = await openSquare(squarePath, { clock: nowMs });
285
292
  const participantCount = (await entryPresentation(presentation, name).finally(() => closeOpenSquare(presentation))).participantCount;
286
293
  return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · just now`, { participantCount });
@@ -296,7 +303,7 @@ export const holdCommand = {
296
303
  parse: parseHold,
297
304
  async execute(intent, context) {
298
305
  const squarePath = requireSquarePath(context);
299
- const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
306
+ const square = await Square.at({ path: squarePath, clock: nowMs });
300
307
  try {
301
308
  const participant = await square.join(intent.name);
302
309
  const result = await participant.hold((await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim());
@@ -320,7 +327,7 @@ export const resumeCommand = {
320
327
  },
321
328
  async execute(intent, context) {
322
329
  const squarePath = requireSquarePath(context);
323
- const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
330
+ const square = await Square.at({ path: squarePath, clock: nowMs });
324
331
  try {
325
332
  const participant = await square.join(intent.name);
326
333
  const result = await participant.resume();
@@ -5,6 +5,6 @@ export interface CodexHookInput {
5
5
  cwd?: unknown;
6
6
  source?: unknown;
7
7
  }
8
- export declare function codexHookResponse(input: CodexHookInput, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
8
+ export declare function codexHookResponse(input: CodexHookInput, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
9
9
  export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
10
10
  export declare function runCodexHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
package/dist/decisions.js CHANGED
@@ -274,11 +274,11 @@ export function coreActivities(squareState, opts, suppliedDelivery) {
274
274
  }
275
275
  if (opts.afterIndex != null)
276
276
  acts = acts.filter((act) => act.index > opts.afterIndex);
277
+ if (opts.beforeIndex != null)
278
+ acts = acts.filter((act) => act.index < opts.beforeIndex);
277
279
  if (canonicalParticipants.length > 0) {
278
280
  acts = acts.filter((act) => act.actor !== undefined && canonicalParticipants.some((participant) => sameName(participant, act.actor)));
279
281
  }
280
- if (opts.before != null)
281
- acts = acts.filter((act) => act.at < opts.before);
282
282
  if (opts.after != null)
283
283
  acts = acts.filter((act) => act.at > opts.after);
284
284
  if (opts.mention != null) {
@@ -1,5 +1,5 @@
1
1
  import { type DirectedNotificationRoute } from './delivery.js';
2
- import type { WakeAttempt } from './wake-attempts.js';
2
+ import type { WakeAttempt } from './square-projections.js';
3
3
  export type DeliveryHealthKind = 'awaiting' | 'wake-accepted' | 'wake-unknown' | 'presented-not-delivered' | 'unreachable';
4
4
  export interface DeliveryHealthItem {
5
5
  squarePath: string;
@@ -0,0 +1,25 @@
1
+ import { type SquareState } from './model.js';
2
+ import type { HostLedgerPort, PresenceRecord, SquareArtifactPort, DeliverPendingInput, DeliveryResult, ObserveSquareInput, ReconcileBindingInput, SquareObservation } from './ports.js';
3
+ import { deriveDeliveryModel } from './delivery.js';
4
+ import { type WakeAttempt } from './square-projections.js';
5
+ export declare function observeSquare(input: ObserveSquareInput): Promise<SquareObservation>;
6
+ export declare function reconcileBinding(input: ReconcileBindingInput): Promise<import("./host-ledger.js").ReconcileBindingResult>;
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[];
9
+ export declare function sweepPending(input: {
10
+ readonly artifact: SquareArtifactPort;
11
+ readonly hostLedger: HostLedgerPort;
12
+ readonly location: string;
13
+ readonly now: number;
14
+ readonly graceMs: number;
15
+ readonly limit: number;
16
+ }): Promise<number[]>;
17
+ export declare function sweepPendingFromState(input: {
18
+ readonly state: SquareState;
19
+ readonly hostLedger: HostLedgerPort;
20
+ readonly location: string;
21
+ readonly now: number;
22
+ readonly graceMs: number;
23
+ readonly limit: number;
24
+ readonly deriveDelivery?: (snapshot: SquareState) => ReturnType<typeof deriveDeliveryModel>;
25
+ }): Promise<number[]>;
@@ -0,0 +1,124 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { formatActivityId, parseActivityId } from './square-core.js';
3
+ import { deriveDeliveryModel } from './delivery.js';
4
+ import { isWakeRouteAttemptable } from './square-projections.js';
5
+ export async function observeSquare(input) {
6
+ const snapshot = await input.artifact.read();
7
+ const delivery = deriveDeliveryModel(snapshot.state);
8
+ const rows = input.hostLedger === undefined ? [] : await input.hostLedger.listPresence({ location: input.location, scopes: ['user', 'local'], now: input.now });
9
+ const bindings = rows.map((record) => ({
10
+ location: record.location,
11
+ participant: record.participant,
12
+ sessionId: record.session,
13
+ channel: record.channel,
14
+ ...(record.route === undefined ? {} : { route: { location: record.location, participant: record.participant, sessionId: record.session, channel: record.channel, kind: record.route.kind, address: { ...record.route.address }, updatedAt: record.updatedAt ?? 0 } }),
15
+ updatedAt: record.updatedAt ?? 0,
16
+ }));
17
+ return { ...(input.location === undefined ? {} : { location: input.location }), version: snapshot.version, state: snapshot.state, pending: delivery.joinedRecipients().map((recipient) => ({ recipient, notifications: delivery.pendingFor(recipient) })), bindings };
18
+ }
19
+ export async function reconcileBinding(input) {
20
+ return input.hostLedger.reconcileBinding({ artifact: input.artifact, scopes: input.scopes, now: input.now });
21
+ }
22
+ export async function deliverPending(input) {
23
+ const observation = await observeSquare({ artifact: input.artifact, hostLedger: input.hostLedger, location: input.location, now: input.now });
24
+ const routes = (await input.hostLedger.listPresence({ location: input.location, scopes: ['user'], now: input.now })).filter((binding) => binding.route !== undefined);
25
+ let attempted = 0;
26
+ let accepted = 0;
27
+ let failed = 0;
28
+ let unknown = 0;
29
+ for (const membership of observation.pending) {
30
+ for (const notification of membership.notifications) {
31
+ const candidates = routes.filter((route) => route.participant.toLocaleLowerCase() === membership.recipient.toLocaleLowerCase());
32
+ if (input.activity !== undefined) {
33
+ const requested = typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
34
+ if (requested === undefined || requested !== notification.item.index)
35
+ continue;
36
+ }
37
+ for (const route of candidates) {
38
+ const activity = formatActivityId(notification.item.index);
39
+ const attention = { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient };
40
+ const leaseMs = input.timeoutMs ?? 5000;
41
+ let leaseId = randomUUID();
42
+ let lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session, at: input.now });
43
+ if (lease.type === 'ambiguous') {
44
+ 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 });
45
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId: lease.lease.leaseId, session: route.session, at: input.now });
46
+ leaseId = randomUUID();
47
+ lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session, at: input.now });
48
+ }
49
+ if (lease.type !== 'acquired')
50
+ continue;
51
+ const attempts = await input.hostLedger.listWakeAttempts({ attention, session: route.session, now: input.now });
52
+ const attemptN = attempts.reduce((highest, record) => Math.max(highest, record.attemptN ?? 0), 0) + 1;
53
+ const request = { location: input.location, participant: membership.recipient, activity, route: { location: route.location, participant: route.participant, sessionId: route.session, channel: route.channel, kind: route.route.kind, address: { ...route.route.address }, updatedAt: route.updatedAt ?? 0 } };
54
+ let outcome;
55
+ const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, now: input.now });
56
+ if (claim.status !== 'acquired') {
57
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
58
+ continue;
59
+ }
60
+ attempted += 1;
61
+ const dispatching = await input.hostLedger.transitionWakeDispatch({ attention, leaseId, phase: 'dispatching', leaseMs, routeKind: route.route.kind, attemptN, session: route.session, at: input.now });
62
+ if (!dispatching) {
63
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
64
+ continue;
65
+ }
66
+ try {
67
+ outcome = await Promise.race([input.transport.attempt(request, leaseMs), new Promise((resolve) => setTimeout(() => resolve({ outcome: 'unknown', diagnostic: 'transport timeout' }), leaseMs))]);
68
+ }
69
+ catch (error) {
70
+ outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
71
+ }
72
+ if (outcome.outcome === 'failed' && outcome.unavailable) {
73
+ await input.transport.invalidate?.(request);
74
+ await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', now: input.now });
75
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
76
+ failed += 1;
77
+ continue;
78
+ }
79
+ 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 });
80
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
81
+ if (outcome.outcome === 'accepted')
82
+ accepted += 1;
83
+ else if (outcome.outcome === 'failed')
84
+ failed += 1;
85
+ else
86
+ unknown += 1;
87
+ }
88
+ }
89
+ }
90
+ return { attempted, accepted, failed, unknown };
91
+ }
92
+ export function selectPendingWakeActivities(state, routes, attempts, now, graceMs, limit, delivery = deriveDeliveryModel(state)) {
93
+ const selected = new Set();
94
+ for (const membership of delivery.joinedRecipients()) {
95
+ for (const notification of delivery.pendingFor(membership)) {
96
+ if (now - notification.item.at <= graceMs)
97
+ continue;
98
+ const eligible = routes.some((binding) => {
99
+ if (binding.route === undefined || binding.participant.toLocaleLowerCase() !== membership.toLocaleLowerCase())
100
+ return false;
101
+ const matching = attempts.filter((attempt) => attempt.session === binding.session && attempt.attention.squarePath === binding.location && attempt.attention.recipient.toLocaleLowerCase() === membership.toLocaleLowerCase() && attempt.attention.actIndex === notification.item.index);
102
+ return isWakeRouteAttemptable({ kind: binding.route.kind, updatedAt: binding.updatedAt ?? 0 }, matching);
103
+ });
104
+ if (eligible)
105
+ selected.add(notification.item.index);
106
+ }
107
+ }
108
+ return [...selected].sort((left, right) => left - right).slice(0, Math.max(0, limit));
109
+ }
110
+ export async function sweepPending(input) {
111
+ const { state } = await input.artifact.read();
112
+ return sweepPendingFromState({ ...input, state });
113
+ }
114
+ export async function sweepPendingFromState(input) {
115
+ const [bindings, records] = await Promise.all([input.hostLedger.listPresence({ location: input.location, scopes: ['user'], now: input.now }), input.hostLedger.listWakeAttempts({ now: input.now })]);
116
+ const attempts = records.flatMap((record) => {
117
+ const index = parseActivityId(record.activity);
118
+ if (index === undefined || record.routeKind === undefined || typeof record.attemptN !== 'number')
119
+ return [];
120
+ 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 }) }];
121
+ });
122
+ const delivery = input.deriveDelivery?.(input.state) ?? deriveDeliveryModel(input.state);
123
+ return selectPendingWakeActivities(input.state, bindings, attempts, input.now, input.graceMs, input.limit, delivery);
124
+ }
package/dist/help.js CHANGED
@@ -43,7 +43,7 @@ const COMMANDS = [
43
43
  {
44
44
  names: ['history'], usage: '[--as <name>] history [filters] [output]', usesSquare: true, group: 'participant',
45
45
  summary: 'Read or search the archive without changing what you have caught.',
46
- details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time>, --until <time> Match a time window.', ' --grep <regex> | --fixed <s> Search activity ids, participants, and original bodies.', ' --mention <name> Match direct attention for a participant.', ' --pending Match attention waiting for --as <name>.', ' --at <ids> Center on comma-separated activity ids; may repeat.', ' -B, -A, -C <N> Set non-negative context around every --at coordinate.', ' --after <id> Match activities after an id.', '', 'Results:', ' --limit <N> | --all Bound the newest matches (default 10).', ' --order <asc|desc> Set display order (default oldest first).', '', 'Output:', ' --full --json --format <fields>', ' Without --as, the default view shows complete activity bodies.'],
46
+ details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time> Match activities after a time.', ' --grep <regex> | --fixed <s> Search activity ids, participants, and original bodies.', ' --mention <name> Match direct attention for a participant.', ' --pending Match attention waiting for --as <name>.', ' --at <ids> Center on comma-separated activity ids; may repeat.', ' -B, -A, -C <N> Set non-negative context around every --at coordinate.', ' --before <id> Read the page immediately before an activity.', ' --after <id> Read the page immediately after an activity.', '', 'Results:', ' --limit <N> Page size (default 10).', ' --order <asc|desc> Set display order (default oldest first).', '', 'Output:', ' --no-truncate --json --format <fields>', ' Bodies are previews by default; --no-truncate expands them. --as keeps participant perception.'],
47
47
  },
48
48
  { names: ['status'], usage: '[--as <name>] status', usesSquare: true, group: 'participant', summary: 'Show who is present and what happened most recently.' },
49
49
  { names: ['participants'], usage: 'participants', usesSquare: true, group: 'host', summary: 'Show the full participant roster and current states.' },
@@ -0,0 +1,34 @@
1
+ import type { HostLedgerPort, HostLedgerScope, PresenceRecord, PresenceKey, PresenceLookup, PresenceResult, EvidenceRecord, EvidenceClaim, EvidenceRelease, EvidenceLookup, EvidenceGc, ClaimResult, ReconcileBindingInput, ReconcileBindingResult, WakeDispatchClaim, WakeDispatchClaimInput, WakeDispatchReleaseInput, WakeDispatchTransitionInput, WakeAttemptLookup } from './host-ledger.js';
2
+ export interface HostLedgerFileAdapterOptions {
3
+ userPath?: string;
4
+ localPath?: string;
5
+ writableScope?: HostLedgerScope;
6
+ readableScopes?: readonly HostLedgerScope[];
7
+ claimsPath?: string;
8
+ now?: () => number;
9
+ }
10
+ export declare class FileHostLedgerPort implements HostLedgerPort {
11
+ private readonly user;
12
+ private readonly local;
13
+ private readonly writable;
14
+ private readonly readable;
15
+ private readonly claims;
16
+ private readonly clock;
17
+ constructor(o?: HostLedgerFileAdapterOptions);
18
+ private file;
19
+ ensurePresence(i: PresenceRecord, scope?: HostLedgerScope): Promise<PresenceResult>;
20
+ removePresence(i: PresenceKey): Promise<void>;
21
+ listPresence(i?: PresenceLookup): Promise<readonly PresenceRecord[]>;
22
+ claimWakeDispatch(i: WakeDispatchClaimInput): Promise<WakeDispatchClaim>;
23
+ transitionWakeDispatch(i: WakeDispatchTransitionInput): Promise<boolean>;
24
+ releaseWakeDispatch(i: WakeDispatchReleaseInput): Promise<void>;
25
+ listWakeAttempts(i?: WakeAttemptLookup): Promise<readonly EvidenceRecord[]>;
26
+ appendWakeAttempt(i: EvidenceRecord): Promise<void>;
27
+ claimEvidence(i: EvidenceClaim): Promise<ClaimResult>;
28
+ releaseEvidence(i: EvidenceRelease): Promise<void>;
29
+ appendEvidence(i: EvidenceRecord): Promise<void>;
30
+ listEvidence(i?: EvidenceLookup): Promise<readonly EvidenceRecord[]>;
31
+ gcEvidence(i: EvidenceGc): Promise<void>;
32
+ reconcileBinding(i?: ReconcileBindingInput): Promise<ReconcileBindingResult>;
33
+ }
34
+ export declare function createHostLedgerPort(o?: HostLedgerFileAdapterOptions): HostLedgerPort;