@astrosheep/square 0.3.30 → 0.3.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/hooks/hooks.json +0 -3
  3. package/claude-plugin/skills/square/SKILL.md +5 -4
  4. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  5. package/dist/activity.js +8 -3
  6. package/dist/artifact.js +10 -51
  7. package/dist/automatic-session.js +49 -14
  8. package/dist/boundary-presentation.d.ts +1 -1
  9. package/dist/boundary-presentation.js +58 -14
  10. package/dist/catch-decisions.d.ts +17 -0
  11. package/dist/catch-decisions.js +53 -0
  12. package/dist/claude-hook.d.ts +2 -1
  13. package/dist/claude-hook.js +5 -2
  14. package/dist/cli/context.js +10 -2
  15. package/dist/cli/observation-commands.d.ts +5 -1
  16. package/dist/cli/observation-commands.js +68 -33
  17. package/dist/cli/square-commands.js +18 -12
  18. package/dist/codex-hook.d.ts +2 -1
  19. package/dist/codex-hook.js +5 -2
  20. package/dist/codex-queue.js +1 -1
  21. package/dist/decisions.js +2 -2
  22. package/dist/delivery-health.d.ts +1 -1
  23. package/dist/delivery-operations.d.ts +25 -0
  24. package/dist/delivery-operations.js +207 -0
  25. package/dist/delivery.d.ts +1 -0
  26. package/dist/help.js +1 -1
  27. package/dist/host-ledger-file-adapter.d.ts +34 -0
  28. package/dist/host-ledger-file-adapter.js +156 -0
  29. package/dist/host-ledger.d.ts +160 -0
  30. package/dist/host-ledger.js +1 -0
  31. package/dist/inbox.d.ts +2 -2
  32. package/dist/inbox.js +22 -14
  33. package/dist/index.d.ts +4 -1
  34. package/dist/index.js +1 -0
  35. package/dist/landing.d.ts +18 -14
  36. package/dist/landing.js +29 -113
  37. package/dist/model.d.ts +9 -17
  38. package/dist/model.js +2 -2
  39. package/dist/notifications.d.ts +12 -10
  40. package/dist/notifications.js +122 -190
  41. package/dist/open-square.d.ts +5 -4
  42. package/dist/open-square.js +1 -1
  43. package/dist/paseo-delivery.js +2 -0
  44. package/dist/ports.d.ts +138 -0
  45. package/dist/ports.js +1 -0
  46. package/dist/presence.d.ts +1 -2
  47. package/dist/presence.js +13 -46
  48. package/dist/presentation-operations.d.ts +3 -0
  49. package/dist/presentation-operations.js +51 -0
  50. package/dist/presentation.d.ts +2 -2
  51. package/dist/presentation.js +11 -7
  52. package/dist/presented.d.ts +1 -12
  53. package/dist/presented.js +6 -75
  54. package/dist/registry.d.ts +10 -10
  55. package/dist/registry.js +47 -113
  56. package/dist/routes.d.ts +27 -30
  57. package/dist/routes.js +88 -151
  58. package/dist/runtime.d.ts +1 -1
  59. package/dist/runtime.js +3 -4
  60. package/dist/square-actions.d.ts +33 -0
  61. package/dist/square-actions.js +204 -0
  62. package/dist/square-facade.d.ts +10 -7
  63. package/dist/square-file-adapter.d.ts +5 -3
  64. package/dist/square-file-adapter.js +23 -5
  65. package/dist/square-projections.d.ts +68 -0
  66. package/dist/square-projections.js +92 -0
  67. package/dist/square-storage.d.ts +2 -2
  68. package/dist/square-storage.js +2 -2
  69. package/dist/square-wiring.d.ts +3 -3
  70. package/dist/square-wiring.js +44 -29
  71. package/dist/views.d.ts +8 -2
  72. package/dist/views.js +28 -24
  73. package/dist/wake-attempts.d.ts +29 -22
  74. package/dist/wake-attempts.js +36 -119
  75. package/dist/wake-evidence.d.ts +6 -18
  76. package/dist/wake-evidence.js +17 -80
  77. package/dist/wake-port.js +1 -1
  78. package/dist/wakes.d.ts +2 -16
  79. package/dist/wakes.js +7 -27
  80. package/dist/watch.js +2 -3
  81. package/extensions/square-pi.js +7 -2
  82. package/package.json +1 -1
  83. package/skills/brainstorm/SKILL.md +2 -2
  84. package/skills/square/SKILL.md +5 -4
@@ -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,13 +1,15 @@
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';
6
7
  import { nowMs } from '../runtime.js';
7
8
  import { createSquare, openSquare } from '../square-file-adapter.js';
8
9
  import { closeOpenSquare } from '../open-square.js';
9
10
  import { openParticipant, Square } from '../square-wiring.js';
10
11
  import { entryPresentation, eventPresentation } from '../views.js';
12
+ import { createDefaultWakeTransport } from '../notifications.js';
11
13
  import { fail, parseHardCap, parsePositiveInteger, readStdin, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
12
14
  function parseBuild(argv) {
13
15
  const options = { force: false, hardCap: null };
@@ -84,13 +86,19 @@ 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 hostLedger = createHostLedgerPort();
90
+ const square = await Square.at({ path: squarePath, clock: nowMs, hostLedger, wakeTransport: await createDefaultWakeTransport(hostLedger, nowMs) });
88
91
  try {
92
+ const reconnect = before.joined
93
+ && await projectLocalParticipantBinding({
94
+ hostLedger: createHostLedgerPort(),
95
+ location: squarePath,
96
+ participant: intent.name,
97
+ sessionIds: sessionIdsFromEnvironment(),
98
+ }) !== undefined;
89
99
  const participant = await square.join(intent.name);
90
100
  const joinedName = participant.name;
91
101
  const isRejoin = before.joined;
92
- const reconnect = isRejoin
93
- && await localParticipantOwner(squarePath, joinedName) !== undefined;
94
102
  if (isRejoin && !intent.kick && !reconnect) {
95
103
  fail([
96
104
  `✕ ${participantIdentity(joinedName)} shoos you out of the square`,
@@ -102,8 +110,7 @@ export const joinCommand = {
102
110
  const afterSquare = await openSquare(squarePath, { clock: nowMs });
103
111
  const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
104
112
  await closeOpenSquare(afterSquare);
105
- await recordLocalJoin(joinedName, squarePath);
106
- await sweepPendingNotifications(squarePath);
113
+ await square.reconcileBinding().catch(() => undefined);
107
114
  const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
108
115
  now: nowMs(),
109
116
  preview: intent.lastN === null ? undefined : 200,
@@ -175,7 +182,6 @@ export const expressCommand = {
175
182
  parse: parseActivity,
176
183
  async execute(intent, context) {
177
184
  const squarePath = requireSquarePath(context);
178
- await sweepPendingNotifications(squarePath);
179
185
  const body = await resolveBody(intent.activity);
180
186
  const reachArg = intent.reach === 'bell' ? ' --bell' : '';
181
187
  await cmdActivity(squarePath, intent.name, body, (value) => value, {
@@ -275,12 +281,12 @@ export const doneCommand = {
275
281
  async execute(intent, context) {
276
282
  const squarePath = requireSquarePath(context);
277
283
  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) });
284
+ const square = await Square.at({ path: squarePath, clock: nowMs });
279
285
  const participant = await square.join(intent.name);
280
286
  const result = await participant.done(body);
287
+ await square.reconcileBinding();
281
288
  await square.close();
282
289
  const name = result.activity.actor;
283
- await recordLocalDone(name, squarePath);
284
290
  const presentation = await openSquare(squarePath, { clock: nowMs });
285
291
  const participantCount = (await entryPresentation(presentation, name).finally(() => closeOpenSquare(presentation))).participantCount;
286
292
  return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · just now`, { participantCount });
@@ -296,7 +302,7 @@ export const holdCommand = {
296
302
  parse: parseHold,
297
303
  async execute(intent, context) {
298
304
  const squarePath = requireSquarePath(context);
299
- const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
305
+ const square = await Square.at({ path: squarePath, clock: nowMs });
300
306
  try {
301
307
  const participant = await square.join(intent.name);
302
308
  const result = await participant.hold((await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim());
@@ -320,7 +326,7 @@ export const resumeCommand = {
320
326
  },
321
327
  async execute(intent, context) {
322
328
  const squarePath = requireSquarePath(context);
323
- const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
329
+ const square = await Square.at({ path: squarePath, clock: nowMs });
324
330
  try {
325
331
  const participant = await square.join(intent.name);
326
332
  const result = await participant.resume();
@@ -1,10 +1,11 @@
1
1
  import type { InboxMembership } from './model.js';
2
+ import type { WakeAdapter } from './delivery.js';
2
3
  export interface CodexHookInput {
3
4
  session_id?: unknown;
4
5
  hook_event_name?: unknown;
5
6
  cwd?: unknown;
6
7
  source?: unknown;
7
8
  }
8
- export declare function codexHookResponse(input: CodexHookInput, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
9
+ export declare function codexHookResponse(input: CodexHookInput, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, deliveryAdapters?: WakeAdapter[]): Promise<object | undefined>;
9
10
  export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
10
11
  export declare function runCodexHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
@@ -2,11 +2,12 @@ import { presentPendingAtBoundary } from './boundary-presentation.js';
2
2
  import { sessionInbox } from './inbox.js';
3
3
  import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
4
4
  import { clearCodexBoundary, recordCodexBoundary } from './codex-boundary-state.js';
5
+ import { sweepPrivilegedPending } from './notifications.js';
5
6
  const CODEX_HOOK_EVENTS = {
6
7
  PostToolUse: 'PostToolUse',
7
8
  Stop: 'Stop',
8
9
  };
9
- export async function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
10
+ export async function codexHookResponse(input, lookup = sessionInbox, env = process.env, deliveryAdapters) {
10
11
  if (typeof input.session_id !== 'string' || input.session_id === '')
11
12
  return undefined;
12
13
  if (typeof input.hook_event_name !== 'string')
@@ -15,9 +16,11 @@ export async function codexHookResponse(input, lookup = sessionInbox, env = proc
15
16
  if (hookEventName === undefined)
16
17
  return undefined;
17
18
  await recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env);
18
- return presentPendingAtBoundary(input.session_id, (context) => hookEventName === 'Stop'
19
+ const response = await presentPendingAtBoundary(input.session_id, (context) => hookEventName === 'Stop'
19
20
  ? { systemMessage: context }
20
21
  : { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }, lookup, env);
22
+ await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters).catch(() => undefined);
23
+ return response;
21
24
  }
22
25
  export async function runCodexHook(inputText, env = process.env) {
23
26
  let input;
@@ -36,7 +36,7 @@ export class CodexQueueAdapter {
36
36
  async dispatch(address, payload, beforeSend) {
37
37
  const threadId = address.threadId?.trim();
38
38
  if (!threadId) {
39
- return { outcome: 'unavailable', signature: 'invalid_address', message: 'Codex route has no thread id.' };
39
+ return { outcome: 'unavailable', signature: 'invalid_address', message: 'Codex route has no thread id.', routeStale: true };
40
40
  }
41
41
  const env = this.opts.env ?? process.env;
42
42
  if (!await codexQueueEligible(threadId, env)) {
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,207 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { formatActivityId, parseActivityId } from './square-core.js';
3
+ import { nameKey } from './model.js';
4
+ import { deriveDeliveryModel } from './delivery.js';
5
+ import { isWakeRouteAttemptable } from './square-projections.js';
6
+ import { retireWakeRouteFromArtifact } from './routes.js';
7
+ export async function observeSquare(input) {
8
+ const snapshot = await input.artifact.read();
9
+ const delivery = deriveDeliveryModel(snapshot.state);
10
+ let rows = [];
11
+ if (input.hostLedger !== undefined) {
12
+ try {
13
+ rows = await input.hostLedger.listPresence({ location: input.location, scopes: ['user', 'local'], now: input.now });
14
+ }
15
+ catch {
16
+ rows = [];
17
+ }
18
+ }
19
+ const bindings = rows.map((record) => ({
20
+ location: record.location,
21
+ participant: record.participant,
22
+ sessionId: record.session,
23
+ channel: record.channel,
24
+ ...(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 } }),
25
+ updatedAt: record.updatedAt ?? 0,
26
+ }));
27
+ return { ...(input.location === undefined ? {} : { location: input.location }), version: snapshot.version, state: snapshot.state, pending: delivery.joinedRecipients().map((recipient) => ({ recipient, notifications: delivery.pendingFor(recipient) })), bindings };
28
+ }
29
+ export async function reconcileBinding(input) {
30
+ return input.hostLedger.reconcileBinding({ artifact: input.artifact, scopes: input.scopes, now: input.now });
31
+ }
32
+ export async function deliverPending(input) {
33
+ const observation = await observeSquare({ artifact: input.artifact, hostLedger: input.hostLedger, location: input.location, now: input.now });
34
+ const routes = (observation.state.routes ?? []).map((route) => ({
35
+ location: route.location,
36
+ participant: route.participant,
37
+ session: route.sessionId,
38
+ channel: route.channel,
39
+ route: { kind: route.kind, address: route.address },
40
+ updatedAt: route.updatedAt,
41
+ }));
42
+ let attempted = 0;
43
+ let accepted = 0;
44
+ let failed = 0;
45
+ let unknown = 0;
46
+ let notCapable = 0;
47
+ for (const membership of observation.pending) {
48
+ for (const notification of membership.notifications) {
49
+ const candidates = routes.filter((route) => nameKey(route.participant) === nameKey(membership.recipient));
50
+ if (candidates.length === 0) {
51
+ notCapable += 1;
52
+ continue;
53
+ }
54
+ if (input.activity !== undefined) {
55
+ const requested = typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
56
+ if (requested === undefined || requested !== notification.item.index)
57
+ continue;
58
+ }
59
+ let acceptedForAttention = false;
60
+ try {
61
+ const prior = await input.hostLedger.listWakeAttempts({ attention: { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient }, now: input.now });
62
+ acceptedForAttention = prior.some((attempt) => attempt.outcome === 'accepted');
63
+ }
64
+ catch { /* capability is handled by the route-level probe */ }
65
+ if (acceptedForAttention)
66
+ continue;
67
+ for (const route of candidates) {
68
+ const activity = formatActivityId(notification.item.index);
69
+ const attention = { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient };
70
+ const leaseMs = input.timeoutMs ?? 5000;
71
+ 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 };
72
+ if (input.transport.probe !== undefined) {
73
+ try {
74
+ const probe = await input.transport.probe(requestRoute);
75
+ if (probe === false || (typeof probe === 'object' && probe.outcome === 'not-capable')) {
76
+ notCapable += 1;
77
+ continue;
78
+ }
79
+ }
80
+ catch {
81
+ notCapable += 1;
82
+ continue;
83
+ }
84
+ }
85
+ let leaseId = randomUUID();
86
+ let lease;
87
+ try {
88
+ lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session, at: input.now });
89
+ }
90
+ catch {
91
+ notCapable += 1;
92
+ continue;
93
+ }
94
+ if (lease.type === 'ambiguous') {
95
+ 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 });
96
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId: lease.lease.leaseId, session: route.session, at: input.now });
97
+ continue;
98
+ }
99
+ if (lease.type !== 'acquired')
100
+ continue;
101
+ let attempts;
102
+ try {
103
+ attempts = await input.hostLedger.listWakeAttempts({ attention, session: route.session, now: input.now });
104
+ }
105
+ catch {
106
+ notCapable += 1;
107
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now }).catch(() => undefined);
108
+ continue;
109
+ }
110
+ if (attempts.some((attempt) => attempt.routeKind === route.route.kind && attempt.outcome === 'unknown')) {
111
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
112
+ continue;
113
+ }
114
+ const attemptN = attempts.reduce((highest, record) => Math.max(highest, record.attemptN ?? 0), 0) + 1;
115
+ const request = { location: input.location, participant: membership.recipient, activity, route: requestRoute };
116
+ let outcome;
117
+ const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, now: input.now });
118
+ if (claim.status !== 'acquired') {
119
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
120
+ if (claim.status === 'degraded')
121
+ notCapable += 1;
122
+ continue;
123
+ }
124
+ attempted += 1;
125
+ const dispatching = await input.hostLedger.transitionWakeDispatch({ attention, leaseId, phase: 'dispatching', leaseMs, routeKind: route.route.kind, attemptN, session: route.session, at: input.now });
126
+ if (!dispatching) {
127
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
128
+ continue;
129
+ }
130
+ try {
131
+ outcome = await Promise.race([input.transport.attempt(request, leaseMs), new Promise((resolve) => setTimeout(() => resolve({ outcome: 'unknown', diagnostic: 'transport timeout' }), leaseMs))]);
132
+ }
133
+ catch (error) {
134
+ outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
135
+ }
136
+ if (outcome.outcome === 'not-capable') {
137
+ await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', now: input.now }).catch(() => undefined);
138
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now }).catch(() => undefined);
139
+ notCapable += 1;
140
+ continue;
141
+ }
142
+ if (outcome.outcome === 'failed' && outcome.unavailable) {
143
+ if (outcome.routeStale === true)
144
+ await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.session });
145
+ await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', now: input.now });
146
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
147
+ failed += 1;
148
+ continue;
149
+ }
150
+ 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 });
151
+ await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
152
+ if (outcome.outcome === 'accepted') {
153
+ accepted += 1;
154
+ acceptedForAttention = true;
155
+ break;
156
+ }
157
+ if (outcome.outcome === 'failed')
158
+ failed += 1;
159
+ else
160
+ unknown += 1;
161
+ }
162
+ }
163
+ }
164
+ return { attempted, accepted, failed, unknown, notCapable };
165
+ }
166
+ export function selectPendingWakeActivities(state, routes, attempts, now, graceMs, limit, delivery = deriveDeliveryModel(state)) {
167
+ const selected = new Set();
168
+ for (const membership of delivery.joinedRecipients()) {
169
+ for (const notification of delivery.pendingFor(membership)) {
170
+ if (now - notification.item.at <= graceMs)
171
+ continue;
172
+ if (attempts.some((attempt) => attempt.attention.actIndex === notification.item.index && nameKey(attempt.attention.recipient) === nameKey(membership) && attempt.outcome === 'accepted'))
173
+ continue;
174
+ const eligible = routes.some((binding) => {
175
+ if (binding.route === undefined || nameKey(binding.participant) !== nameKey(membership))
176
+ return false;
177
+ const matching = attempts.filter((attempt) => attempt.session === binding.session && nameKey(attempt.attention.recipient) === nameKey(membership) && attempt.attention.actIndex === notification.item.index);
178
+ return isWakeRouteAttemptable({ kind: binding.route.kind, updatedAt: binding.updatedAt ?? 0 }, matching);
179
+ });
180
+ if (eligible)
181
+ selected.add(notification.item.index);
182
+ }
183
+ }
184
+ return [...selected].sort((left, right) => left - right).slice(0, Math.max(0, limit));
185
+ }
186
+ export async function sweepPending(input) {
187
+ const { state } = await input.artifact.read();
188
+ return sweepPendingFromState({ ...input, state });
189
+ }
190
+ export async function sweepPendingFromState(input) {
191
+ 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 }));
192
+ let records = [];
193
+ try {
194
+ records = await input.hostLedger.listWakeAttempts({ now: input.now });
195
+ }
196
+ catch {
197
+ records = [];
198
+ }
199
+ const attempts = records.flatMap((record) => {
200
+ const index = parseActivityId(record.activity);
201
+ if (index === undefined || record.routeKind === undefined || typeof record.attemptN !== 'number')
202
+ return [];
203
+ 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 }) }];
204
+ });
205
+ const delivery = input.deriveDelivery?.(input.state) ?? deriveDeliveryModel(input.state);
206
+ return selectPendingWakeActivities(input.state, bindings, attempts, input.now, input.graceMs, input.limit, delivery);
207
+ }
@@ -35,6 +35,7 @@ export type WakeDispatchResult = {
35
35
  message: string;
36
36
  diagnostic?: unknown;
37
37
  retainRoute?: boolean;
38
+ routeStale?: boolean;
38
39
  } | {
39
40
  outcome: 'cancelled';
40
41
  };