@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/dist/runtime.js CHANGED
@@ -184,6 +184,43 @@ export function readCursor(squareState, name, landed = replayLandedAudiences(squ
184
184
  export function latestActIndex(acts) {
185
185
  return acts.reduce((max, act) => Math.max(max, act.index), -1);
186
186
  }
187
+ /**
188
+ * "Was here" is a claim of presence, so it rides on evidence: the participant's own acts
189
+ * and seen receipts after their latest join. Anything else — in particular acts merely
190
+ * not addressed to them — says nothing about where they actually were.
191
+ */
192
+ export function presenceEvidenceCursor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
193
+ const recipient = landed.resolveParticipant(name) ?? name;
194
+ const boundary = landed.lastJoinIndex(recipient) ?? -1;
195
+ if (boundary < 0)
196
+ return -1;
197
+ let evidence = boundary;
198
+ for (const act of squareState.acts) {
199
+ if (act.index <= boundary || act.index <= evidence || act.actor === undefined)
200
+ continue;
201
+ if (sameName(act.actor, recipient)
202
+ || squareState.runtime.observations?.[recipient]?.[formatActivityId(act.index)]?.state === 'seen') {
203
+ evidence = act.index;
204
+ }
205
+ }
206
+ return evidence;
207
+ }
208
+ /** The last public act the participant was verifiably present for, or -1 when there is no evidence. */
209
+ export function presenceAnchor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
210
+ const recipient = landed.resolveParticipant(name) ?? name;
211
+ const boundary = landed.lastJoinIndex(recipient) ?? -1;
212
+ if (boundary < 0)
213
+ return -1;
214
+ const evidence = presenceEvidenceCursor(squareState, recipient, landed);
215
+ for (let i = squareState.acts.length - 1; i >= 0; i--) {
216
+ const event = squareState.acts[i];
217
+ if (event.index <= boundary || event.index > evidence)
218
+ continue;
219
+ if (event.kind === 'say' || event.kind === 'done')
220
+ return event.index;
221
+ }
222
+ return -1;
223
+ }
187
224
  export function freshWatchLease(squareState, name, at = Date.now()) {
188
225
  const key = canonicalRuntimeName(squareState, name);
189
226
  const lease = watchLease(squareState, key);
@@ -29,7 +29,7 @@ export async function catchUp(square, name, options = {}, project) {
29
29
  const deadline = Date.now() + idle;
30
30
  while (true) {
31
31
  if (!await assertLiveOwner(square, name)) {
32
- throw new SquareError('already_joined', `${participantIdentity(name)} is already bound to another session`);
32
+ throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`);
33
33
  }
34
34
  const attempt = await square.artifact.transact((state, version) => {
35
35
  const decision = decideCatch(state, name, options, square.clock(), project);
@@ -192,7 +192,7 @@ export async function takeover(square, name, _oldSessionIds = []) {
192
192
  return committed;
193
193
  });
194
194
  if (outcome.status === 'busy')
195
- throw new SquareError('already_joined', `${participantIdentity(name)} is already bound to another session`);
195
+ throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`);
196
196
  return { name: outcome.result.name, activities: outcome.result.stored.map(exposeActivity), epoch: outcome.epoch };
197
197
  }
198
198
  const committed = await commitLifecycle();
@@ -227,7 +227,7 @@ export async function implicitJoin(square, name) {
227
227
  }
228
228
  export async function express(square, name, body, options = {}) {
229
229
  if (!await assertLiveOwner(square, name)) {
230
- throw new SquareError('already_joined', `${participantIdentity(name)} is already bound to another session`);
230
+ throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`);
231
231
  }
232
232
  const now = square.clock();
233
233
  const reply = options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
@@ -280,7 +280,7 @@ async function landCore(square, verb, actor, body = '', fence = {}) {
280
280
  // be completed by a stale done — an old expected epoch refuses instead of appending.
281
281
  const commit = async () => {
282
282
  if (verb === 'done' && !await assertLiveOwner(square, actor, fence.expectedEpoch)) {
283
- throw new SquareError('already_done', `${participantIdentity(actor)} is already bound to another session`);
283
+ throw new SquareError('already_done', `✕ ${participantIdentity(actor)} already stands here — another session holds the name`);
284
284
  }
285
285
  const now = square.clock();
286
286
  return square.artifact.transact((state) => {
package/dist/stream.js CHANGED
@@ -52,6 +52,6 @@ export async function cmdStreamNdjson(squarePath, recipient, start = { kind: 'ta
52
52
  }
53
53
  export async function cmdStream(squarePath) {
54
54
  process.stderr.write('✕ interactive stream was removed\n');
55
- process.stderr.write(`» square --location ${quoteShell(path.resolve(squarePath))} stream --ndjson\n`);
55
+ process.stderr.write(`square --location ${quoteShell(path.resolve(squarePath))} stream --ndjson\n`);
56
56
  process.exitCode = 2;
57
57
  }
@@ -0,0 +1,7 @@
1
+ export type StyleRole = 'blocked' | 'release' | 'changed' | 'dim' | 'match';
2
+ export declare function style(role: StyleRole, text: string, opts?: {
3
+ stream?: {
4
+ isTTY?: boolean;
5
+ };
6
+ env?: NodeJS.ProcessEnv;
7
+ }): string;
@@ -0,0 +1,13 @@
1
+ const PALETTE = {
2
+ blocked: '\x1b[38;5;203m',
3
+ release: '\x1b[38;5;114m',
4
+ changed: '\x1b[38;5;179m',
5
+ dim: '\x1b[38;5;244m',
6
+ match: '\x1b[38;5;222m\x1b[1m',
7
+ };
8
+ export function style(role, text, opts = {}) {
9
+ if (!(opts.stream ?? process.stdout).isTTY || (opts.env ?? process.env).NO_COLOR !== undefined || text === '') {
10
+ return text;
11
+ }
12
+ return `${PALETTE[role]}${text}\x1b[0m`;
13
+ }
package/dist/views.js CHANGED
@@ -1,9 +1,9 @@
1
- import { formatActivityId, parseActivityId } from './square-core.js';
1
+ import { formatActivityId, parseActivityId, replayLandedAudiences } from './square-core.js';
2
2
  import { deliveryDelta, directedPeerSays } from './activity-feed.js';
3
3
  import { coreActivities, coreParticipants, coreStatus, resolveKnownName } from './decisions.js';
4
4
  import { deriveDeliveryModel, isActivitySeen } from './delivery.js';
5
5
  import { SquareError, nameKey } from './model.js';
6
- import { countSays, currentHold, foldedState, freshWatchLease, inSquareCount, isCurrentlyJoined, resolveRosterName, rosterNames, watchTerminalStatus } from './runtime.js';
6
+ import { countSays, currentHold, foldedState, freshWatchLease, inSquareCount, isCurrentlyJoined, presenceAnchor, resolveRosterName, rosterNames, watchTerminalStatus } from './runtime.js';
7
7
  function expose(stored) {
8
8
  if (stored.kind === 'read' || stored.actor === undefined)
9
9
  throw new Error(`Cannot expose stored activity ${formatActivityId(stored.index)}`);
@@ -40,10 +40,11 @@ function statuses(square, state) {
40
40
  }
41
41
  function anchors(state, delivery) {
42
42
  const result = {};
43
+ const landed = replayLandedAudiences(state.acts);
43
44
  for (const name of delivery.participants()) {
44
- const activity = state.acts.findLast((candidate) => candidate.index <= delivery.cursorFor(name) && (candidate.kind === 'say' || candidate.kind === 'done'));
45
- if (activity !== undefined)
46
- result[activity.index] = [...(result[activity.index] ?? []), name];
45
+ const anchor = presenceAnchor(state, name, landed);
46
+ if (anchor >= 0)
47
+ result[anchor] = [...(result[anchor] ?? []), name];
47
48
  }
48
49
  return result;
49
50
  }
@@ -7,6 +7,16 @@ export interface WakeAttention {
7
7
  actIndex: number;
8
8
  recipient: string;
9
9
  }
10
+ export interface WakeReleaseDiagnostic {
11
+ readonly at: number;
12
+ readonly attention: WakeAttention;
13
+ readonly routeKind?: WakeRouteKind;
14
+ readonly attemptN?: number;
15
+ readonly session?: string;
16
+ readonly signature?: string;
17
+ readonly message?: string;
18
+ readonly diagnostic?: unknown;
19
+ }
10
20
  export type WakeAttempt = ProjectedWakeAttempt;
11
21
  export interface WakeDispatchLease {
12
22
  readonly leaseId: string;
@@ -36,6 +46,14 @@ export declare function readWakeAttempts(opts?: {
36
46
  now?: number;
37
47
  env?: NodeJS.ProcessEnv;
38
48
  }): Promise<WakeAttempt[]>;
49
+ export declare function redactWakeDiagnostic(value: unknown, env?: NodeJS.ProcessEnv): unknown;
50
+ export declare function readWakeReleaseDiagnostics(opts?: {
51
+ location?: string;
52
+ attention?: WakeAttention;
53
+ sessionId?: string;
54
+ now?: number;
55
+ env?: NodeJS.ProcessEnv;
56
+ }): Promise<WakeReleaseDiagnostic[]>;
39
57
  export declare function recordWakeAttempt(attempt: Omit<WakeAttempt, 'at'> & {
40
58
  at?: number;
41
59
  }, env?: NodeJS.ProcessEnv): Promise<WakeAttempt>;
@@ -5,6 +5,7 @@ import { canonicalSquarePath } from './registry.js';
5
5
  import { formatActivityId, parseActivityId } from './square-core.js';
6
6
  import { createHostLedgerPort } from './host-ledger-file-adapter.js';
7
7
  import { terminalWakeEvidence } from './square-projections.js';
8
+ import { redactDiagnostic } from './diagnostic-redaction.js';
8
9
  export { hasAttemptableWakeRoute, isWakeRouteAttemptable, terminalWakeEvidence } from './square-projections.js';
9
10
  const VALID_OUTCOMES = new Set(['accepted', 'unknown', 'failed']);
10
11
  function ledger(env) {
@@ -44,17 +45,43 @@ export async function readWakeAttempts(opts = {}) {
44
45
  const keys = await Promise.all(scoped.map((attempt) => wakeAttentionKey(attempt.attention)));
45
46
  return scoped.filter((_, index) => keys[index] === expected);
46
47
  }
47
- function redact(value, secret) {
48
- if (typeof value === 'string') {
49
- const withoutKnownSecret = secret ? value.split(secret).join('[redacted]') : value;
50
- return withoutKnownSecret.replace(/([?&]password=)[^&\s]+/gi, '$1[redacted]');
51
- }
52
- if (Array.isArray(value))
53
- return value.map((item) => redact(item, secret));
54
- if (value !== null && typeof value === 'object') {
55
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redact(item, secret)]));
48
+ export function redactWakeDiagnostic(value, env = process.env) {
49
+ return redactDiagnostic(value, env.PASEO_PASSWORD);
50
+ }
51
+ export async function readWakeReleaseDiagnostics(opts = {}) {
52
+ const now = opts.now ?? Date.now();
53
+ const env = opts.env ?? process.env;
54
+ const rows = await ledger(env).listEvidence({
55
+ kind: 'wake',
56
+ location: opts.location ?? opts.attention?.squarePath,
57
+ includeReleased: true,
58
+ now,
59
+ });
60
+ const expected = opts.attention === undefined ? undefined : await wakeAttentionKey(opts.attention);
61
+ const releases = [];
62
+ for (const row of rows) {
63
+ if (row.outcome !== 'released')
64
+ continue;
65
+ const actIndex = parseActivityId(row.activity);
66
+ if (actIndex === undefined || row.at === undefined)
67
+ continue;
68
+ if (opts.sessionId !== undefined && row.session !== opts.sessionId)
69
+ continue;
70
+ const attention = { squarePath: row.location, recipient: row.participant, actIndex };
71
+ if (expected !== undefined && JSON.stringify([row.location, formatActivityId(actIndex), nameKey(row.participant)]) !== expected)
72
+ continue;
73
+ releases.push({
74
+ at: row.at,
75
+ attention,
76
+ ...(row.routeKind === undefined ? {} : { routeKind: row.routeKind }),
77
+ ...(row.attemptN === undefined ? {} : { attemptN: row.attemptN }),
78
+ ...(row.session === undefined ? {} : { session: row.session }),
79
+ ...(row.signature === undefined ? {} : { signature: row.signature }),
80
+ ...(row.message === undefined ? {} : { message: redactWakeDiagnostic(row.message, env) }),
81
+ ...(row.diagnostic === undefined ? {} : { diagnostic: redactWakeDiagnostic(row.diagnostic, env) }),
82
+ });
56
83
  }
57
- return value;
84
+ return releases.sort((left, right) => right.at - left.at);
58
85
  }
59
86
  export async function recordWakeAttempt(attempt, env = process.env) {
60
87
  const value = { ...attempt, at: attempt.at ?? Date.now() };
@@ -63,7 +90,7 @@ export async function recordWakeAttempt(attempt, env = process.env) {
63
90
  if (value.outcome !== 'accepted' && !value.signature) {
64
91
  throw new Error(`${value.outcome} wake attempts require a transport signature.`);
65
92
  }
66
- const safe = redact(value, env.PASEO_PASSWORD);
93
+ const safe = redactWakeDiagnostic(value, env);
67
94
  const session = safe.session ?? safe.signature ?? `route:${safe.routeKind}`;
68
95
  const claim = await ledger(env).claimEvidence({ location: safe.attention.squarePath, participant: safe.attention.recipient, session, activity: formatActivityId(safe.attention.actIndex), kind: 'wake', leaseMs: 5000 });
69
96
  if (claim.status === 'acquired')
package/dist/watch.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
2
  import { isSquareError, } from './model.js';
3
- import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, nowMs, } from './runtime.js';
3
+ import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, inSquareCount, nowMs, } from './runtime.js';
4
4
  import { openSquare } from './square-file-adapter.js';
5
5
  import { closeOpenSquare } from './open-square.js';
6
6
  import { openParticipant } from './square-wiring.js';
7
7
  import { resolveParticipant, watchPresentation } from './views.js';
8
8
  import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
9
- import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchReplaceMissing, renderWatchOutput, renderWatchReplaced, renderWatchStatus, participantCommandPrefix, joinRecoveryCommand, participantsRecoveryCommand, withPathOutput, } from './presentation.js';
9
+ import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchReplaceMissing, renderWatchOutput, renderWatchReplaced, renderWatchStatus, formatRefusal, participantCommandPrefix, joinRecoveryCommand, participantsRecoveryCommand, withPathOutput, } from './presentation.js';
10
10
  import { hasAutomaticDeliveryIdentity } from './registry.js';
11
11
  import { parseActivityId } from './square-core.js';
12
12
  function watchStatusExitCode(status) {
@@ -37,7 +37,7 @@ function watchOutputResult(squarePath, presentation, name, caught, opts = {}) {
37
37
  showCatchHint: !hasAutomaticDeliveryIdentity(),
38
38
  perceptions,
39
39
  }) + (caught.remaining > 0
40
- ? `\n○ ${caught.remaining} matching ${caught.remaining === 1 ? 'activity remains' : 'activities remain'}\n» ${catchContinuationCommand(squarePath, name, opts)}`
40
+ ? `\n○ ${caught.remaining} matching ${caught.remaining === 1 ? 'activity remains' : 'activities remain'}\n${catchContinuationCommand(squarePath, name, opts)}`
41
41
  : ''),
42
42
  remaining: caught.remaining,
43
43
  ...(opts.status ? { status: opts.status } : {}),
@@ -61,7 +61,7 @@ function writeWatchOutput(squarePath, name, presentation, stdout, remaining, sta
61
61
  return;
62
62
  }
63
63
  const fallback = showCatchHint && remaining === 0
64
- ? `» ${participantCommandPrefix(squarePath, name)} catch --idle 30m\n stay available for new activity`
64
+ ? `${participantCommandPrefix(squarePath, name)} catch --idle 30m\n stay available for new activity`
65
65
  : '';
66
66
  process.stdout.write(withPathOutput(squarePath, [stdout.trimEnd(), fallback].filter(Boolean).join('\n\n').trimEnd(), headerOpts));
67
67
  }
@@ -109,7 +109,7 @@ function installWatchInterruptHandler(square, squarePath, name, currentLeaseId)
109
109
  const onInterrupt = () => {
110
110
  void (async () => {
111
111
  await endWatch(square, name, currentLeaseId());
112
- process.stdout.write(withPathOutput(squarePath, '✕ catch stopped'));
112
+ process.stdout.write(withPathOutput(squarePath, '○ you step back — catch stopped'));
113
113
  process.exit(130);
114
114
  })().catch((error) => {
115
115
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
@@ -144,19 +144,30 @@ async function cmdWatchNow(squarePath, name, opts) {
144
144
  await closeOpenSquare(square);
145
145
  }
146
146
  }
147
+ async function countParticipants(square) {
148
+ try {
149
+ return inSquareCount((await square.artifact.read()).state);
150
+ }
151
+ catch {
152
+ return undefined;
153
+ }
154
+ }
147
155
  /** `false` is reserved for a quiet --now; idle completion preserves its existing sweep boundary. */
148
156
  export async function cmdWatch(squarePath, name, opts) {
149
157
  let square;
158
+ let participantCount;
150
159
  try {
151
160
  square = await openSquare(squarePath, { clock: nowMs });
161
+ participantCount = inSquareCount((await square.artifact.read()).state);
152
162
  name = (await resolveParticipant(square, name)).name;
153
163
  }
154
164
  catch (err) {
155
165
  if (isSquareError(err)) {
156
- process.stderr.write(err.message + '\n');
166
+ const bodyLines = [err.message];
157
167
  // The caller's own name has never joined; only a join admits it.
158
- if (/^Unknown participant/.test(err.message))
159
- process.stderr.write(`${joinRecoveryCommand(squarePath, name)}\n`);
168
+ if (/^✕ .* has never stepped into/.test(err.message))
169
+ bodyLines.push(joinRecoveryCommand(squarePath, name));
170
+ process.stderr.write(formatRefusal(squarePath, bodyLines, participantCount === undefined ? {} : { participantCount }));
160
171
  process.exit(err.code === 'not_found' ? 1 : 2);
161
172
  }
162
173
  throw err;
@@ -169,12 +180,14 @@ export async function cmdWatch(squarePath, name, opts) {
169
180
  }
170
181
  }
171
182
  catch (err) {
183
+ const participantCount = await countParticipants(square);
172
184
  await closeOpenSquare(square).catch(() => undefined);
173
185
  if (isSquareError(err)) {
174
- process.stderr.write(err.message + '\n');
186
+ const bodyLines = [err.message];
175
187
  // A filter target is unknown, not the caller; the roster is the useful next read.
176
- if (/^(Unknown participant|Unknown mention target)/.test(err.message))
177
- process.stderr.write(`${participantsRecoveryCommand(squarePath)}\n`);
188
+ if (/^✕ .*(has never stepped into|is not standing in this square)/.test(err.message))
189
+ bodyLines.push(participantsRecoveryCommand(squarePath));
190
+ process.stderr.write(formatRefusal(squarePath, bodyLines, participantCount === undefined ? {} : { participantCount }));
178
191
  process.exit(err.code === 'not_found' ? 1 : 2);
179
192
  }
180
193
  throw err;
@@ -31,12 +31,20 @@ export function renderPiInbox(inbox) {
31
31
 
32
32
  const NOTIFY_BODY_MAX = 80;
33
33
 
34
+ function framePiMessage(content) {
35
+ return `\n${content.replace(/^\n+/, '').replace(/\n+$/, '')}\n`;
36
+ }
37
+
34
38
  export function summarizePendingForNotify(pending) {
35
39
  const lines = pending.flatMap((membership) => membership.notifications.map((note) => {
36
- const verb = note.route === 'bell' ? 'rang the bell for' : note.route === 'mention' ? 'walked over to' : 'paid attention to';
40
+ const verb = note.route === 'bell'
41
+ ? 'rang the bell'
42
+ : note.route === 'mention'
43
+ ? `called your name (${membership.name})`
44
+ : `spoke within your earshot (${membership.name})`;
37
45
  const body = note.body.replace(/\s+/g, ' ').trim();
38
46
  const clipped = body.length > NOTIFY_BODY_MAX ? `${body.slice(0, NOTIFY_BODY_MAX).trimEnd()}…` : body;
39
- return `${note.actor} ${verb} ${membership.name}${clipped ? ` — ${clipped}` : ''}`;
47
+ return `${note.actor} ${verb}${clipped ? ` — ${clipped}` : ''}`;
40
48
  }));
41
49
  if (lines.length === 0) return undefined;
42
50
  return lines.length === 1 ? `■ square · ${lines[0]}` : `■ square · ${lines[0]} (+${lines.length - 1} more)`;
@@ -203,10 +211,11 @@ export default function squarePiExtension(pi) {
203
211
  // reaches the model; a brief notify tells the human what just landed.
204
212
  const summary = summarizePendingForNotify(pending);
205
213
  if (summary) ui?.notify?.(summary, 'info');
206
- const landing = waitForLanding(content, signal, 'steer');
214
+ const messageContent = framePiMessage(content);
215
+ const landing = waitForLanding(messageContent, signal, 'steer');
207
216
  try {
208
217
  Promise.resolve(pi.sendMessage(
209
- { customType: 'square', content, display: false },
218
+ { customType: 'square', content: messageContent, display: false },
210
219
  { deliverAs: 'steer', triggerTurn: true },
211
220
  )).catch((error) => landing.ack.settle(error));
212
221
  } catch (error) {
@@ -266,10 +275,11 @@ export default function squarePiExtension(pi) {
266
275
  const signal = watcherAbort.signal;
267
276
  void automaticSessionStart('pi', sessionId, sessionCwd, sessionEnv(sessionId)).then((context) => {
268
277
  if (context === undefined || sessionId === undefined || token !== generation) return;
269
- const landing = waitForLanding(context, signal, 'nextTurn');
278
+ const messageContent = framePiMessage(context);
279
+ const landing = waitForLanding(messageContent, signal, 'nextTurn');
270
280
  try {
271
281
  Promise.resolve(pi.sendMessage(
272
- { customType: 'square', content: context, display: true },
282
+ { customType: 'square', content: messageContent, display: true },
273
283
  { deliverAs: 'nextTurn' },
274
284
  )).catch((error) => landing.ack.settle(error));
275
285
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.57",
3
+ "version": "0.3.61",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,8 +21,8 @@
21
21
  "sync-manifests": "node scripts/sync-manifests.mjs",
22
22
  "release": "npm version patch && npm run sync-manifests -- --commit && npm publish && git push --follow-tags",
23
23
  "prepublishOnly": "npm run sync-manifests && npm run build && npm run verify-release",
24
- "test": "npm run build && node --test test/*.test.js",
25
- "test:cli-process": "npm run build && node --test test/process/square-cli-render.test.js"
24
+ "test": "npm run build && node --import ./test/sandbox-env.js --test test/*.test.js",
25
+ "test:cli-process": "npm run build && node --import ./test/sandbox-env.js --test test/process/square-cli-render.test.js"
26
26
  },
27
27
  "keywords": [
28
28
  "brainstorm",