@astrosheep/square 0.3.58 → 0.3.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/skills/square/SKILL.md +5 -7
  3. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  4. package/dist/activity.d.ts +6 -2
  5. package/dist/activity.js +170 -133
  6. package/dist/artifact.d.ts +4 -0
  7. package/dist/artifact.js +43 -14
  8. package/dist/attention-presentation.d.ts +1 -0
  9. package/dist/attention-presentation.js +21 -19
  10. package/dist/automatic-session.js +16 -26
  11. package/dist/boundary-presentation.d.ts +1 -2
  12. package/dist/boundary-presentation.js +4 -19
  13. package/dist/catch-decisions.js +1 -1
  14. package/dist/cli/context.d.ts +2 -0
  15. package/dist/cli/context.js +17 -33
  16. package/dist/cli/harness-command.js +1 -1
  17. package/dist/cli/mcp-server-command.d.ts +2 -0
  18. package/dist/cli/mcp-server-command.js +13 -0
  19. package/dist/cli/observation-commands.js +132 -64
  20. package/dist/cli/program.js +36 -9
  21. package/dist/cli/registry.js +2 -0
  22. package/dist/cli/square-commands.d.ts +1 -1
  23. package/dist/cli/square-commands.js +103 -133
  24. package/dist/decisions.d.ts +4 -0
  25. package/dist/decisions.js +82 -34
  26. package/dist/delivery-operations.d.ts +1 -2
  27. package/dist/delivery-operations.js +1 -4
  28. package/dist/file-changes.d.ts +12 -0
  29. package/dist/file-changes.js +235 -0
  30. package/dist/help.js +4 -8
  31. package/dist/host-ledger-file-adapter.d.ts +15 -13
  32. package/dist/host-ledger-file-adapter.js +131 -79
  33. package/dist/host-ledger-root.d.ts +2 -0
  34. package/dist/host-ledger-root.js +9 -0
  35. package/dist/host-ledger.d.ts +4 -24
  36. package/dist/inbox.d.ts +10 -5
  37. package/dist/inbox.js +152 -67
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.js +1 -0
  40. package/dist/list.js +12 -9
  41. package/dist/mcp.d.ts +7 -0
  42. package/dist/mcp.js +122 -0
  43. package/dist/mention-parse.d.ts +8 -0
  44. package/dist/mention-parse.js +107 -0
  45. package/dist/model.d.ts +5 -0
  46. package/dist/model.js +4 -1
  47. package/dist/notifications.d.ts +1 -0
  48. package/dist/notifications.js +25 -20
  49. package/dist/participant-host.js +2 -2
  50. package/dist/ports.d.ts +2 -9
  51. package/dist/presentation.d.ts +19 -1
  52. package/dist/presentation.js +111 -73
  53. package/dist/presented.js +2 -1
  54. package/dist/registry.d.ts +8 -4
  55. package/dist/registry.js +43 -76
  56. package/dist/routes.d.ts +19 -2
  57. package/dist/routes.js +48 -5
  58. package/dist/runtime.d.ts +10 -0
  59. package/dist/runtime.js +39 -0
  60. package/dist/square-actions.d.ts +26 -13
  61. package/dist/square-actions.js +203 -87
  62. package/dist/square-application.d.ts +58 -0
  63. package/dist/square-application.js +183 -0
  64. package/dist/square-facade.d.ts +24 -9
  65. package/dist/square-file-adapter.d.ts +5 -12
  66. package/dist/square-file-adapter.js +12 -64
  67. package/dist/square-projections.d.ts +3 -2
  68. package/dist/square-projections.js +12 -3
  69. package/dist/square-storage.js +27 -34
  70. package/dist/square-wiring.d.ts +12 -7
  71. package/dist/square-wiring.js +36 -32
  72. package/dist/state-cell.d.ts +1 -1
  73. package/dist/stream.js +31 -27
  74. package/dist/tty-style.d.ts +7 -0
  75. package/dist/tty-style.js +13 -0
  76. package/dist/views.d.ts +3 -3
  77. package/dist/views.js +10 -9
  78. package/dist/wake-attempts.js +2 -1
  79. package/dist/wake-evidence.js +2 -8
  80. package/dist/wakes.d.ts +1 -0
  81. package/dist/wakes.js +5 -5
  82. package/dist/watch.d.ts +6 -1
  83. package/dist/watch.js +43 -28
  84. package/extensions/square-pi.js +107 -109
  85. package/guides/application-boundary.md +32 -0
  86. package/guides/change-waits.md +28 -0
  87. package/guides/host-ledger.md +9 -0
  88. package/guides/mcp.md +9 -0
  89. package/guides/operation-cancellation.md +17 -0
  90. package/package.json +10 -4
  91. package/skills/square/SKILL.md +5 -7
@@ -1,11 +1,10 @@
1
1
  import { closeOpenSquare } from './open-square.js';
2
2
  import { buildMemorySquare, buildSquare, openSquare } from './square-file-adapter.js';
3
- import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, resume, } from './square-actions.js';
3
+ import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, resume, retireEndedSessionRoutes, } from './square-actions.js';
4
4
  import { markBoundarySeen as recordBoundarySeen } from './presence.js';
5
5
  import { history, participantHistory, participants, resolveParticipant, snapshot } from './views.js';
6
6
  import { currentParticipant } from './views.js';
7
7
  import { projectSessionBindings } from './square-projections.js';
8
- import { reconcileBinding as reconcileBindingOperation } from './delivery-operations.js';
9
8
  class ParticipantHandle {
10
9
  name;
11
10
  square;
@@ -15,32 +14,32 @@ class ParticipantHandle {
15
14
  this.square = square;
16
15
  this.context = context;
17
16
  }
18
- express(body, options) {
19
- return express(this.context, this.name, body, options);
17
+ express(body, options, control) {
18
+ return express(this.context, this.name, body, options, control);
20
19
  }
21
- listen(target) {
22
- return listen(this.context, this.name, target);
20
+ listen(target, control) {
21
+ return listen(this.context, this.name, target, control);
23
22
  }
24
- ignore(target) {
25
- return ignore(this.context, this.name, target);
23
+ ignore(target, control) {
24
+ return ignore(this.context, this.name, target, control);
26
25
  }
27
- listening() {
28
- return listening(this.context, this.name);
26
+ listening(control) {
27
+ return listening(this.context, this.name, control);
29
28
  }
30
- catch(options) {
31
- return catchUp(this.context, this.name, options);
29
+ catch(options, control) {
30
+ return catchUp(this.context, this.name, options, undefined, control);
32
31
  }
33
- history(query) {
34
- return participantHistory(this.square, this.name, query);
32
+ history(query, control) {
33
+ return participantHistory(this.square, this.name, query, control);
35
34
  }
36
- hold(reason) {
37
- return hold(this.context, this.name, reason);
35
+ hold(reason, control) {
36
+ return hold(this.context, this.name, reason, control);
38
37
  }
39
- resume() {
40
- return resume(this.context, this.name);
38
+ resume(control) {
39
+ return resume(this.context, this.name, control);
41
40
  }
42
- done(body) {
43
- return done(this.context, this.name, body);
41
+ done(body, control) {
42
+ return done(this.context, this.name, body, {}, control);
44
43
  }
45
44
  }
46
45
  export class Square {
@@ -61,27 +60,33 @@ export class Square {
61
60
  static inMemory(input) {
62
61
  return new Square('memory', buildMemorySquare(input));
63
62
  }
64
- async join(name) {
65
- const joined = await join(this.context, name);
63
+ async join(name, control) {
64
+ const joined = await join(this.context, name, control);
66
65
  return new ParticipantHandle(joined.name, this.square, this.context);
67
66
  }
68
- async joinWithActivity(name) {
69
- const joined = await join(this.context, name);
67
+ async joinWithActivity(name, control) {
68
+ const joined = await join(this.context, name, control);
70
69
  return { participant: new ParticipantHandle(joined.name, this.square, this.context), activity: joined.activity };
71
70
  }
72
- async takeover(name) {
73
- const result = await takeover(this.context, name);
74
- return new ParticipantHandle(result.name, this.square, this.context);
71
+ async takeoverWithActivity(name, control) {
72
+ const result = await takeover(this.context, name, [], control);
73
+ return { participant: new ParticipantHandle(result.name, this.square, this.context), activities: result.activities };
75
74
  }
76
- async implicitJoin(name) {
77
- const joined = await implicitJoin(this.context, name);
75
+ async takeover(name, control) {
76
+ return (await this.takeoverWithActivity(name, control)).participant;
77
+ }
78
+ doneOwnedSession(name, body, expectedEpoch, control) {
79
+ return done(this.context, name, body, { expectedEpoch }, control);
80
+ }
81
+ async implicitJoin(name, control) {
82
+ const joined = await implicitJoin(this.context, name, control);
78
83
  return joined.state === 'done'
79
84
  ? { state: joined.state }
80
85
  : { state: joined.state, participant: new ParticipantHandle(joined.name, this.square, this.context) };
81
86
  }
82
87
  participants() { return participants(this.square); }
83
88
  snapshot() { return snapshot(this.square); }
84
- history(query) { return history(this.square, query); }
89
+ history(query, control) { return history(this.square, query, control); }
85
90
  async recognize(env) {
86
91
  if (this.square.hostLedger === undefined)
87
92
  return null;
@@ -93,7 +98,6 @@ export class Square {
93
98
  hostLedger: this.square.hostLedger,
94
99
  location: this.square.location,
95
100
  sessionId,
96
- scopes: ['user', 'local'],
97
101
  })))).flat();
98
102
  if (candidates.length !== 1)
99
103
  return null;
@@ -101,8 +105,8 @@ export class Square {
101
105
  return canonicalName === undefined ? null : new ParticipantHandle(canonicalName, this.square, this.context);
102
106
  }
103
107
  close() { return closeOpenSquare(this.square); }
104
- reconcileBinding() { return reconcileBindingOperation({ artifact: this.square.artifact, hostLedger: this.square.hostLedger, location: this.location }); }
105
108
  endOwnedSession(name, sessionId, expectedEpoch) { return endOwnedSession(this.context, name, sessionId, expectedEpoch); }
109
+ retireEndedSessionRoutes(sessionId) { return retireEndedSessionRoutes(this.context, sessionId); }
106
110
  }
107
111
  export function markBoundarySeen(squarePath, name, actIndexes, at) {
108
112
  return recordBoundarySeen(squarePath, name, actIndexes, at);
@@ -9,6 +9,6 @@ export interface StateCell {
9
9
  state: SquareState;
10
10
  version: number;
11
11
  }>;
12
- changed(sinceVersion: number, timeoutMs: number): Promise<boolean>;
12
+ changed(sinceVersion: number, timeoutMs: number, signal?: AbortSignal): Promise<boolean>;
13
13
  close(): Promise<void>;
14
14
  }
package/dist/stream.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { once } from 'node:events';
4
- import { setTimeout as sleep } from 'node:timers/promises';
5
4
  import { quoteShell } from './presentation.js';
6
- import { SLEEP_MS } from './runtime.js';
7
- import { openSquare } from './square-file-adapter.js';
5
+ import { openSquare, observeSquareChanges } from './square-file-adapter.js';
8
6
  import { closeOpenSquare } from './open-square.js';
9
7
  import { streamProjection, streamTailProjection } from './views.js';
10
8
  async function writeNdjson(value) {
@@ -20,38 +18,44 @@ export async function cmdStreamNdjson(squarePath, recipient, start = { kind: 'ta
20
18
  }
21
19
  let cursor = start.kind === 'after' ? start.after : -1;
22
20
  let initial = start.kind === 'tail' ? start : undefined;
23
- while (true) {
24
- try {
25
- const square = await openSquare(squarePath);
21
+ const observer = await observeSquareChanges(squarePath);
22
+ try {
23
+ while (true) {
24
+ const generation = observer.generation;
26
25
  try {
27
- const projection = initial === undefined
28
- ? await streamProjection(square, cursor, recipient)
29
- : await streamTailProjection(square, initial.last, recipient);
30
- for (const item of projection.activities) {
31
- await writeNdjson({
32
- seq: item.activity.index,
33
- square: squarePath,
34
- ...item.activity,
35
- ...(item.route === undefined ? {} : { route: item.route }),
36
- });
26
+ const baseline = await observer.read();
27
+ const square = await openSquare(squarePath);
28
+ let hasMore;
29
+ try {
30
+ const projection = initial === undefined
31
+ ? await streamProjection(square, cursor, recipient)
32
+ : await streamTailProjection(square, initial.last, recipient);
33
+ for (const item of projection.activities) {
34
+ await writeNdjson({ seq: item.activity.index, square: squarePath, ...item.activity,
35
+ ...(item.route === undefined ? {} : { route: item.route }) });
36
+ }
37
+ cursor = projection.cursor;
38
+ initial = undefined;
39
+ hasMore = projection.hasMore;
37
40
  }
38
- cursor = projection.cursor;
39
- initial = undefined;
40
- if (projection.hasMore)
41
- continue;
41
+ finally {
42
+ await closeOpenSquare(square);
43
+ }
44
+ if (!hasMore)
45
+ await observer.changed(baseline, Infinity);
42
46
  }
43
- finally {
44
- await closeOpenSquare(square);
47
+ catch {
48
+ // Repair/replacement is a storage hint, not a reason to scan history on a timer.
49
+ await observer.hinted(generation, Infinity);
45
50
  }
46
51
  }
47
- catch {
48
- // A concurrent artifact replacement is retried on the next poll.
49
- }
50
- await sleep(SLEEP_MS);
52
+ }
53
+ finally {
54
+ observer.close();
51
55
  }
52
56
  }
53
57
  export async function cmdStream(squarePath) {
54
58
  process.stderr.write('✕ interactive stream was removed\n');
55
- process.stderr.write(`» square --location ${quoteShell(path.resolve(squarePath))} stream --ndjson\n`);
59
+ process.stderr.write(`square --location ${quoteShell(path.resolve(squarePath))} stream --ndjson\n`);
56
60
  process.exitCode = 2;
57
61
  }
@@ -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.d.ts CHANGED
@@ -3,7 +3,7 @@ import { coreParticipants, coreStatus } from './decisions.js';
3
3
  import { type DeliveryModel, type PlannedNotification } from './delivery.js';
4
4
  import { type ActivitiesOptions, type ActivityObservation, type InboxNotification, type PublicAct, type RoomChangeAct, type SquareState, type StoredAct } from './model.js';
5
5
  import type { OpenSquare } from './open-square.js';
6
- import type { Activity, HistoryQuery, ParticipantStatus, SquareSnapshot } from './square-facade.js';
6
+ import type { Activity, HistoryQuery, OperationControl, ParticipantStatus, SquareSnapshot } from './square-facade.js';
7
7
  export interface ActivityPresentation {
8
8
  readonly name: string;
9
9
  readonly roster: readonly string[];
@@ -71,8 +71,8 @@ export interface PendingDeliveryProjection {
71
71
  readonly recipient: string;
72
72
  readonly notifications: readonly PlannedNotification[];
73
73
  }
74
- export declare function history(square: OpenSquare, query?: HistoryQuery): Promise<Activity[]>;
75
- export declare function participantHistory(square: OpenSquare, _name: string, query?: HistoryQuery): Promise<Activity[]>;
74
+ export declare function history(square: OpenSquare, query?: HistoryQuery, control?: OperationControl): Promise<Activity[]>;
75
+ export declare function participantHistory(square: OpenSquare, _name: string, query?: HistoryQuery, control?: OperationControl): Promise<Activity[]>;
76
76
  export declare function resolveParticipant(square: OpenSquare, name: string): Promise<{
77
77
  readonly name: string;
78
78
  readonly roster: readonly string[];
package/dist/views.js CHANGED
@@ -1,13 +1,13 @@
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)}`);
10
- return { id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor, ...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}), mentions: stored.kind === 'say' ? stored.mentions ?? [] : [], ...('target' in stored ? { target: stored.target } : {}), ...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}) };
10
+ return { id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor, ...(stored.kind === 'say' && stored.reach !== undefined ? { reach: stored.reach } : {}), ...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}), mentions: stored.kind === 'say' ? stored.mentions ?? [] : [], ...('target' in stored ? { target: stored.target } : {}), ...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}) };
11
11
  }
12
12
  function parseRequiredActivityId(id) {
13
13
  const index = parseActivityId(id);
@@ -20,7 +20,7 @@ function historyOptions(query) {
20
20
  throw new SquareError('invalid_args', 'History cannot combine before and after cursors');
21
21
  const afterIndex = query.after === undefined ? undefined : parseRequiredActivityId(query.after);
22
22
  const beforeIndex = query.before === undefined ? undefined : parseRequiredActivityId(query.before);
23
- return { ...(query.from === undefined ? {} : { participants: [...query.from] }), ...(query.grep === undefined ? {} : { grep: query.grep }), ...(query.mention === undefined ? {} : { mention: query.mention }), ...(afterIndex === undefined ? {} : { afterIndex }), ...(beforeIndex === undefined ? {} : { beforeIndex }), order: 'asc' };
23
+ return { ...(query.from === undefined ? {} : { participants: [...query.from] }), ...(query.grep === undefined ? {} : { grep: query.grep }), ...(query.fixed === undefined ? {} : { fixed: query.fixed }), ...(query.mention === undefined ? {} : { mention: query.mention }), ...(afterIndex === undefined ? {} : { afterIndex }), ...(beforeIndex === undefined ? {} : { beforeIndex }), order: 'asc' };
24
24
  }
25
25
  function selectHistory(stored, query) {
26
26
  let selected = stored.filter((activity) => activity.kind !== 'read');
@@ -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
  }
@@ -58,8 +59,8 @@ function sayNumbers(state) {
58
59
  }
59
60
  return result;
60
61
  }
61
- export async function history(square, query = {}) { const { state } = await square.artifact.read(); return selectHistory(coreActivities(state, historyOptions(query)), query).map(expose); }
62
- export async function participantHistory(square, _name, query = {}) { const { state } = await square.artifact.read(); const effective = query.limit !== undefined ? query : { ...query, limit: 10 }; return selectHistory(coreActivities(state, historyOptions(effective)), effective).map(expose); }
62
+ export async function history(square, query = {}, control) { const { state } = await square.artifact.read(control?.signal); return selectHistory(coreActivities(state, historyOptions(query)), query).map(expose); }
63
+ export async function participantHistory(square, _name, query = {}, control) { const { state } = await square.artifact.read(control?.signal); const effective = query.limit !== undefined ? query : { ...query, limit: 10 }; return selectHistory(coreActivities(state, historyOptions(effective)), effective).map(expose); }
63
64
  export async function resolveParticipant(square, name) { const { state } = await square.artifact.read(); return { name: resolveKnownName(state, name), roster: rosterNames(state) }; }
64
65
  export async function currentParticipant(square, name) { const { state } = await square.artifact.read(); const known = resolveRosterName(state, name); return known !== undefined && isCurrentlyJoined(state.acts, known) ? known : undefined; }
65
66
  export async function participants(square) { const { state } = await square.artifact.read(); return statuses(square, state); }
@@ -1,4 +1,5 @@
1
1
  import os from 'node:os';
2
+ import { hostLedgerRoot } from './host-ledger-root.js';
2
3
  import path from 'node:path';
3
4
  import { isWakeRouteKind, nameKey } from './model.js';
4
5
  import { canonicalSquarePath } from './registry.js';
@@ -9,7 +10,7 @@ import { redactDiagnostic } from './diagnostic-redaction.js';
9
10
  export { hasAttemptableWakeRoute, isWakeRouteAttemptable, terminalWakeEvidence } from './square-projections.js';
10
11
  const VALID_OUTCOMES = new Set(['accepted', 'unknown', 'failed']);
11
12
  function ledger(env) {
12
- return createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? path.dirname(wakeAttemptsPath(env)), readableScopes: ['user'], writableScope: 'user' });
13
+ return createHostLedgerPort({ rootPath: hostLedgerRoot(env) });
13
14
  }
14
15
  export function wakeAttemptsPath(env = process.env) {
15
16
  return env.SQUARE_WAKE_ATTEMPTS || path.join(os.homedir(), '.square', 'wake-attempts.ndjsonl');
@@ -1,17 +1,11 @@
1
- import path from 'node:path';
1
+ import { hostLedgerRoot } from './host-ledger-root.js';
2
2
  import { createHostLedgerPort } from './host-ledger-file-adapter.js';
3
3
  import { openSquare } from './square-file-adapter.js';
4
4
  import { closeOpenSquare } from './open-square.js';
5
5
  import { projectWakeEvidenceFromState, wakeIsEligible, } from './square-projections.js';
6
6
  export { wakeIsEligible };
7
7
  function hostLedgerForEnv(env) {
8
- const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
9
- return createHostLedgerPort({
10
- userPath: env.SQUARE_HOST_LEDGER_USER ?? root,
11
- localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root,
12
- readableScopes: ['user'],
13
- writableScope: 'user',
14
- });
8
+ return createHostLedgerPort({ rootPath: hostLedgerRoot(env) });
15
9
  }
16
10
  /** Adapter entry: open the artifact, assemble host ports, and project wake evidence. */
17
11
  export async function wakeEvidenceProjection(squarePath, now, env) {
package/dist/wakes.d.ts CHANGED
@@ -13,6 +13,7 @@ export type WatchLeasePulse = {
13
13
  readonly type: 'replaced';
14
14
  } | {
15
15
  readonly type: 'held';
16
+ readonly heartbeatAt?: number;
16
17
  } | {
17
18
  readonly type: 'terminal';
18
19
  readonly status: 'capped' | 'quorum';
package/dist/wakes.js CHANGED
@@ -21,15 +21,15 @@ export async function pulseWatchLease(square, name, leaseId, options, heartbeatD
21
21
  const lease = freshWatchLease(state, known, at);
22
22
  if (lease?.leaseId !== leaseId)
23
23
  return { result: { type: 'replaced' } };
24
- if (currentHold(state.acts).active)
25
- return { result: { type: 'held' } };
26
- const terminal = watchTerminalStatus(state, known);
24
+ const held = currentHold(state.acts).active;
25
+ const terminal = held ? undefined : watchTerminalStatus(state, known);
27
26
  if (terminal !== undefined)
28
27
  return { result: { type: 'terminal', status: terminal } };
28
+ const type = held ? 'held' : 'sleep';
29
29
  if (!heartbeatDue)
30
- return { result: { type: 'sleep' } };
30
+ return { result: { type } };
31
31
  writeWatchLease(state, known, { leaseId, heartbeatAt: at, expiresAt: at + WATCH_STALE_MS, ...(Object.keys(filter(options)).length === 0 ? {} : { filter: filter(options) }) });
32
- return { state, result: { type: 'sleep', heartbeatAt: at } };
32
+ return { state, result: { type, heartbeatAt: at } };
33
33
  });
34
34
  }
35
35
  export async function releaseWatchLease(square, name, leaseId) {
package/dist/watch.d.ts CHANGED
@@ -1,3 +1,8 @@
1
1
  import { type WatchOptions } from './model.js';
2
+ interface WatchCallerContext {
3
+ readonly cwd: string;
4
+ readonly env: NodeJS.ProcessEnv;
5
+ }
2
6
  /** `false` is reserved for a quiet --now; idle completion preserves its existing sweep boundary. */
3
- export declare function cmdWatch(squarePath: string, name: string, opts: WatchOptions): Promise<boolean | undefined>;
7
+ export declare function cmdWatch(squarePath: string, name: string, opts: WatchOptions, caller?: WatchCallerContext): Promise<boolean | undefined>;
8
+ export {};
package/dist/watch.js CHANGED
@@ -1,12 +1,11 @@
1
- import { setTimeout as sleep } from 'node:timers/promises';
2
1
  import { isSquareError, } from './model.js';
3
- import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, nowMs, } from './runtime.js';
2
+ import { STALE_MS, WATCH_HEARTBEAT_MS, inSquareCount, nowMs, } from './runtime.js';
4
3
  import { openSquare } from './square-file-adapter.js';
5
4
  import { closeOpenSquare } from './open-square.js';
6
- import { openParticipant } from './square-wiring.js';
5
+ import { createSquareApplication } from './square-application.js';
7
6
  import { resolveParticipant, watchPresentation } from './views.js';
8
7
  import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
9
- import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchReplaceMissing, renderWatchOutput, renderWatchReplaced, renderWatchStatus, participantCommandPrefix, joinRecoveryCommand, participantsRecoveryCommand, withPathOutput, } from './presentation.js';
8
+ import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchReplaceMissing, renderWatchOutput, renderWatchReplaced, renderWatchStatus, formatRefusal, participantCommandPrefix, joinRecoveryCommand, participantsRecoveryCommand, withPathOutput, } from './presentation.js';
10
9
  import { hasAutomaticDeliveryIdentity } from './registry.js';
11
10
  import { parseActivityId } from './square-core.js';
12
11
  function watchStatusExitCode(status) {
@@ -37,7 +36,7 @@ function watchOutputResult(squarePath, presentation, name, caught, opts = {}) {
37
36
  showCatchHint: !hasAutomaticDeliveryIdentity(),
38
37
  perceptions,
39
38
  }) + (caught.remaining > 0
40
- ? `\n○ ${caught.remaining} matching ${caught.remaining === 1 ? 'activity remains' : 'activities remain'}\n» ${catchContinuationCommand(squarePath, name, opts)}`
39
+ ? `\n○ ${caught.remaining} matching ${caught.remaining === 1 ? 'activity remains' : 'activities remain'}\n${catchContinuationCommand(squarePath, name, opts)}`
41
40
  : ''),
42
41
  remaining: caught.remaining,
43
42
  ...(opts.status ? { status: opts.status } : {}),
@@ -61,7 +60,7 @@ function writeWatchOutput(squarePath, name, presentation, stdout, remaining, sta
61
60
  return;
62
61
  }
63
62
  const fallback = showCatchHint && remaining === 0
64
- ? `» ${participantCommandPrefix(squarePath, name)} catch --idle 30m\n stay available for new activity`
63
+ ? `${participantCommandPrefix(squarePath, name)} catch --idle 30m\n stay available for new activity`
65
64
  : '';
66
65
  process.stdout.write(withPathOutput(squarePath, [stdout.trimEnd(), fallback].filter(Boolean).join('\n\n').trimEnd(), headerOpts));
67
66
  }
@@ -109,7 +108,7 @@ function installWatchInterruptHandler(square, squarePath, name, currentLeaseId)
109
108
  const onInterrupt = () => {
110
109
  void (async () => {
111
110
  await endWatch(square, name, currentLeaseId());
112
- process.stdout.write(withPathOutput(squarePath, '✕ catch stopped'));
111
+ process.stdout.write(withPathOutput(squarePath, '○ you step back — catch stopped'));
113
112
  process.exit(130);
114
113
  })().catch((error) => {
115
114
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
@@ -121,11 +120,11 @@ function installWatchInterruptHandler(square, squarePath, name, currentLeaseId)
121
120
  process.off('SIGINT', onInterrupt);
122
121
  };
123
122
  }
124
- async function cmdWatchNow(squarePath, name, opts) {
125
- const square = await openSquare(squarePath, { clock: nowMs });
126
- const facade = await openParticipant({ path: squarePath, clock: nowMs }, name);
123
+ async function cmdWatchNow(squarePath, name, opts, caller) {
124
+ const square = await openSquare(squarePath, { clock: nowMs, env: caller.env });
125
+ const application = createSquareApplication({ cwd: caller.cwd, env: caller.env, squarePath, participant: name, clock: nowMs });
127
126
  try {
128
- const caught = await facade.participant.catch({
127
+ const caught = await application.catch({
129
128
  ...(opts.id === undefined ? {} : { id: opts.id }),
130
129
  ...(opts.participants === undefined ? {} : { from: opts.participants }),
131
130
  ...(opts.mention === undefined ? {} : { mention: true }),
@@ -140,23 +139,33 @@ async function cmdWatchNow(squarePath, name, opts) {
140
139
  return caught.activities.length > 0;
141
140
  }
142
141
  finally {
143
- await facade.close();
144
142
  await closeOpenSquare(square);
145
143
  }
146
144
  }
145
+ async function countParticipants(square) {
146
+ try {
147
+ return inSquareCount((await square.artifact.read()).state);
148
+ }
149
+ catch {
150
+ return undefined;
151
+ }
152
+ }
147
153
  /** `false` is reserved for a quiet --now; idle completion preserves its existing sweep boundary. */
148
- export async function cmdWatch(squarePath, name, opts) {
154
+ export async function cmdWatch(squarePath, name, opts, caller = { cwd: process.cwd(), env: { ...process.env } }) {
149
155
  let square;
156
+ let participantCount;
150
157
  try {
151
- square = await openSquare(squarePath, { clock: nowMs });
158
+ square = await openSquare(squarePath, { clock: nowMs, env: caller.env });
159
+ participantCount = inSquareCount((await square.artifact.read()).state);
152
160
  name = (await resolveParticipant(square, name)).name;
153
161
  }
154
162
  catch (err) {
155
163
  if (isSquareError(err)) {
156
- process.stderr.write(err.message + '\n');
164
+ const bodyLines = [err.message];
157
165
  // 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`);
166
+ if (/^✕ .* has never stepped into/.test(err.message))
167
+ bodyLines.push(joinRecoveryCommand(squarePath, name));
168
+ process.stderr.write(formatRefusal(squarePath, bodyLines, participantCount === undefined ? {} : { participantCount }));
160
169
  process.exit(err.code === 'not_found' ? 1 : 2);
161
170
  }
162
171
  throw err;
@@ -169,19 +178,21 @@ export async function cmdWatch(squarePath, name, opts) {
169
178
  }
170
179
  }
171
180
  catch (err) {
181
+ const participantCount = await countParticipants(square);
172
182
  await closeOpenSquare(square).catch(() => undefined);
173
183
  if (isSquareError(err)) {
174
- process.stderr.write(err.message + '\n');
184
+ const bodyLines = [err.message];
175
185
  // 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`);
186
+ if (/^✕ .*(has never stepped into|is not standing in this square)/.test(err.message))
187
+ bodyLines.push(participantsRecoveryCommand(squarePath));
188
+ process.stderr.write(formatRefusal(squarePath, bodyLines, participantCount === undefined ? {} : { participantCount }));
178
189
  process.exit(err.code === 'not_found' ? 1 : 2);
179
190
  }
180
191
  throw err;
181
192
  }
182
193
  if (opts.now) {
183
194
  await closeOpenSquare(square);
184
- return cmdWatchNow(squarePath, name, opts);
195
+ return cmdWatchNow(squarePath, name, opts, caller);
185
196
  }
186
197
  const start = await beginWatch(square, squarePath, name, opts);
187
198
  if (start.type === 'active') {
@@ -191,6 +202,7 @@ export async function cmdWatch(squarePath, name, opts) {
191
202
  process.exit(1);
192
203
  }
193
204
  let staleSince = nowMs();
205
+ let wasHeld = false;
194
206
  let currentLeaseId = start.leaseId;
195
207
  let nextHeartbeatAt = start.heartbeatAt + WATCH_HEARTBEAT_MS;
196
208
  if (opts.replace) {
@@ -201,16 +213,18 @@ export async function cmdWatch(squarePath, name, opts) {
201
213
  }
202
214
  const idleMs = opts.idleMs ?? STALE_MS;
203
215
  const removeInterruptHandler = installWatchInterruptHandler(square, squarePath, name, () => currentLeaseId);
204
- const facade = await openParticipant({ path: squarePath, clock: nowMs }, name);
216
+ const application = createSquareApplication({ cwd: caller.cwd, env: caller.env, squarePath, participant: name, clock: nowMs });
205
217
  try {
206
218
  while (true) {
219
+ // Capture before evaluation: changes during catch/lease checks must not be lost.
220
+ const baseline = (await square.artifact.read()).version;
207
221
  const leaseState = await pulseWatchLease(square, name, currentLeaseId, opts, nowMs() >= nextHeartbeatAt);
208
- if (leaseState.type === 'sleep' && leaseState.heartbeatAt !== undefined) {
222
+ if ((leaseState.type === 'sleep' || leaseState.type === 'held') && leaseState.heartbeatAt !== undefined) {
209
223
  nextHeartbeatAt = leaseState.heartbeatAt + WATCH_HEARTBEAT_MS;
210
224
  }
211
225
  let result = leaseState;
212
226
  if (result.type === 'sleep') {
213
- const caught = await facade.participant.catch({
227
+ const caught = await application.catch({
214
228
  ...(opts.participants === undefined ? {} : { from: opts.participants }),
215
229
  ...(opts.mention === undefined ? {} : { mention: true }),
216
230
  ...(opts.limit === undefined ? {} : { limit: opts.limit }),
@@ -223,9 +237,10 @@ export async function cmdWatch(squarePath, name, opts) {
223
237
  currentLeaseId = undefined;
224
238
  return;
225
239
  }
226
- if (result.type === 'held')
240
+ if (result.type === 'held' || wasHeld)
227
241
  staleSince = nowMs();
228
- if (nowMs() - staleSince >= idleMs) {
242
+ wasHeld = result.type === 'held';
243
+ if (!wasHeld && nowMs() - staleSince >= idleMs) {
229
244
  const result = !await ownsWatchLease(square, name, currentLeaseId)
230
245
  ? { type: 'replaced' }
231
246
  : { type: 'terminal', status: 'stale' };
@@ -234,11 +249,11 @@ export async function cmdWatch(squarePath, name, opts) {
234
249
  return;
235
250
  }
236
251
  }
237
- await sleep(SLEEP_MS);
252
+ const nextDeadline = Math.min(nextHeartbeatAt, wasHeld ? Infinity : staleSince + idleMs);
253
+ await square.artifact.changed(baseline, Math.max(0, nextDeadline - nowMs()));
238
254
  }
239
255
  }
240
256
  finally {
241
- await facade.close();
242
257
  await closeOpenSquare(square);
243
258
  removeInterruptHandler();
244
259
  }