@astrosheep/square 0.3.9 → 0.3.11

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 (43) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/dist/activity.js +4 -0
  3. package/dist/artifact.js +33 -2
  4. package/dist/cli/context.js +1 -1
  5. package/dist/cli/maintenance-commands.js +12 -1
  6. package/dist/cli/observation-commands.js +3 -16
  7. package/dist/cli/program.js +0 -4
  8. package/dist/cli/square-commands.js +23 -3
  9. package/dist/cmd/notify-once.js +5 -15
  10. package/dist/decisions.js +10 -2
  11. package/dist/delivery-health.js +55 -136
  12. package/dist/doctor.js +1 -0
  13. package/dist/file-lock.js +112 -0
  14. package/dist/harness-codex.js +35 -29
  15. package/dist/harness-links.js +0 -3
  16. package/dist/harness-pi.js +57 -0
  17. package/dist/harness.js +10 -15
  18. package/dist/help.js +8 -8
  19. package/dist/index.js +5 -1
  20. package/dist/model.js +4 -0
  21. package/dist/notifications.js +205 -28
  22. package/dist/paseo-connection.js +135 -0
  23. package/dist/paseo-delivery.js +73 -144
  24. package/dist/paseo-state.js +1 -1
  25. package/dist/paseo-timeline.js +32 -42
  26. package/dist/presentation.js +2 -2
  27. package/dist/presented.js +10 -72
  28. package/dist/registry.js +23 -24
  29. package/dist/routes.js +153 -0
  30. package/dist/square-application.js +47 -49
  31. package/dist/stream.js +1 -1
  32. package/dist/wake-attempts.js +171 -0
  33. package/dist/wake-evidence.js +35 -0
  34. package/dist/wake-port.js +22 -0
  35. package/dist/wake-sink.js +45 -6
  36. package/dist/watch.js +1 -2
  37. package/guides/participant.md +1 -1
  38. package/package.json +6 -1
  39. package/skills/brainstorm/SKILL.md +24 -24
  40. package/skills/square/.claude-plugin/plugin.json +1 -1
  41. package/skills/square/SKILL.md +4 -3
  42. package/skills/square-feedback/SKILL.md +2 -2
  43. package/dist/notification-failures.js +0 -54
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
package/dist/activity.js CHANGED
@@ -7,6 +7,7 @@ import { SquareError, validateName } from './model.js';
7
7
  import { expressHintLine, renderActivityBlocked, renderActivityLimit, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, withPathOutput, } from './presentation.js';
8
8
  import { currentHold, inSquareCount, nowMs, SLEEP_MS, resolveRosterName } from './runtime.js';
9
9
  import { resolveKnownName } from './decisions.js';
10
+ import { dispatchActNotifications } from './notifications.js';
10
11
  import { execute } from './square-application.js';
11
12
  import { formatTimestamp } from './time.js';
12
13
  function draftDirFor(squarePath) {
@@ -83,6 +84,9 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
83
84
  const held = currentHold(freshDoc.acts).active;
84
85
  switch (decision.type) {
85
86
  case 'sent': {
87
+ const sayAct = committed.acts.find((act) => act.kind === 'say');
88
+ if (sayAct !== undefined)
89
+ await dispatchActNotifications(squarePath, sayAct);
86
90
  const hasPending = decision.pendingPublic.length > 0 || decision.pendingRoomChanges.length > 0;
87
91
  const pending = hasPending ? `\n\n${renderPendingFeed(freshDoc.acts, decision.pendingPublic, decision.pendingRoomChanges)}` : '';
88
92
  const hint = expressHintLine(decision.ownActCount);
package/dist/artifact.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { ACTIVITIES_HEADING, ACTIVITIES_MARKER, ACT_MARKER_PREFIX, WARMUP_HEADING, WARMUP_MARKER, SquareError, CURRENT_FORMAT_VERSION, formatHardCap, sameName, } from './model.js';
3
3
  import { formatTimestamp, parseTimestamp } from './time.js';
4
+ import { isWakeRouteKind } from './model.js';
4
5
  const V2_KINDS = new Set(['say', 'join', 'done', 'hold', 'resume']);
5
6
  export function quoteBody(body) {
6
7
  const normalized = body.replace(/\r\n/g, '\n').trim();
@@ -64,6 +65,7 @@ export function emptyRuntimeState(nextActIndex = 0) {
64
65
  cursors: {},
65
66
  deliveryReceipts: {},
66
67
  leases: {},
68
+ notifyLeases: {},
67
69
  };
68
70
  }
69
71
  function renderFrontmatter(doc) {
@@ -136,12 +138,16 @@ function validateRuntimeSidecar(squarePath, value) {
136
138
  if (!isObject(value.leases) || !Object.values(value.leases).every(isWatchLease)) {
137
139
  throw invalidRuntimeSidecar(squarePath, 'leases contains an invalid watch lease.');
138
140
  }
141
+ if (value.notifyLeases !== undefined && (!isObject(value.notifyLeases) || !Object.values(value.notifyLeases).every(isNotifyLease))) {
142
+ throw invalidRuntimeSidecar(squarePath, 'notifyLeases contains an invalid notify lease.');
143
+ }
139
144
  return {
140
145
  version: 2,
141
146
  nextActIndex: value.nextActIndex,
142
147
  cursors: value.cursors,
143
148
  deliveryReceipts: value.deliveryReceipts,
144
149
  leases: value.leases,
150
+ notifyLeases: (value.notifyLeases ?? {}),
145
151
  };
146
152
  }
147
153
  export function loadRuntimeSidecar(squarePath, fallbackRuntime) {
@@ -284,6 +290,21 @@ export function isWatchLease(value) {
284
290
  const mention = value.filter.mention;
285
291
  return mention === undefined || typeof mention === 'string';
286
292
  }
293
+ export function isNotifyLease(value) {
294
+ if (!isObject(value))
295
+ return false;
296
+ if (typeof value.leaseId !== 'string' || value.leaseId === '')
297
+ return false;
298
+ if (typeof value.expiresAt !== 'number' || !Number.isFinite(value.expiresAt))
299
+ return false;
300
+ if (value.phase !== 'claimed' && value.phase !== 'dispatching')
301
+ return false;
302
+ if (value.attemptN !== undefined && (typeof value.attemptN !== 'number' || !Number.isInteger(value.attemptN) || value.attemptN <= 0))
303
+ return false;
304
+ if (value.routeKind !== undefined && !isWakeRouteKind(value.routeKind))
305
+ return false;
306
+ return value.phase !== 'dispatching' || (value.attemptN !== undefined && value.routeKind !== undefined);
307
+ }
287
308
  function invalidVersionGuidance(reason) {
288
309
  return new SquareError('invalid_args', `${reason} This format is no longer supported. Create a new square with \`square build\`.`);
289
310
  }
@@ -439,7 +460,11 @@ function normalizeActMeta(marker, kind, actor, head) {
439
460
  if (marker.reply !== undefined && kind !== 'say') {
440
461
  throw new SquareError('invalid_args', 'Invalid square: only say acts may reply to another activity.');
441
462
  }
442
- return { index: marker.index, ...(marker.reach !== undefined ? { reach: marker.reach } : {}), ...(marker.reply !== undefined ? { reply: marker.reply } : {}) };
463
+ return {
464
+ index: marker.index,
465
+ ...(marker.reach !== undefined ? { reach: marker.reach } : {}),
466
+ ...(marker.reply !== undefined ? { reply: marker.reply } : {}),
467
+ };
443
468
  }
444
469
  export function activitiesSourceLines(text) {
445
470
  const lines = text.split('\n');
@@ -490,7 +515,13 @@ function parseActBlock(blockLines) {
490
515
  i++;
491
516
  body = unquoteBody(blockLines.slice(i));
492
517
  }
493
- return { ...head, body, index: meta.index, ...(meta.reach !== undefined ? { reach: meta.reach } : {}), ...(meta.reply !== undefined ? { reply: meta.reply } : {}) };
518
+ return {
519
+ ...head,
520
+ body,
521
+ index: meta.index,
522
+ ...(meta.reach !== undefined ? { reach: meta.reach } : {}),
523
+ ...(meta.reply !== undefined ? { reply: meta.reply } : {}),
524
+ };
494
525
  }
495
526
  function parseActs(text) {
496
527
  const lines = activitiesSourceLines(text);
@@ -118,7 +118,7 @@ export function parseGlobalArgs(rawArgs) {
118
118
  let requestedPath;
119
119
  let name;
120
120
  for (let index = 0; index < args.length; index++) {
121
- if (args[index] === '--square-path') {
121
+ if (args[index] === '--location') {
122
122
  requestedPath = requireValue(args, index, args[index]);
123
123
  args.splice(index, 2);
124
124
  index -= 1;
@@ -19,6 +19,17 @@ function readSquareText(squarePath) {
19
19
  function quarantinePath(squarePath) {
20
20
  return squarePath.replace(/\.md$/, '') + '.quarantine.md';
21
21
  }
22
+ function registryActs(squarePath) {
23
+ if (!fs.existsSync(squarePath))
24
+ return [];
25
+ try {
26
+ return loadSquare(squarePath).acts;
27
+ }
28
+ catch {
29
+ // A temporarily unreadable artifact cannot disprove a cache binding.
30
+ return undefined;
31
+ }
32
+ }
22
33
  export const doctorCommand = {
23
34
  parse(argv, context) {
24
35
  let fix = false;
@@ -59,7 +70,7 @@ export const doctorCommand = {
59
70
  };
60
71
  }
61
72
  const repaired = repair.repaired;
62
- const registry = pruneRegistry();
73
+ const registry = pruneRegistry(registryActs);
63
74
  if (registry.removed > 0) {
64
75
  repaired.actions.push({ message: `pruned ${registry.removed} obsolete registry membership(s)` });
65
76
  }
@@ -3,11 +3,11 @@ import { runClaudeHook } from '../claude-hook.js';
3
3
  import { runCodexHook } from '../codex-hook.js';
4
4
  import { coreActivities, coreParticipants, coreStatus } from '../decisions.js';
5
5
  import { sessionInbox } from '../inbox.js';
6
+ import { sweepPendingNotifications } from '../notifications.js';
6
7
  import { cmdListSquares } from '../list.js';
7
8
  import { sameName } from '../model.js';
8
9
  import { commandPrefix, participantCommandPrefix, renderActivitiesView, renderGrepActivitiesView, renderVisibleEvent, withPathOutput, } from '../presentation.js';
9
- import { recordLocalJoin } from '../registry.js';
10
- import { actId, inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName, sayNumberFor, } from '../runtime.js';
10
+ import { actId, inSquareCount, nowMs, sayNumberFor } from '../runtime.js';
11
11
  import { cmdStream, cmdStreamNdjson } from '../stream.js';
12
12
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
13
13
  import { cmdWatch } from '../watch.js';
@@ -93,6 +93,7 @@ export const catchCommand = {
93
93
  },
94
94
  async execute(intent, context) {
95
95
  await cmdWatch(context.squarePath, requireParticipant(context.name), intent);
96
+ await sweepPendingNotifications(context.squarePath);
96
97
  },
97
98
  present: () => { },
98
99
  };
@@ -440,17 +441,3 @@ function hookCommand(runHook) {
440
441
  }
441
442
  export const claudeHookCommand = hookCommand(runClaudeHook);
442
443
  export const codexHookCommand = hookCommand(runCodexHook);
443
- /** Maintain the local discovery cache before participant-facing adapters run. */
444
- export function refreshLocalRegistration(squarePath, name) {
445
- if (name === undefined)
446
- return;
447
- try {
448
- const doc = loadSquare(squarePath);
449
- const known = resolveRosterName(doc, name);
450
- if (known !== undefined && isCurrentlyJoined(doc.acts, known))
451
- recordLocalJoin(known, squarePath);
452
- }
453
- catch {
454
- // The machine-local discovery cache never makes a Square command fail.
455
- }
456
- }
@@ -1,7 +1,6 @@
1
1
  import { helpRequest } from '../help.js';
2
2
  import { SquareError } from '../model.js';
3
3
  import { defaultContext, parseGlobalArgs } from './context.js';
4
- import { refreshLocalRegistration } from './observation-commands.js';
5
4
  import { executeRegisteredCommand, findCommand } from './registry.js';
6
5
  function isMutatingCommand(command, argv) {
7
6
  if (['build', 'join', 'catch', 'express', 'done', 'hold', 'resume', 'compact'].includes(command))
@@ -37,9 +36,6 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
37
36
  process.stderr.write('✕ more than one square is active here; choose the path before changing or consuming activity.\n» square list\n');
38
37
  process.exit(2);
39
38
  }
40
- if (['express', 'catch', 'done', 'hold', 'resume'].includes(command)) {
41
- refreshLocalRegistration(parsed.squarePath, parsed.name);
42
- }
43
39
  await executeRegisteredCommand(command, parsed.args.slice(1), defaultContext(command, parsed.squarePath, parsed.name));
44
40
  }
45
41
  catch (error) {
@@ -3,7 +3,8 @@ import { loadSquare } from '../artifact.js';
3
3
  import { cmdCompact } from '../compact.js';
4
4
  import { SquareError, formatHardCap, validateName, } from '../model.js';
5
5
  import { participantCommandPrefix, quoteShell, renderEventCli, renderPublicTail, withPathOutput, } from '../presentation.js';
6
- import { hasAutomaticDeliveryIdentity, recordLocalDone, recordLocalJoin } from '../registry.js';
6
+ import { hasAutomaticDeliveryIdentity, localParticipantOwner, recordLocalDone, recordLocalJoin } from '../registry.js';
7
+ import { sweepPendingNotifications } from '../notifications.js';
7
8
  import { inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName } from '../runtime.js';
8
9
  import { createSquare, execute } from '../square-application.js';
9
10
  import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireValue, resolveBody, usage, } from './context.js';
@@ -56,6 +57,7 @@ export const buildCommand = {
56
57
  };
57
58
  function parseJoin(argv, context) {
58
59
  let lastN = 10;
60
+ let kick = false;
59
61
  for (let index = 0; index < argv.length; index++) {
60
62
  if (argv[index] === '--last') {
61
63
  lastN = parsePositiveInteger(requireValue(argv, index, argv[index]), argv[index]);
@@ -64,11 +66,14 @@ function parseJoin(argv, context) {
64
66
  else if (argv[index] === '--all') {
65
67
  lastN = null;
66
68
  }
69
+ else if (argv[index] === '--kick') {
70
+ kick = true;
71
+ }
67
72
  else {
68
73
  usage(context.command);
69
74
  }
70
75
  }
71
- return { name: requireParticipant(context.name), lastN };
76
+ return { name: requireParticipant(context.name), lastN, kick };
72
77
  }
73
78
  export const joinCommand = {
74
79
  parse: parseJoin,
@@ -81,6 +86,7 @@ export const joinCommand = {
81
86
  const after = loadSquare(context.squarePath);
82
87
  const preamble = after.preamble.at(-1) === '---' ? after.preamble.slice(0, -1) : after.preamble;
83
88
  recordLocalJoin(joinedName, context.squarePath);
89
+ await sweepPendingNotifications(context.squarePath);
84
90
  const activities = renderPublicTail(after.acts, intent.lastN, nowMs(), joinedName);
85
91
  const contextText = preamble.join('\n').trim();
86
92
  const fallback = hasAutomaticDeliveryIdentity()
@@ -102,11 +108,24 @@ export const joinCommand = {
102
108
  const joinedName = resolveRosterName(doc, intent.name);
103
109
  if (joinedName === undefined || !isCurrentlyJoined(doc.acts, joinedName))
104
110
  throw error;
111
+ const reconnect = localParticipantOwner(context.squarePath, joinedName) !== undefined;
112
+ if (!intent.kick && !reconnect) {
113
+ fail([
114
+ `✕ ${joinedName} shoos you out of the square`,
115
+ ` · a same-named participant stands here — the name is taken`,
116
+ ` · --kick banishes her and the name becomes yours`,
117
+ `» ${participantCommandPrefix(context.squarePath, joinedName)} join --kick`,
118
+ ].join('\n'));
119
+ }
105
120
  recordLocalJoin(joinedName, context.squarePath);
121
+ await sweepPendingNotifications(context.squarePath);
106
122
  const fallback = hasAutomaticDeliveryIdentity()
107
123
  ? ''
108
124
  : `\n» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m\n no session delivery detected — keep this catch open for new activity`;
109
- return withPathOutput(context.squarePath, `● ${joinedName} is already in the square${fallback}`, { participantCount: inSquareCount(doc) });
125
+ const line = reconnect && !intent.kick
126
+ ? `● you are already in the square`
127
+ : `✓ you banished the original ${joinedName} — the name is yours`;
128
+ return withPathOutput(context.squarePath, `${line}${fallback}`, { participantCount: inSquareCount(doc) });
110
129
  }
111
130
  },
112
131
  present: (result) => process.stdout.write(result),
@@ -156,6 +175,7 @@ function parseActivity(argv, context) {
156
175
  export const expressCommand = {
157
176
  parse: parseActivity,
158
177
  async execute(intent, context) {
178
+ await sweepPendingNotifications(context.squarePath);
159
179
  const reachArg = intent.reach === 'bell' ? ' --bell' : intent.reach === undefined ? '' : ` --beside ${quoteShell(intent.reach.beside)}`;
160
180
  await cmdActivity(context.squarePath, intent.name, intent.activity, resolveBody, {
161
181
  force: intent.force,
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from 'node:path';
3
3
  import { setTimeout as sleep } from 'node:timers/promises';
4
- import { recordNotificationFailure } from '../notification-failures.js';
5
- import { notificationDeliveryWaitMs, processActNotificationsOnce } from '../notifications.js';
4
+ import { processActNotificationsOnce, wakeGraceMs } from '../notifications.js';
6
5
  function args(argv) {
7
6
  let squarePath;
8
7
  let actIndex;
9
8
  for (let index = 0; index < argv.length; index += 1) {
10
- if (argv[index] === '--square-path' && argv[index + 1] !== undefined)
9
+ if (argv[index] === '--location' && argv[index + 1] !== undefined)
11
10
  squarePath = resolve(argv[++index]);
12
11
  else if (argv[index] === '--act-index' && /^\d+$/.test(argv[index + 1] ?? ''))
13
12
  actIndex = Number(argv[++index]);
@@ -15,25 +14,16 @@ function args(argv) {
15
14
  throw new Error(`Unknown notify-once argument: ${argv[index]}`);
16
15
  }
17
16
  if (squarePath === undefined || actIndex === undefined)
18
- throw new Error('notify-once requires --square-path and --act-index.');
17
+ throw new Error('notify-once requires --location and --act-index.');
19
18
  return { squarePath, actIndex };
20
19
  }
21
20
  async function main() {
22
21
  if (process.env.SQUARE_DISABLE_PASEO_WAKE === '1')
23
22
  return;
24
23
  const { squarePath, actIndex } = args(process.argv.slice(2));
25
- await sleep(notificationDeliveryWaitMs());
24
+ await sleep(wakeGraceMs());
26
25
  await processActNotificationsOnce(squarePath, actIndex);
27
26
  }
28
- main().catch((error) => {
29
- const squarePath = process.argv.includes('--square-path') ? process.argv[process.argv.indexOf('--square-path') + 1] : undefined;
30
- if (squarePath) {
31
- recordNotificationFailure(squarePath, {
32
- actIndex: Number(process.argv[process.argv.indexOf('--act-index') + 1]) || 0,
33
- sink: 'worker',
34
- message: error instanceof Error ? error.message : String(error),
35
- diagnostic: { phase: 'worker' },
36
- });
37
- }
27
+ main().catch(() => {
38
28
  process.exitCode = 0;
39
29
  });
package/dist/decisions.js CHANGED
@@ -46,7 +46,11 @@ export function decideAct(doc, input) {
46
46
  }
47
47
  const state = foldedState(doc);
48
48
  const current = participantState(state, name);
49
- const result = validate(state, { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}), ...(reply !== undefined ? { reply } : {}) }, { hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
49
+ const result = validate(state, {
50
+ kind: 'say', actor: name, at: now, body,
51
+ ...(reach !== undefined ? { reach } : {}),
52
+ ...(reply !== undefined ? { reply } : {}),
53
+ }, { hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
50
54
  if (!result.ok) {
51
55
  if (result.reason === 'done')
52
56
  throw new SquareError('conflict', `${name} is done; rejoin to express again`);
@@ -101,7 +105,11 @@ export function decideAct(doc, input) {
101
105
  const ownActCount = (current?.activityCount ?? 0) + 1;
102
106
  return {
103
107
  type: 'sent',
104
- act: { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}), ...(reply !== undefined ? { reply } : {}) },
108
+ act: {
109
+ kind: 'say', actor: name, at: now, body,
110
+ ...(reach !== undefined ? { reach } : {}),
111
+ ...(reply !== undefined ? { reply } : {}),
112
+ },
105
113
  confirmation: `● heads turn your way — #${ownActCount}`,
106
114
  ownActCount,
107
115
  pendingPublic: unreadPublic,
@@ -1,143 +1,62 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
1
  import { loadSquare } from './artifact.js';
5
- import { deriveDeliveryModel } from './delivery.js';
6
- import { readNotificationFailures } from './notification-failures.js';
7
- import { isCurrentlyJoined } from './runtime.js';
8
- import { sameName } from './model.js';
2
+ import { deriveDeliveryModel, } from './delivery.js';
9
3
  import { formatDuration } from './time.js';
10
- const STALE_MS = 60_000;
11
- const LOOKBACK_MS = 60 * 60 * 1000;
12
- function positive(name, fallback, env) {
13
- const raw = env[name];
14
- if (raw === undefined)
15
- return fallback;
16
- const value = Number(raw);
17
- if (!Number.isInteger(value) || value <= 0)
18
- throw new Error(`Invalid ${name}: expected a positive integer.`);
19
- return value;
20
- }
21
- export function deliveryStaleMs(env = process.env) {
22
- return positive('SQUARE_DELIVERY_STALE_MS', STALE_MS, env);
23
- }
24
- export function deliveryLookbackMs(env = process.env) {
25
- return positive('SQUARE_DELIVERY_LOOKBACK_MS', LOOKBACK_MS, env);
26
- }
27
- function actedAfter(doc, recipient, actIndex) {
28
- return doc.acts.some((act) => act.actor !== undefined && sameName(act.actor, recipient) && act.index > actIndex);
29
- }
30
- function pending(squarePath, now) {
4
+ import { joinedRecipients, wakeEvidence } from './wake-evidence.js';
5
+ const DISPLAY_ORDER = [
6
+ 'awaiting',
7
+ 'wake-accepted',
8
+ 'wake-unknown',
9
+ 'presented-not-delivered',
10
+ 'unreachable',
11
+ ];
12
+ const ACTIONABLE = new Set(['wake-unknown', 'unreachable']);
13
+ /** Purely classify current pending attention from the artifact and durable ledgers. */
14
+ export function classifyDeliveryHealth(squarePath, opts) {
15
+ const now = opts.now ?? Date.now();
16
+ const env = opts.env ?? process.env;
31
17
  const doc = loadSquare(squarePath);
32
18
  const model = deriveDeliveryModel(doc);
33
- const recipients = [...new Set(doc.acts.filter((act) => act.kind === 'join').map((act) => act.actor))]
34
- .filter((name) => isCurrentlyJoined(doc.acts, name));
35
- return recipients.flatMap((recipient) => model.pendingFor(recipient).map((note) => ({
36
- squarePath,
37
- recipient: note.recipient,
38
- actIndex: note.item.index,
39
- actor: note.item.actor,
40
- at: note.item.at,
41
- ageMs: now - note.item.at,
42
- route: note.route,
43
- actedAfterWithoutDelivery: actedAfter(doc, note.recipient, note.item.index),
44
- })));
45
- }
46
- export function partitionPendingDeliveries(squarePath, opts = {}) {
47
- const now = opts.now ?? Date.now();
48
- const staleMs = opts.staleMs ?? deliveryStaleMs();
49
- const lookbackMs = Math.max(opts.lookbackMs ?? deliveryLookbackMs(), staleMs);
50
- const recent = [];
51
- const historical = [];
52
- for (const item of pending(squarePath, now)) {
53
- if (item.ageMs >= staleMs && item.ageMs <= lookbackMs)
54
- recent.push(item);
55
- else if (item.ageMs >= staleMs)
56
- historical.push(item);
57
- }
58
- return { recent, historical };
59
- }
60
- function byRecipient(items) {
61
- const groups = new Map();
62
- for (const item of items)
63
- groups.set(item.recipient, [...(groups.get(item.recipient) ?? []), item]);
64
- return [...groups].map(([recipient, notes]) => {
65
- const oldest = notes.reduce((first, item) => item.at < first.at ? item : first);
66
- const adapterFault = notes.some((item) => item.actedAfterWithoutDelivery);
67
- return adapterFault
68
- ? ` · @${recipient}: ${notes.length} pending; they acted after it without a receipt (act_${oldest.actIndex})`
69
- : ` · @${recipient}: ${notes.length} pending (oldest act_${oldest.actIndex} from @${oldest.actor})`;
70
- });
71
- }
72
- export function formatStaleDeliveryWarnings(recent, historical = [], opts = {}) {
73
- const out = [];
74
- if (recent.length > 0) {
75
- const adapterFaults = recent.filter((item) => item.actedAfterWithoutDelivery);
76
- out.push(adapterFaults.length > 0
77
- ? `✕ ${adapterFaults.length} pending notification(s) point to an adapter/pull dead path.`
78
- : `✕ ${recent.length} recent notification(s) have no delivered receipt.`);
79
- out.push(...byRecipient(recent));
19
+ return joinedRecipients(doc).flatMap((recipient) => model.pendingFor(recipient).map((note) => {
20
+ const ageMs = Math.max(0, now - note.item.at);
21
+ const evidence = wakeEvidence(squarePath, note.recipient, note.item.index, now, env);
22
+ const kind = evidence.presented
23
+ ? 'presented-not-delivered'
24
+ : evidence.terminal?.outcome === 'accepted'
25
+ ? 'wake-accepted'
26
+ : evidence.terminal?.outcome === 'unknown'
27
+ ? 'wake-unknown'
28
+ : ageMs > opts.graceMs && evidence.attemptableRoutes.length === 0
29
+ ? 'unreachable'
30
+ : 'awaiting';
31
+ const attempt = evidence.terminal ?? evidence.attempts.at(-1);
32
+ return {
33
+ squarePath,
34
+ recipient: note.recipient,
35
+ actIndex: note.item.index,
36
+ actor: note.item.actor,
37
+ at: note.item.at,
38
+ ageMs,
39
+ route: note.route,
40
+ kind,
41
+ ...(attempt === undefined ? {} : { attempt }),
42
+ };
43
+ }));
44
+ }
45
+ function formatItem(item) {
46
+ const evidence = item.attempt?.signature === undefined ? '' : ` · ${item.attempt.signature}`;
47
+ return ` · act_${item.actIndex} @${item.recipient} from @${item.actor} · ${formatDuration(item.ageMs)}${evidence}`;
48
+ }
49
+ export function doctorDeliveryHealth(squarePath, graceMs, now = Date.now(), env = process.env) {
50
+ const items = classifyDeliveryHealth(squarePath, { graceMs, now, env });
51
+ if (items.length === 0)
52
+ return ['✓ no pending delivery attention'];
53
+ const out = [`· delivery attention · ${items.length} pending`];
54
+ for (const kind of DISPLAY_ORDER) {
55
+ const group = items.filter((item) => item.kind === kind);
56
+ if (group.length === 0)
57
+ continue;
58
+ out.push(`${ACTIONABLE.has(kind) ? '✕' : '○'} ${kind}: ${group.length}`);
59
+ out.push(...group.map(formatItem));
80
60
  }
81
- if (historical.length > 0) {
82
- out.push(`○ ${historical.length} older pending notification(s) remain as historical backlog.`);
83
- out.push(...byRecipient(historical));
84
- if (opts.previousBacklog !== undefined) {
85
- const delta = historical.length - opts.previousBacklog;
86
- out.push(delta === 0 ? ' · backlog unchanged since last doctor.' : delta > 0 ? ` · backlog grew by ${delta} since last doctor.` : ` · backlog shrank by ${-delta} since last doctor.`);
87
- }
88
- }
89
- else if ((opts.previousBacklog ?? 0) > 0)
90
- out.push(`○ backlog cleared (was ${opts.previousBacklog}).`);
91
61
  return out;
92
62
  }
93
- function baselineFile(env) {
94
- return env.SQUARE_DELIVERY_BASELINE ?? path.join(os.homedir(), '.square', 'delivery-baseline.json');
95
- }
96
- function baseline(squarePath, env) {
97
- try {
98
- return JSON.parse(fs.readFileSync(baselineFile(env), 'utf8'))[path.resolve(squarePath)]?.backlogCount;
99
- }
100
- catch {
101
- return undefined;
102
- }
103
- }
104
- function writeBaseline(squarePath, backlogCount, env, at) {
105
- const file = baselineFile(env);
106
- let rows = {};
107
- try {
108
- rows = JSON.parse(fs.readFileSync(file, 'utf8'));
109
- }
110
- catch { }
111
- rows[path.resolve(squarePath)] = { backlogCount, at };
112
- fs.mkdirSync(path.dirname(file), { recursive: true });
113
- fs.writeFileSync(file, `${JSON.stringify(rows, null, 2)}\n`, { mode: 0o600 });
114
- }
115
- function formatFailures(squarePath, recent, env) {
116
- const failures = readNotificationFailures(squarePath, env);
117
- if (failures.length === 0)
118
- return [];
119
- const pendingKeys = new Set(recent.map((item) => `${item.recipient}\0${item.actIndex}`));
120
- const current = failures.filter((item) => item.recipient !== undefined && pendingKeys.has(`${item.recipient}\0${item.actIndex}`));
121
- const rows = current.length > 0 ? current : failures;
122
- const historical = current.length === 0;
123
- const latest = rows.at(-1);
124
- const diagnostic = latest.diagnostic;
125
- return [
126
- historical ? `○ ${rows.length} historical notification failure(s) retained: ${latest.message}` : `✕ ${rows.length} notification attempt(s) failed: ${latest.message}; receipt remains pending.`,
127
- ...(diagnostic?.passwordPresent === false ? [' · PASEO_PASSWORD absent; pass PASEO_PASSWORD to the Codex process.'] : []),
128
- ` · ${notificationFailuresPathForDisplay(squarePath, env)}`,
129
- ];
130
- }
131
- function notificationFailuresPathForDisplay(squarePath, env) {
132
- return env.SQUARE_NOTIFICATION_FAILURES ?? path.join(path.dirname(squarePath), 'notification-failures.ndjsonl');
133
- }
134
- export function doctorDeliveryHealth(squarePath, now = Date.now(), env = process.env) {
135
- const { recent, historical } = partitionPendingDeliveries(squarePath, { now, staleMs: deliveryStaleMs(env), lookbackMs: deliveryLookbackMs(env) });
136
- const prior = baseline(squarePath, env);
137
- writeBaseline(squarePath, historical.length, env, now);
138
- return [
139
- `· stale after ${formatDuration(deliveryStaleMs(env))} · scan window ${formatDuration(deliveryLookbackMs(env))}`,
140
- ...(recent.length === 0 && historical.length === 0 ? ['✓ no stale undelivered notifications'] : formatStaleDeliveryWarnings(recent, historical, { previousBacklog: prior })),
141
- ...formatFailures(squarePath, recent, env),
142
- ];
143
- }
package/dist/doctor.js CHANGED
@@ -29,6 +29,7 @@ export function planRepair(text) {
29
29
  cursors: {},
30
30
  deliveryReceipts: {},
31
31
  leases: {},
32
+ notifyLeases: {},
32
33
  },
33
34
  };
34
35
  return { diagnosis, repaired: { doc, actions, quarantinedBlocks: diagnosis.quarantined.map((q) => q.raw) } };