@astrosheep/square 0.3.57 → 0.3.61
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +16 -14
- package/dist/attention-presentation.js +19 -19
- package/dist/automatic-session.js +2 -2
- package/dist/catch-decisions.js +1 -1
- package/dist/cli/context.js +16 -16
- package/dist/cli/observation-commands.js +85 -45
- package/dist/cli/program.js +33 -9
- package/dist/cli/square-commands.js +21 -19
- package/dist/decisions.d.ts +2 -0
- package/dist/decisions.js +38 -31
- package/dist/delivery-health.d.ts +3 -2
- package/dist/delivery-health.js +79 -47
- package/dist/delivery-operations.js +50 -8
- package/dist/diagnostic-redaction.d.ts +2 -0
- package/dist/diagnostic-redaction.js +40 -0
- package/dist/help.js +1 -1
- package/dist/host-ledger-file-adapter.js +2 -2
- package/dist/host-ledger.d.ts +9 -2
- package/dist/list.js +12 -9
- package/dist/notifications.js +28 -11
- package/dist/ports.d.ts +5 -1
- package/dist/presentation.d.ts +7 -0
- package/dist/presentation.js +75 -67
- package/dist/registry.js +2 -2
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +37 -0
- package/dist/square-actions.js +4 -4
- package/dist/stream.js +1 -1
- package/dist/tty-style.d.ts +7 -0
- package/dist/tty-style.js +13 -0
- package/dist/views.js +6 -5
- package/dist/wake-attempts.d.ts +18 -0
- package/dist/wake-attempts.js +38 -11
- package/dist/watch.js +24 -11
- package/extensions/square-pi.js +16 -6
- package/package.json +3 -3
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { cmdActivity } from '../activity.js';
|
|
2
2
|
import { formatActivityId, formatHardCap, parseActivityId, } from '../model.js';
|
|
3
|
-
import { participantCommandPrefix, participantIdentity, quoteShell,
|
|
3
|
+
import { formatRefusal, participantCommandPrefix, participantIdentity, quoteShell, renderRoomChangeText, renderAmbientEvent, withPathOutput, } from '../presentation.js';
|
|
4
4
|
import { claimSessionParticipant, hasAutomaticDeliveryIdentity, localSessionIdentities, lookupParticipant, readParticipantOwner, recordSessionDone, } from '../registry.js';
|
|
5
5
|
import { createHostLedgerPort } from '../host-ledger-file-adapter.js';
|
|
6
6
|
import { sessionIdsFromEnvironment } from '../square-projections.js';
|
|
7
|
-
import { nowMs } from '../runtime.js';
|
|
7
|
+
import { actId, nowMs } from '../runtime.js';
|
|
8
8
|
import { createSquare, openSquare } from '../square-file-adapter.js';
|
|
9
9
|
import { closeOpenSquare } from '../open-square.js';
|
|
10
10
|
import { openParticipant, Square } from '../square-wiring.js';
|
|
11
11
|
import { entryPresentation, eventPresentation } from '../views.js';
|
|
12
12
|
import { createDefaultWakeTransport } from '../notifications.js';
|
|
13
|
+
import { style } from '../tty-style.js';
|
|
13
14
|
import { fail, parseBoundedLimit, parseHardCap, parsePositiveInteger, readStdin, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
|
|
14
15
|
const JOIN_DEFAULT_LAST = 10;
|
|
15
16
|
const JOIN_MAX_LAST = 100;
|
|
@@ -57,7 +58,7 @@ export const buildCommand = {
|
|
|
57
58
|
await createSquare(squarePath, intent.options, snippet);
|
|
58
59
|
const cap = intent.options.hardCap === null ? 'unlimited' : formatHardCap(intent.options.hardCap);
|
|
59
60
|
const throttle = intent.options.throttlePerMinute === undefined ? [] : [` · throttle ${intent.options.throttlePerMinute}/min`];
|
|
60
|
-
return withPathOutput(squarePath, ['✓
|
|
61
|
+
return withPathOutput(squarePath, ['✓ the square is open', ` · cap ${cap}`, ...throttle, ' · nobody here yet — the first join steps in'].join('\n'), { participantCount: 0 });
|
|
61
62
|
},
|
|
62
63
|
present: (result) => process.stdout.write(result),
|
|
63
64
|
};
|
|
@@ -95,12 +96,13 @@ export const joinCommand = {
|
|
|
95
96
|
const reconnect = before.joined && identity !== undefined && owner?.sessionId === identity.sessionId;
|
|
96
97
|
const isRejoin = before.joined;
|
|
97
98
|
if (isRejoin && !intent.kick && !reconnect) {
|
|
98
|
-
|
|
99
|
+
process.stderr.write(formatRefusal(squarePath, [
|
|
99
100
|
`✕ ${participantIdentity(intent.name)} shoos you out of the square`,
|
|
100
101
|
` · a same-named participant stands here — the name is taken`,
|
|
101
|
-
` · --kick banishes
|
|
102
|
-
|
|
103
|
-
].
|
|
102
|
+
` · --kick banishes the one standing there — the name becomes yours`,
|
|
103
|
+
`${participantCommandPrefix(squarePath, intent.name)} join --kick`,
|
|
104
|
+
], { participantCount: before.participantCount }));
|
|
105
|
+
process.exit(2);
|
|
104
106
|
}
|
|
105
107
|
const takeoverNeeded = isRejoin && intent.kick;
|
|
106
108
|
const oldBindings = takeoverNeeded ? await lookupParticipant(squarePath, intent.name) : [];
|
|
@@ -134,20 +136,20 @@ export const joinCommand = {
|
|
|
134
136
|
const contextText = after.joinContext;
|
|
135
137
|
const fallback = hasAutomaticDeliveryIdentity()
|
|
136
138
|
? []
|
|
137
|
-
: ['',
|
|
139
|
+
: ['', `${participantCommandPrefix(squarePath, joinedName)} catch --idle 30m`, ' the square has no way to call you — keep this catch open and stay within earshot'];
|
|
138
140
|
const scene = after.scene;
|
|
139
141
|
const entryLine = !isRejoin
|
|
140
|
-
? '●
|
|
142
|
+
? '● you stepped into the square'
|
|
141
143
|
: reconnect && !intent.kick
|
|
142
144
|
? '● you are already in the square'
|
|
143
145
|
: `✓ you banished the original ${participantIdentity(joinedName)} — the name is yours`;
|
|
144
146
|
const output = [
|
|
145
147
|
entryLine,
|
|
146
148
|
'',
|
|
147
|
-
"
|
|
149
|
+
"· carved into the fountain's edge: every word here lands on a real ear — speak when someone needs it.",
|
|
148
150
|
...(reconnect || scene === '' ? [] : ['', scene]),
|
|
149
|
-
...(isRejoin || contextText === '' ? [] : ['', 'context', contextText]),
|
|
150
|
-
...(isRejoin || activities === '' ? [] : ['', 'recent activity', activities]),
|
|
151
|
+
...(isRejoin || contextText === '' ? [] : ['', style('dim', 'context'), contextText]),
|
|
152
|
+
...(isRejoin || activities === '' ? [] : ['', style('dim', 'recent activity'), activities]),
|
|
151
153
|
...fallback,
|
|
152
154
|
].join('\n');
|
|
153
155
|
return withPathOutput(squarePath, output, { participantCount: after.participantCount });
|
|
@@ -173,7 +175,7 @@ function parseActivity(argv, context) {
|
|
|
173
175
|
else if (argument === '--no-wait')
|
|
174
176
|
noWait = true;
|
|
175
177
|
else if (argument === '--beside')
|
|
176
|
-
fail('✕ express does not know --beside\
|
|
178
|
+
fail('✕ express does not know --beside\nsquare express --help');
|
|
177
179
|
else if (argument === '--bell')
|
|
178
180
|
bell = true;
|
|
179
181
|
else if (argument === '--no-mention')
|
|
@@ -194,11 +196,11 @@ function parseActivity(argv, context) {
|
|
|
194
196
|
}
|
|
195
197
|
const reach = bell ? 'bell' : undefined;
|
|
196
198
|
if (bell && (noMention || mentions.length > 0))
|
|
197
|
-
fail('✕ --bell cannot be combined with --mention or --no-mention\
|
|
199
|
+
fail('✕ --bell cannot be combined with --mention or --no-mention\nsquare express --help');
|
|
198
200
|
if (noMention && mentions.length > 0)
|
|
199
|
-
fail('✕ --no-mention cannot be combined with --mention\
|
|
201
|
+
fail('✕ --no-mention cannot be combined with --mention\nsquare express --help');
|
|
200
202
|
if (!bell && !noMention && mentions.length === 0)
|
|
201
|
-
fail('✕ express needs --mention <name>, --no-mention, or --bell\
|
|
203
|
+
fail('✕ express needs --mention <name>, --no-mention, or --bell\nsquare express --help');
|
|
202
204
|
if (bodyArgs.length !== 1) {
|
|
203
205
|
if (bodyArgs.length === 0) {
|
|
204
206
|
if (!process.stdin.isTTY)
|
|
@@ -320,7 +322,7 @@ export const doneCommand = {
|
|
|
320
322
|
const name = result.activity.actor;
|
|
321
323
|
const presentation = await openSquare(squarePath, { clock: nowMs });
|
|
322
324
|
const participantCount = (await entryPresentation(presentation, name).finally(() => closeOpenSquare(presentation))).participantCount;
|
|
323
|
-
return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · just now`, { participantCount });
|
|
325
|
+
return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · ${result.activity.id} · just now`, { participantCount });
|
|
324
326
|
},
|
|
325
327
|
present: (result) => process.stdout.write(result),
|
|
326
328
|
};
|
|
@@ -341,7 +343,7 @@ export const holdCommand = {
|
|
|
341
343
|
const presentationSquare = await openSquare(squarePath, { clock: nowMs });
|
|
342
344
|
const presentation = await eventPresentation(presentationSquare, result.activity.id);
|
|
343
345
|
await closeOpenSquare(presentationSquare);
|
|
344
|
-
return withPathOutput(squarePath,
|
|
346
|
+
return withPathOutput(squarePath, `· ${renderRoomChangeText(presentation.activity)} · ${actId(presentation.activity.index)} · just now`, { participantCount: presentation.participantCount, held: true });
|
|
345
347
|
}
|
|
346
348
|
finally {
|
|
347
349
|
await square.close();
|
|
@@ -365,7 +367,7 @@ export const resumeCommand = {
|
|
|
365
367
|
const presentationSquare = await openSquare(squarePath, { clock: nowMs });
|
|
366
368
|
const presentation = await eventPresentation(presentationSquare, result.activity.id);
|
|
367
369
|
await closeOpenSquare(presentationSquare);
|
|
368
|
-
return withPathOutput(squarePath,
|
|
370
|
+
return withPathOutput(squarePath, `✓ ${renderRoomChangeText(presentation.activity)} · ${actId(presentation.activity.index)} · just now`, { participantCount: presentation.participantCount });
|
|
369
371
|
}
|
|
370
372
|
finally {
|
|
371
373
|
await square.close();
|
package/dist/decisions.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export type ActDecision = {
|
|
|
58
58
|
type: 'bell_quota';
|
|
59
59
|
nextAt: number;
|
|
60
60
|
};
|
|
61
|
+
/** The unread activity a viewer has not caught up to, grouped by speaker. */
|
|
62
|
+
export declare function unreadActivitySummaries(squareState: SquareState, name: string, now: number): UnreadActivitySummary[];
|
|
61
63
|
export declare function decideAct(squareState: SquareState, input: {
|
|
62
64
|
name: string;
|
|
63
65
|
body: string;
|
package/dist/decisions.js
CHANGED
|
@@ -9,7 +9,7 @@ export function resolveKnownName(squareState, name) {
|
|
|
9
9
|
validateName(name);
|
|
10
10
|
const known = resolveRosterName(squareState, name);
|
|
11
11
|
if (known === undefined) {
|
|
12
|
-
throw new SquareError('invalid_args',
|
|
12
|
+
throw new SquareError('invalid_args', `✕ ${participantIdentity(name)} has never stepped into this square`);
|
|
13
13
|
}
|
|
14
14
|
return known;
|
|
15
15
|
}
|
|
@@ -66,6 +66,40 @@ function requireStanding(squareState, act) {
|
|
|
66
66
|
throw new Error(`Unexpected standing validation result: ${result.reason}`);
|
|
67
67
|
}
|
|
68
68
|
const UNREAD_PREVIEW_LIMIT = 3;
|
|
69
|
+
/** The unread activity a viewer has not caught up to, grouped by speaker. */
|
|
70
|
+
export function unreadActivitySummaries(squareState, name, now) {
|
|
71
|
+
const delivery = deriveDeliveryModel(squareState);
|
|
72
|
+
const delta = actDelta(squareState.acts, delivery.cursorFor(name));
|
|
73
|
+
const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
|
|
74
|
+
const sayCountByActor = new Map();
|
|
75
|
+
const unreadByParticipant = new Map();
|
|
76
|
+
for (const item of unreadPublic) {
|
|
77
|
+
if (item.kind === 'say') {
|
|
78
|
+
const key = item.actor.toLocaleLowerCase();
|
|
79
|
+
sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
|
|
80
|
+
}
|
|
81
|
+
if (sameName(item.actor, name))
|
|
82
|
+
continue;
|
|
83
|
+
const actorKey = item.actor.toLocaleLowerCase();
|
|
84
|
+
const currentSummary = unreadByParticipant.get(item.actor);
|
|
85
|
+
unreadByParticipant.set(item.actor, {
|
|
86
|
+
count: (currentSummary?.count ?? 0) + 1,
|
|
87
|
+
latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
|
|
88
|
+
previews: [
|
|
89
|
+
...(currentSummary?.previews ?? []),
|
|
90
|
+
{ number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
|
|
91
|
+
].slice(-UNREAD_PREVIEW_LIMIT),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return [...unreadByParticipant.entries()]
|
|
95
|
+
.map(([participant, summary]) => ({
|
|
96
|
+
name: participant,
|
|
97
|
+
count: summary.count,
|
|
98
|
+
latestActivityAgeMs: Math.max(0, now - summary.latestAt),
|
|
99
|
+
previews: summary.previews,
|
|
100
|
+
}))
|
|
101
|
+
.sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
|
|
102
|
+
}
|
|
69
103
|
export function decideAct(squareState, input) {
|
|
70
104
|
const { now, force } = input;
|
|
71
105
|
const name = resolveStandingName(squareState, input.name);
|
|
@@ -81,7 +115,7 @@ export function decideAct(squareState, input) {
|
|
|
81
115
|
validateName(requested);
|
|
82
116
|
const resolved = joinedNames.find((candidate) => sameName(candidate, requested));
|
|
83
117
|
if (resolved === undefined) {
|
|
84
|
-
throw new SquareError('invalid_args',
|
|
118
|
+
throw new SquareError('invalid_args', `✕ ${participantIdentity(requested)} is not standing in this square`);
|
|
85
119
|
}
|
|
86
120
|
if (!mentionNames.some((existing) => sameName(existing, resolved))) {
|
|
87
121
|
if (mentionNames.length >= MAX_IDENTITY_SET_SIZE) {
|
|
@@ -126,34 +160,7 @@ export function decideAct(squareState, input) {
|
|
|
126
160
|
const delta = actDelta(squareState.acts, delivery.cursorFor(name));
|
|
127
161
|
const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
|
|
128
162
|
const unreadRoomChanges = [];
|
|
129
|
-
const
|
|
130
|
-
const unreadByParticipant = new Map();
|
|
131
|
-
for (const item of unreadPublic) {
|
|
132
|
-
if (item.kind === 'say') {
|
|
133
|
-
const key = item.actor.toLocaleLowerCase();
|
|
134
|
-
sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
|
|
135
|
-
}
|
|
136
|
-
if (sameName(item.actor, name))
|
|
137
|
-
continue;
|
|
138
|
-
const actorKey = item.actor.toLocaleLowerCase();
|
|
139
|
-
const currentSummary = unreadByParticipant.get(item.actor);
|
|
140
|
-
unreadByParticipant.set(item.actor, {
|
|
141
|
-
count: (currentSummary?.count ?? 0) + 1,
|
|
142
|
-
latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
|
|
143
|
-
previews: [
|
|
144
|
-
...(currentSummary?.previews ?? []),
|
|
145
|
-
{ number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
|
|
146
|
-
].slice(-UNREAD_PREVIEW_LIMIT),
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
const activitySummaries = [...unreadByParticipant.entries()]
|
|
150
|
-
.map(([participant, summary]) => ({
|
|
151
|
-
name: participant,
|
|
152
|
-
count: summary.count,
|
|
153
|
-
latestActivityAgeMs: Math.max(0, now - summary.latestAt),
|
|
154
|
-
previews: summary.previews,
|
|
155
|
-
}))
|
|
156
|
-
.sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
|
|
163
|
+
const activitySummaries = unreadActivitySummaries(squareState, name, now);
|
|
157
164
|
const latestActivityAgeMs = activitySummaries[0]?.latestActivityAgeMs;
|
|
158
165
|
const hasUnread = unreadPublic.length > 0;
|
|
159
166
|
const hasFreshUnreadActivity = latestActivityAgeMs !== undefined && latestActivityAgeMs <= UNREAD_BLOCK_GRACE_MS;
|
|
@@ -168,7 +175,7 @@ export function decideAct(squareState, input) {
|
|
|
168
175
|
...(reach !== undefined ? { reach } : {}),
|
|
169
176
|
...(reply !== undefined ? { reply } : {}),
|
|
170
177
|
},
|
|
171
|
-
confirmation: `●
|
|
178
|
+
confirmation: `● your activity lands — #${ownActCount}`,
|
|
172
179
|
ownActCount,
|
|
173
180
|
pendingPublic: unreadPublic,
|
|
174
181
|
pendingRoomChanges: unreadRoomChanges,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type DirectedNotificationRoute } from './delivery.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type WakeAttempt } from './square-projections.js';
|
|
3
|
+
import { type WakeReleaseDiagnostic } from './wake-attempts.js';
|
|
3
4
|
export type DeliveryHealthKind = 'awaiting' | 'wake-accepted' | 'wake-unknown' | 'presented-not-delivered' | 'unreachable';
|
|
4
5
|
export interface DeliveryHealthItem {
|
|
5
6
|
squarePath: string;
|
|
@@ -18,5 +19,5 @@ export declare function classifyDeliveryHealth(squarePath: string, opts: {
|
|
|
18
19
|
now?: number;
|
|
19
20
|
env?: NodeJS.ProcessEnv;
|
|
20
21
|
}): Promise<DeliveryHealthItem[]>;
|
|
21
|
-
export declare function renderDeliveryHealth(items: readonly DeliveryHealthItem[]): string[];
|
|
22
|
+
export declare function renderDeliveryHealth(items: readonly DeliveryHealthItem[], releases?: readonly WakeReleaseDiagnostic[], now?: number): string[];
|
|
22
23
|
export declare function doctorDeliveryHealth(squarePath: string, graceMs: number, now?: number, env?: NodeJS.ProcessEnv): Promise<string[]>;
|
package/dist/delivery-health.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { deriveDeliveryModel } from './delivery.js';
|
|
1
2
|
import { formatActivityId } from './square-core.js';
|
|
2
3
|
import { participantIdentity, truncateChars } from './presentation.js';
|
|
3
4
|
import { formatDuration } from './time.js';
|
|
4
|
-
import {
|
|
5
|
+
import { projectWakeEvidenceFromState } from './square-projections.js';
|
|
5
6
|
import { openSquare } from './square-file-adapter.js';
|
|
6
7
|
import { closeOpenSquare } from './open-square.js';
|
|
7
|
-
import {
|
|
8
|
+
import { pendingDeliveriesFromState } from './views.js';
|
|
9
|
+
import { readWakeReleaseDiagnostics } from './wake-attempts.js';
|
|
8
10
|
const DISPLAY_ORDER = [
|
|
9
11
|
'awaiting',
|
|
10
12
|
'wake-accepted',
|
|
@@ -19,37 +21,51 @@ const DISPLAY_FIELD_LIMIT = 160;
|
|
|
19
21
|
export async function classifyDeliveryHealth(squarePath, opts) {
|
|
20
22
|
const now = opts.now ?? Date.now();
|
|
21
23
|
const env = opts.env ?? process.env;
|
|
22
|
-
const square = await openSquare(squarePath, { clock: () => now });
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
24
|
+
const square = await openSquare(squarePath, { clock: () => now, env });
|
|
25
|
+
try {
|
|
26
|
+
const { state } = await square.artifact.read();
|
|
27
|
+
const delivery = deriveDeliveryModel(state);
|
|
28
|
+
const pending = pendingDeliveriesFromState(state, delivery);
|
|
29
|
+
const evidenceProjection = await projectWakeEvidenceFromState({
|
|
30
|
+
location: squarePath,
|
|
31
|
+
state,
|
|
32
|
+
hostLedger: square.hostLedger,
|
|
33
|
+
now,
|
|
34
|
+
delivery,
|
|
35
|
+
});
|
|
36
|
+
const items = [];
|
|
37
|
+
for (const pendingDelivery of pending) {
|
|
38
|
+
for (const note of pendingDelivery.notifications) {
|
|
39
|
+
const ageMs = Math.max(0, now - note.item.at);
|
|
40
|
+
const evidence = evidenceProjection.evidence(pendingDelivery.recipient, note.item.index);
|
|
41
|
+
const kind = evidence.presented
|
|
42
|
+
? 'presented-not-delivered'
|
|
43
|
+
: evidence.terminal?.outcome === 'accepted'
|
|
44
|
+
? 'wake-accepted'
|
|
45
|
+
: evidence.terminal?.outcome === 'unknown'
|
|
46
|
+
? 'wake-unknown'
|
|
47
|
+
: ageMs > opts.graceMs && evidence.attemptableRoutes.length === 0
|
|
48
|
+
? 'unreachable'
|
|
49
|
+
: 'awaiting';
|
|
50
|
+
const attempt = evidence.terminal ?? evidence.attempts.at(-1);
|
|
51
|
+
items.push({
|
|
52
|
+
squarePath,
|
|
53
|
+
recipient: pendingDelivery.recipient,
|
|
54
|
+
actIndex: note.item.index,
|
|
55
|
+
actor: note.item.actor,
|
|
56
|
+
at: note.item.at,
|
|
57
|
+
ageMs,
|
|
58
|
+
route: note.route,
|
|
59
|
+
kind,
|
|
60
|
+
...(attempt === undefined ? {} : { attempt }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
50
63
|
}
|
|
64
|
+
return items;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await closeOpenSquare(square);
|
|
51
68
|
}
|
|
52
|
-
return items;
|
|
53
69
|
}
|
|
54
70
|
function displayField(value) {
|
|
55
71
|
const truncated = truncateChars(value, DISPLAY_FIELD_LIMIT - 1);
|
|
@@ -59,24 +75,40 @@ function formatItem(item) {
|
|
|
59
75
|
const evidence = item.attempt?.signature === undefined ? '' : ` · ${displayField(item.attempt.signature)}`;
|
|
60
76
|
return ` · ${formatActivityId(item.actIndex)} → ${displayField(participantIdentity(item.recipient))} from ${displayField(participantIdentity(item.actor))} · ${formatDuration(item.ageMs)}${evidence}`;
|
|
61
77
|
}
|
|
62
|
-
export function renderDeliveryHealth(items) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
export function renderDeliveryHealth(items, releases = [], now = Date.now()) {
|
|
79
|
+
const out = items.length === 0 ? ['✓ no pending delivery attention'] : [`· delivery attention · ${items.length} pending`];
|
|
80
|
+
if (items.length > 0) {
|
|
81
|
+
let displayed = 0;
|
|
82
|
+
for (const kind of DISPLAY_ORDER) {
|
|
83
|
+
const group = items.filter((item) => item.kind === kind);
|
|
84
|
+
if (group.length === 0)
|
|
85
|
+
continue;
|
|
86
|
+
out.push(`${ACTIONABLE.has(kind) ? '✕' : '○'} ${kind}: ${group.length}`);
|
|
87
|
+
const details = group.slice(0, DETAIL_LIMIT - displayed);
|
|
88
|
+
out.push(...details.map(formatItem));
|
|
89
|
+
displayed += details.length;
|
|
90
|
+
}
|
|
91
|
+
if (items.length > DETAIL_LIMIT)
|
|
92
|
+
out.push(`${DETAIL_LIMIT} of ${items.length} pending details shown`);
|
|
93
|
+
}
|
|
94
|
+
if (releases.length > 0) {
|
|
95
|
+
out.push(`· recent wake releases · ${releases.length}`);
|
|
96
|
+
out.push(...releases.slice(0, DETAIL_LIMIT).map((release) => formatRelease(release, now)));
|
|
97
|
+
if (releases.length > DETAIL_LIMIT)
|
|
98
|
+
out.push(`${DETAIL_LIMIT} of ${releases.length} wake releases shown`);
|
|
75
99
|
}
|
|
76
|
-
if (items.length > DETAIL_LIMIT)
|
|
77
|
-
out.push(`${DETAIL_LIMIT} of ${items.length} pending details shown`);
|
|
78
100
|
return out;
|
|
79
101
|
}
|
|
102
|
+
function formatRelease(release, now) {
|
|
103
|
+
const route = displayField(release.routeKind ?? 'unknown route');
|
|
104
|
+
const reason = displayField(release.signature ?? 'reason not recorded');
|
|
105
|
+
const age = formatDuration(Math.max(0, now - release.at));
|
|
106
|
+
const message = release.message === undefined ? '' : ` — ${displayField(release.message)}`;
|
|
107
|
+
const diagnostic = release.diagnostic === undefined ? '' : ` · ${displayField(JSON.stringify(release.diagnostic))}`;
|
|
108
|
+
return ` · ${formatActivityId(release.attention.actIndex)} → ${displayField(participantIdentity(release.attention.recipient))} · ${route} · ${reason} · ${age}${message}${diagnostic}`;
|
|
109
|
+
}
|
|
80
110
|
export async function doctorDeliveryHealth(squarePath, graceMs, now = Date.now(), env = process.env) {
|
|
81
|
-
|
|
111
|
+
const items = await classifyDeliveryHealth(squarePath, { graceMs, now, env });
|
|
112
|
+
const releases = await readWakeReleaseDiagnostics({ location: squarePath, now, env });
|
|
113
|
+
return renderDeliveryHealth(items, releases, now);
|
|
82
114
|
}
|
|
@@ -4,6 +4,22 @@ import { nameKey } from './model.js';
|
|
|
4
4
|
import { deriveDeliveryModel } from './delivery.js';
|
|
5
5
|
import { currentSessionBindings, isWakeRouteAttemptable, presentationSuppressesWake, projectPresentationEvidence } from './square-projections.js';
|
|
6
6
|
import { retireWakeRouteFromArtifact } from './routes.js';
|
|
7
|
+
import { redactCurrentDiagnostic } from './diagnostic-redaction.js';
|
|
8
|
+
async function releaseWakeClaim(input, details) {
|
|
9
|
+
await input.hostLedger.releaseEvidence({
|
|
10
|
+
location: input.location,
|
|
11
|
+
participant: input.participant,
|
|
12
|
+
session: input.session,
|
|
13
|
+
activity: input.activity,
|
|
14
|
+
kind: 'wake',
|
|
15
|
+
claimToken: input.claimToken,
|
|
16
|
+
...(input.routeKind === undefined ? {} : { routeKind: input.routeKind }),
|
|
17
|
+
...(input.attemptN === undefined ? {} : { attemptN: input.attemptN }),
|
|
18
|
+
signature: details.signature,
|
|
19
|
+
message: redactCurrentDiagnostic(details.message),
|
|
20
|
+
...(details.diagnostic === undefined ? {} : { diagnostic: redactCurrentDiagnostic(details.diagnostic) }),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
7
23
|
async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
|
|
8
24
|
let timer;
|
|
9
25
|
const timeout = new Promise((resolve) => {
|
|
@@ -119,7 +135,10 @@ export async function deliverPending(input) {
|
|
|
119
135
|
if (lease.type === 'ambiguous') {
|
|
120
136
|
let recovered = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: lease.lease.leaseId });
|
|
121
137
|
if (recovered.status === 'busy' && recovered.record.claimToken !== undefined) {
|
|
122
|
-
await input.hostLedger
|
|
138
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken: recovered.record.claimToken, routeKind: lease.lease.routeKind ?? route.route.kind, attemptN: lease.lease.attemptN }, {
|
|
139
|
+
signature: 'stale_dispatch_claim_recovered',
|
|
140
|
+
message: 'A stale dispatch claim was released so recovery could continue.',
|
|
141
|
+
});
|
|
123
142
|
recovered = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: lease.lease.leaseId });
|
|
124
143
|
}
|
|
125
144
|
if (recovered.status === 'acquired') {
|
|
@@ -168,7 +187,10 @@ export async function deliverPending(input) {
|
|
|
168
187
|
const claimToken = claim.claimToken;
|
|
169
188
|
const dispatching = await input.hostLedger.transitionWakeDispatch({ attention, leaseId, phase: 'dispatching', leaseMs, routeKind: route.route.kind, attemptN, session: route.session });
|
|
170
189
|
if (!dispatching) {
|
|
171
|
-
await input.hostLedger
|
|
190
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
|
|
191
|
+
signature: 'dispatch_claim_transition_failed',
|
|
192
|
+
message: 'The wake dispatch claim could not enter the dispatching phase.',
|
|
193
|
+
}).catch(() => undefined);
|
|
172
194
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
173
195
|
continue;
|
|
174
196
|
}
|
|
@@ -190,7 +212,11 @@ export async function deliverPending(input) {
|
|
|
190
212
|
&& published.kind === route.route.kind
|
|
191
213
|
&& JSON.stringify(published.address) === JSON.stringify(route.route.address));
|
|
192
214
|
if (!stillPending || !stillBound || !stillPublished) {
|
|
193
|
-
await input.hostLedger
|
|
215
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
|
|
216
|
+
signature: 'pre_send_revalidation_failed',
|
|
217
|
+
message: 'Wake was not sent because the activity, session binding, or published route changed before dispatch.',
|
|
218
|
+
diagnostic: { pending: stillPending, bound: stillBound, publishedRoute: stillPublished },
|
|
219
|
+
}).catch(() => undefined);
|
|
194
220
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
195
221
|
continue;
|
|
196
222
|
}
|
|
@@ -200,7 +226,11 @@ export async function deliverPending(input) {
|
|
|
200
226
|
}
|
|
201
227
|
catch { /* capability is handled by the transport path */ }
|
|
202
228
|
if (presentationSuppressesWake(latestPresentations)) {
|
|
203
|
-
await input.hostLedger
|
|
229
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
|
|
230
|
+
signature: 'presentation_already_recorded',
|
|
231
|
+
message: 'Wake was suppressed because presentation evidence already exists.',
|
|
232
|
+
diagnostic: { outcomes: latestPresentations.map((row) => row.outcome) },
|
|
233
|
+
}).catch(() => undefined);
|
|
204
234
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
205
235
|
continue;
|
|
206
236
|
}
|
|
@@ -221,13 +251,20 @@ export async function deliverPending(input) {
|
|
|
221
251
|
outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
222
252
|
}
|
|
223
253
|
if (suppressedDuringSend) {
|
|
224
|
-
await input.hostLedger
|
|
254
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
|
|
255
|
+
signature: 'presentation_recorded_during_dispatch',
|
|
256
|
+
message: 'Wake was cancelled because presentation evidence appeared before the final send check.',
|
|
257
|
+
}).catch(() => undefined);
|
|
225
258
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
226
259
|
continue;
|
|
227
260
|
}
|
|
228
261
|
attempted += 1;
|
|
229
262
|
if (outcome.outcome === 'not-capable') {
|
|
230
|
-
await input.hostLedger
|
|
263
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
|
|
264
|
+
signature: 'transport_not_capable',
|
|
265
|
+
message: 'The wake transport reported that it could not deliver this route.',
|
|
266
|
+
diagnostic: outcome.diagnostic,
|
|
267
|
+
}).catch(() => undefined);
|
|
231
268
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
232
269
|
notCapableForAttention = true;
|
|
233
270
|
continue;
|
|
@@ -235,12 +272,17 @@ export async function deliverPending(input) {
|
|
|
235
272
|
if (outcome.outcome === 'failed' && outcome.unavailable) {
|
|
236
273
|
if (outcome.routeStale === true)
|
|
237
274
|
await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.session });
|
|
238
|
-
await input.hostLedger
|
|
275
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN: outcome.attemptN ?? attemptN }, {
|
|
276
|
+
signature: outcome.signature ?? 'transport_unavailable',
|
|
277
|
+
message: outcome.message ?? 'The wake transport was unavailable.',
|
|
278
|
+
diagnostic: outcome.diagnostic,
|
|
279
|
+
});
|
|
239
280
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
240
281
|
failedForAttention = true;
|
|
241
282
|
continue;
|
|
242
283
|
}
|
|
243
|
-
|
|
284
|
+
const safeOutcome = redactCurrentDiagnostic(outcome);
|
|
285
|
+
await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: safeOutcome.outcome, routeKind: route.route.kind, attemptN: safeOutcome.attemptN ?? attemptN, ...(safeOutcome.signature === undefined ? {} : { signature: safeOutcome.signature }), ...(safeOutcome.outcome !== 'accepted' && safeOutcome.message !== undefined ? { message: safeOutcome.message } : {}), ...(safeOutcome.outcome !== 'accepted' && safeOutcome.diagnostic !== undefined ? { diagnostic: safeOutcome.diagnostic } : {}), claimToken });
|
|
244
286
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
245
287
|
if (outcome.outcome === 'accepted') {
|
|
246
288
|
acceptedForAttention = true;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const MAX_STRING_LENGTH = 1_000;
|
|
2
|
+
const MAX_COLLECTION_ITEMS = 32;
|
|
3
|
+
const MAX_NESTING = 8;
|
|
4
|
+
function redactText(value, secret) {
|
|
5
|
+
const withoutSecret = secret?.trim() ? value.split(secret.trim()).join('[redacted]') : value;
|
|
6
|
+
const withoutUrlPassword = withoutSecret.replace(/([?&]password=)[^&\s]+/gi, '$1[redacted]');
|
|
7
|
+
return [...withoutUrlPassword].length <= MAX_STRING_LENGTH
|
|
8
|
+
? withoutUrlPassword
|
|
9
|
+
: `${[...withoutUrlPassword].slice(0, MAX_STRING_LENGTH - 1).join('')}…`;
|
|
10
|
+
}
|
|
11
|
+
function redactValue(value, secret, seen, depth) {
|
|
12
|
+
if (typeof value === 'string')
|
|
13
|
+
return redactText(value, secret);
|
|
14
|
+
if (depth >= MAX_NESTING)
|
|
15
|
+
return '[truncated]';
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
if (seen.has(value))
|
|
18
|
+
return '[circular]';
|
|
19
|
+
seen.add(value);
|
|
20
|
+
return value.slice(0, MAX_COLLECTION_ITEMS).map((item) => redactValue(item, secret, seen, depth + 1));
|
|
21
|
+
}
|
|
22
|
+
if (value !== null && typeof value === 'object') {
|
|
23
|
+
if (seen.has(value))
|
|
24
|
+
return '[circular]';
|
|
25
|
+
seen.add(value);
|
|
26
|
+
return Object.fromEntries(Object.entries(value).slice(0, MAX_COLLECTION_ITEMS).map(([key, item]) => [
|
|
27
|
+
key,
|
|
28
|
+
/password|secret|api[-_]?key|token/i.test(key) && typeof item !== 'boolean'
|
|
29
|
+
? '[redacted]'
|
|
30
|
+
: redactValue(item, secret, seen, depth + 1),
|
|
31
|
+
]));
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
export function redactDiagnostic(value, secret) {
|
|
36
|
+
return redactValue(value, secret, new WeakSet(), 0);
|
|
37
|
+
}
|
|
38
|
+
export function redactCurrentDiagnostic(value, env = process.env) {
|
|
39
|
+
return redactDiagnostic(value, env.PASEO_PASSWORD);
|
|
40
|
+
}
|
package/dist/help.js
CHANGED
|
@@ -66,7 +66,7 @@ const COMMANDS = [
|
|
|
66
66
|
{
|
|
67
67
|
names: ['harness'], usage: 'harness doctor [claude|codex|opencode|pi|delivery]', usesSquare: true, group: 'maintenance', hiddenFromIndex: true,
|
|
68
68
|
summary: 'Diagnose installed agent-host support.',
|
|
69
|
-
details: ['Targets:', ' claude, codex, opencode, pi Diagnose one installed adapter.', ' delivery Diagnose delivery for the selected square.'],
|
|
69
|
+
details: ['Targets:', ' claude, codex, opencode, pi Diagnose one installed adapter.', ' delivery Diagnose pending delivery and recent wake-release reasons for the selected square.'],
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
names: ['doctor'], usage: 'doctor', usesSquare: true, group: 'maintenance',
|
|
@@ -154,11 +154,11 @@ export class FileHostLedgerPort {
|
|
|
154
154
|
return { status: 'degraded', error };
|
|
155
155
|
} }
|
|
156
156
|
async releaseEvidence(i) { const f = this.file('user', 'evidence'), location = await canon(i.location), at = i.now ?? this.clock(); await withFileLock(f + '.lock', LOCK, async () => { const all = await read(f, at, true), same = (x) => x.kind === i.kind && x.location === location && nameKey(x.participant) === nameKey(i.participant) && x.activity === i.activity && x.session === i.session, current = all.findLast(x => same(x) && x.outcome === 'dispatching'); if (current === undefined || current.claimToken !== i.claimToken)
|
|
157
|
-
return; const tombstone = { ...current, outcome: 'released', at, expiresAt: undefined }; delete tombstone.expiresAt; await write(f, [...all.filter(x => x !== current), tombstone]); }); }
|
|
157
|
+
return; const tombstone = { ...current, outcome: 'released', at, ...(i.routeKind === undefined ? {} : { routeKind: i.routeKind }), ...(i.attemptN === undefined ? {} : { attemptN: i.attemptN }), ...(i.signature === undefined ? {} : { signature: i.signature }), ...(i.message === undefined ? {} : { message: i.message }), ...(i.diagnostic === undefined ? {} : { diagnostic: i.diagnostic }), expiresAt: undefined }; delete tombstone.expiresAt; await write(f, [...all.filter(x => x !== current), tombstone]); }); }
|
|
158
158
|
async appendEvidence(i) { const f = this.file('user', 'evidence'), r = { ...i, location: await canon(i.location), at: i.at ?? this.clock(), v: 1 }; const token = i.claimToken; if (typeof token !== 'string' || token.length === 0)
|
|
159
159
|
return; await withFileLock(f + '.lock', LOCK, async () => { const all = await read(f, r.at, true); const same = (x) => x.kind === r.kind && x.location === r.location && nameKey(x.participant) === nameKey(r.participant) && x.activity === r.activity && x.session === r.session; const current = all.findLast(x => same(x) && x.outcome === 'dispatching'); if (current === undefined || current.claimToken !== token)
|
|
160
160
|
return; const terminalKey = (x) => same(x) && x.attemptN === r.attemptN && x.outcome !== 'dispatching' && x.outcome !== 'released'; await write(f, [...all.filter(x => x !== current && !terminalKey(x)), r]); }); }
|
|
161
|
-
async listEvidence(i = {}) { const loc = i.location === undefined ? undefined : await canon(i.location); return (await read(this.file('user', 'evidence'), i.now ?? this.clock())).filter(r => r.outcome !== 'released' && (!loc || r.location === loc) && (!i.participant || nameKey(r.participant) === nameKey(i.participant)) && (!i.session || r.session === i.session) && (!i.activity || r.activity === i.activity) && (!i.kind || r.kind === i.kind)); }
|
|
161
|
+
async listEvidence(i = {}) { const loc = i.location === undefined ? undefined : await canon(i.location); return (await read(this.file('user', 'evidence'), i.now ?? this.clock())).filter(r => (i.includeReleased === true || r.outcome !== 'released') && (!loc || r.location === loc) && (!i.participant || nameKey(r.participant) === nameKey(i.participant)) && (!i.session || r.session === i.session) && (!i.activity || r.activity === i.activity) && (!i.kind || r.kind === i.kind)); }
|
|
162
162
|
async gcEvidence(i) { const f = this.file('user', 'evidence'), pending = new Set(i.pendingWakeActivities ?? []); await withFileLock(f + '.lock', LOCK, async () => write(f, (await read(f, this.clock(), true)).filter(r => r.at >= i.before || r.kind === 'wake' && r.outcome === 'accepted' && pending.has(r.activity)))); }
|
|
163
163
|
async reconcileBinding(i = {}) { let b = []; try {
|
|
164
164
|
b = [...await this.listPresence({ scopes: i.scopes, now: i.now })];
|
package/dist/host-ledger.d.ts
CHANGED
|
@@ -65,9 +65,15 @@ export interface EvidenceClaim {
|
|
|
65
65
|
readonly claimToken?: string;
|
|
66
66
|
readonly now?: number;
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
/** Release metadata and includeReleased are diagnostic-only; default evidence reads stay behavior-safe. */
|
|
69
|
+
export interface EvidenceRelease extends Omit<EvidenceClaim, 'leaseMs'> {
|
|
69
70
|
readonly claimToken: string;
|
|
70
|
-
|
|
71
|
+
readonly routeKind?: WakeRouteKind;
|
|
72
|
+
readonly attemptN?: number;
|
|
73
|
+
readonly signature?: string;
|
|
74
|
+
readonly message?: string;
|
|
75
|
+
readonly diagnostic?: unknown;
|
|
76
|
+
}
|
|
71
77
|
export interface EvidenceLookup {
|
|
72
78
|
readonly location?: string;
|
|
73
79
|
readonly participant?: string;
|
|
@@ -75,6 +81,7 @@ export interface EvidenceLookup {
|
|
|
75
81
|
readonly activity?: string;
|
|
76
82
|
readonly kind?: EvidenceRecord['kind'];
|
|
77
83
|
readonly now?: number;
|
|
84
|
+
readonly includeReleased?: boolean;
|
|
78
85
|
}
|
|
79
86
|
export interface EvidenceGc {
|
|
80
87
|
readonly before: number;
|