@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.
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +4 -0
- package/dist/artifact.js +33 -2
- package/dist/cli/context.js +1 -1
- package/dist/cli/maintenance-commands.js +12 -1
- package/dist/cli/observation-commands.js +3 -16
- package/dist/cli/program.js +0 -4
- package/dist/cli/square-commands.js +23 -3
- package/dist/cmd/notify-once.js +5 -15
- package/dist/decisions.js +10 -2
- package/dist/delivery-health.js +55 -136
- package/dist/doctor.js +1 -0
- package/dist/file-lock.js +112 -0
- package/dist/harness-codex.js +35 -29
- package/dist/harness-links.js +0 -3
- package/dist/harness-pi.js +57 -0
- package/dist/harness.js +10 -15
- package/dist/help.js +8 -8
- package/dist/index.js +5 -1
- package/dist/model.js +4 -0
- package/dist/notifications.js +205 -28
- package/dist/paseo-connection.js +135 -0
- package/dist/paseo-delivery.js +73 -144
- package/dist/paseo-state.js +1 -1
- package/dist/paseo-timeline.js +32 -42
- package/dist/presentation.js +2 -2
- package/dist/presented.js +10 -72
- package/dist/registry.js +23 -24
- package/dist/routes.js +153 -0
- package/dist/square-application.js +47 -49
- package/dist/stream.js +1 -1
- package/dist/wake-attempts.js +171 -0
- package/dist/wake-evidence.js +35 -0
- package/dist/wake-port.js +22 -0
- package/dist/wake-sink.js +45 -6
- package/dist/watch.js +1 -2
- package/guides/participant.md +1 -1
- package/package.json +6 -1
- package/skills/brainstorm/SKILL.md +24 -24
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +4 -3
- package/skills/square-feedback/SKILL.md +2 -2
- package/dist/notification-failures.js +0 -54
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 {
|
|
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 {
|
|
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);
|
package/dist/cli/context.js
CHANGED
|
@@ -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] === '--
|
|
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 {
|
|
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
|
-
}
|
package/dist/cli/program.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/dist/cmd/notify-once.js
CHANGED
|
@@ -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 {
|
|
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] === '--
|
|
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 --
|
|
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(
|
|
24
|
+
await sleep(wakeGraceMs());
|
|
26
25
|
await processActNotificationsOnce(squarePath, actIndex);
|
|
27
26
|
}
|
|
28
|
-
main().catch((
|
|
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, {
|
|
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: {
|
|
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,
|
package/dist/delivery-health.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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