@astrosheep/square 0.3.61 → 0.3.63

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 (90) 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 +168 -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 +8 -6
  10. package/dist/automatic-session.js +14 -24
  11. package/dist/boundary-presentation.d.ts +1 -2
  12. package/dist/boundary-presentation.js +4 -19
  13. package/dist/cli/context.d.ts +2 -0
  14. package/dist/cli/context.js +2 -18
  15. package/dist/cli/harness-command.js +1 -1
  16. package/dist/cli/mcp-server-command.d.ts +2 -0
  17. package/dist/cli/mcp-server-command.js +13 -0
  18. package/dist/cli/observation-commands.js +47 -19
  19. package/dist/cli/program.js +5 -2
  20. package/dist/cli/registry.js +2 -0
  21. package/dist/cli/square-commands.d.ts +1 -1
  22. package/dist/cli/square-commands.js +93 -125
  23. package/dist/decisions.d.ts +2 -0
  24. package/dist/decisions.js +44 -3
  25. package/dist/delivery-operations.d.ts +1 -2
  26. package/dist/delivery-operations.js +1 -4
  27. package/dist/file-changes.d.ts +12 -0
  28. package/dist/file-changes.js +235 -0
  29. package/dist/help.js +4 -8
  30. package/dist/host-ledger-file-adapter.d.ts +15 -13
  31. package/dist/host-ledger-file-adapter.js +131 -79
  32. package/dist/host-ledger-root.d.ts +2 -0
  33. package/dist/host-ledger-root.js +9 -0
  34. package/dist/host-ledger.d.ts +4 -24
  35. package/dist/inbox.d.ts +10 -5
  36. package/dist/inbox.js +152 -67
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.js +1 -0
  39. package/dist/mcp.d.ts +7 -0
  40. package/dist/mcp.js +122 -0
  41. package/dist/mention-parse.d.ts +8 -0
  42. package/dist/mention-parse.js +107 -0
  43. package/dist/model.d.ts +5 -0
  44. package/dist/model.js +4 -1
  45. package/dist/notifications.d.ts +1 -0
  46. package/dist/notifications.js +25 -20
  47. package/dist/participant-host.js +2 -2
  48. package/dist/ports.d.ts +2 -9
  49. package/dist/presentation.d.ts +12 -1
  50. package/dist/presentation.js +42 -12
  51. package/dist/presented.js +2 -1
  52. package/dist/registry.d.ts +8 -4
  53. package/dist/registry.js +41 -74
  54. package/dist/routes.d.ts +19 -2
  55. package/dist/routes.js +48 -5
  56. package/dist/runtime.d.ts +2 -0
  57. package/dist/runtime.js +2 -0
  58. package/dist/square-actions.d.ts +26 -13
  59. package/dist/square-actions.js +200 -84
  60. package/dist/square-application.d.ts +58 -0
  61. package/dist/square-application.js +183 -0
  62. package/dist/square-facade.d.ts +24 -9
  63. package/dist/square-file-adapter.d.ts +5 -12
  64. package/dist/square-file-adapter.js +12 -64
  65. package/dist/square-projections.d.ts +3 -2
  66. package/dist/square-projections.js +12 -3
  67. package/dist/square-storage.js +27 -34
  68. package/dist/square-wiring.d.ts +12 -7
  69. package/dist/square-wiring.js +36 -32
  70. package/dist/state-cell.d.ts +1 -1
  71. package/dist/stream.js +30 -26
  72. package/dist/views.d.ts +3 -3
  73. package/dist/views.js +4 -4
  74. package/dist/wake-attempts.js +2 -1
  75. package/dist/wake-evidence.js +2 -8
  76. package/dist/wakes.d.ts +1 -0
  77. package/dist/wakes.js +5 -5
  78. package/dist/watch.d.ts +6 -1
  79. package/dist/watch.js +20 -18
  80. package/extensions/square-pi.js +100 -112
  81. package/guides/application-boundary.md +32 -0
  82. package/guides/architect.md +77 -77
  83. package/guides/brainstorm.md +197 -197
  84. package/guides/change-waits.md +28 -0
  85. package/guides/host-ledger.md +9 -0
  86. package/guides/mcp.md +9 -0
  87. package/guides/operation-cancellation.md +17 -0
  88. package/guides/participant.md +7 -7
  89. package/package.json +8 -2
  90. 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,34 +18,40 @@ 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) {
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
@@ -7,7 +7,7 @@ import { countSays, currentHold, foldedState, freshWatchLease, inSquareCount, is
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');
@@ -59,8 +59,8 @@ function sayNumbers(state) {
59
59
  }
60
60
  return result;
61
61
  }
62
- export async function history(square, query = {}) { const { state } = await square.artifact.read(); return selectHistory(coreActivities(state, historyOptions(query)), query).map(expose); }
63
- 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); }
64
64
  export async function resolveParticipant(square, name) { const { state } = await square.artifact.read(); return { name: resolveKnownName(state, name), roster: rosterNames(state) }; }
65
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; }
66
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,9 +1,8 @@
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, inSquareCount, 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
8
  import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchReplaceMissing, renderWatchOutput, renderWatchReplaced, renderWatchStatus, formatRefusal, participantCommandPrefix, joinRecoveryCommand, participantsRecoveryCommand, withPathOutput, } from './presentation.js';
@@ -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,7 +139,6 @@ 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
  }
@@ -153,11 +151,11 @@ async function countParticipants(square) {
153
151
  }
154
152
  }
155
153
  /** `false` is reserved for a quiet --now; idle completion preserves its existing sweep boundary. */
156
- export async function cmdWatch(squarePath, name, opts) {
154
+ export async function cmdWatch(squarePath, name, opts, caller = { cwd: process.cwd(), env: { ...process.env } }) {
157
155
  let square;
158
156
  let participantCount;
159
157
  try {
160
- square = await openSquare(squarePath, { clock: nowMs });
158
+ square = await openSquare(squarePath, { clock: nowMs, env: caller.env });
161
159
  participantCount = inSquareCount((await square.artifact.read()).state);
162
160
  name = (await resolveParticipant(square, name)).name;
163
161
  }
@@ -194,7 +192,7 @@ export async function cmdWatch(squarePath, name, opts) {
194
192
  }
195
193
  if (opts.now) {
196
194
  await closeOpenSquare(square);
197
- return cmdWatchNow(squarePath, name, opts);
195
+ return cmdWatchNow(squarePath, name, opts, caller);
198
196
  }
199
197
  const start = await beginWatch(square, squarePath, name, opts);
200
198
  if (start.type === 'active') {
@@ -204,6 +202,7 @@ export async function cmdWatch(squarePath, name, opts) {
204
202
  process.exit(1);
205
203
  }
206
204
  let staleSince = nowMs();
205
+ let wasHeld = false;
207
206
  let currentLeaseId = start.leaseId;
208
207
  let nextHeartbeatAt = start.heartbeatAt + WATCH_HEARTBEAT_MS;
209
208
  if (opts.replace) {
@@ -214,16 +213,18 @@ export async function cmdWatch(squarePath, name, opts) {
214
213
  }
215
214
  const idleMs = opts.idleMs ?? STALE_MS;
216
215
  const removeInterruptHandler = installWatchInterruptHandler(square, squarePath, name, () => currentLeaseId);
217
- const facade = await openParticipant({ path: squarePath, clock: nowMs }, name);
216
+ const application = createSquareApplication({ cwd: caller.cwd, env: caller.env, squarePath, participant: name, clock: nowMs });
218
217
  try {
219
218
  while (true) {
219
+ // Capture before evaluation: changes during catch/lease checks must not be lost.
220
+ const baseline = (await square.artifact.read()).version;
220
221
  const leaseState = await pulseWatchLease(square, name, currentLeaseId, opts, nowMs() >= nextHeartbeatAt);
221
- if (leaseState.type === 'sleep' && leaseState.heartbeatAt !== undefined) {
222
+ if ((leaseState.type === 'sleep' || leaseState.type === 'held') && leaseState.heartbeatAt !== undefined) {
222
223
  nextHeartbeatAt = leaseState.heartbeatAt + WATCH_HEARTBEAT_MS;
223
224
  }
224
225
  let result = leaseState;
225
226
  if (result.type === 'sleep') {
226
- const caught = await facade.participant.catch({
227
+ const caught = await application.catch({
227
228
  ...(opts.participants === undefined ? {} : { from: opts.participants }),
228
229
  ...(opts.mention === undefined ? {} : { mention: true }),
229
230
  ...(opts.limit === undefined ? {} : { limit: opts.limit }),
@@ -236,9 +237,10 @@ export async function cmdWatch(squarePath, name, opts) {
236
237
  currentLeaseId = undefined;
237
238
  return;
238
239
  }
239
- if (result.type === 'held')
240
+ if (result.type === 'held' || wasHeld)
240
241
  staleSince = nowMs();
241
- if (nowMs() - staleSince >= idleMs) {
242
+ wasHeld = result.type === 'held';
243
+ if (!wasHeld && nowMs() - staleSince >= idleMs) {
242
244
  const result = !await ownsWatchLease(square, name, currentLeaseId)
243
245
  ? { type: 'replaced' }
244
246
  : { type: 'terminal', status: 'stale' };
@@ -247,11 +249,11 @@ export async function cmdWatch(squarePath, name, opts) {
247
249
  return;
248
250
  }
249
251
  }
250
- 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()));
251
254
  }
252
255
  }
253
256
  finally {
254
- await facade.close();
255
257
  await closeOpenSquare(square);
256
258
  removeInterruptHandler();
257
259
  }