@astrosheep/square 0.3.63 → 0.3.65

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 (171) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +3 -2
  2. package/claude-plugin/hooks/hooks.json +1 -22
  3. package/claude-plugin/hooks/register.js +119 -0
  4. package/claude-plugin/types.d.ts +7 -0
  5. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  6. package/dist/activity-feed.d.ts +1 -10
  7. package/dist/activity-feed.js +1 -27
  8. package/dist/activity.js +57 -25
  9. package/dist/artifact.d.ts +1 -1
  10. package/dist/artifact.js +24 -25
  11. package/dist/attention-presentation.js +1 -0
  12. package/dist/automatic-session.d.ts +3 -1
  13. package/dist/automatic-session.js +21 -34
  14. package/dist/boundary-presentation.js +4 -13
  15. package/dist/canonical-path.d.ts +11 -0
  16. package/dist/canonical-path.js +47 -0
  17. package/dist/catch-decisions.js +14 -10
  18. package/dist/catch-selection.d.ts +6 -0
  19. package/dist/catch-selection.js +14 -0
  20. package/dist/claude-delivery.d.ts +11 -0
  21. package/dist/claude-delivery.js +145 -0
  22. package/dist/claude-mod.d.ts +8 -0
  23. package/dist/claude-mod.js +164 -0
  24. package/dist/cli/context.d.ts +0 -1
  25. package/dist/cli/context.js +9 -21
  26. package/dist/cli/harness-command.d.ts +0 -1
  27. package/dist/cli/harness-command.js +0 -6
  28. package/dist/cli/observation-commands.d.ts +1 -1
  29. package/dist/cli/observation-commands.js +31 -58
  30. package/dist/cli/program.js +20 -18
  31. package/dist/cli/registry.js +2 -2
  32. package/dist/cli/square-commands.js +10 -6
  33. package/dist/codex-boundary-state.d.ts +2 -2
  34. package/dist/codex-boundary-state.js +4 -4
  35. package/dist/codex-hook.js +12 -15
  36. package/dist/codex-queue.d.ts +2 -2
  37. package/dist/codex-queue.js +7 -5
  38. package/dist/decisions.d.ts +2 -6
  39. package/dist/decisions.js +16 -17
  40. package/dist/delivery-health.d.ts +1 -2
  41. package/dist/delivery-health.js +4 -5
  42. package/dist/delivery-operations.d.ts +2 -5
  43. package/dist/delivery-operations.js +77 -214
  44. package/dist/delivery.d.ts +25 -47
  45. package/dist/delivery.js +14 -46
  46. package/dist/diagnostic-redaction.js +7 -9
  47. package/dist/file-changes.d.ts +0 -1
  48. package/dist/file-changes.js +2 -19
  49. package/dist/harness-claude.js +8 -6
  50. package/dist/harness-codex.d.ts +0 -2
  51. package/dist/harness-codex.js +0 -2
  52. package/dist/harness-links.js +13 -17
  53. package/dist/harness.js +3 -7
  54. package/dist/help.js +4 -3
  55. package/dist/host-ledger-file-adapter.d.ts +17 -8
  56. package/dist/host-ledger-file-adapter.js +164 -80
  57. package/dist/host-ledger.d.ts +55 -50
  58. package/dist/inbox.d.ts +0 -3
  59. package/dist/inbox.js +88 -50
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +1 -1
  62. package/dist/list.js +1 -4
  63. package/dist/mcp.js +24 -41
  64. package/dist/mention-parse.js +3 -9
  65. package/dist/model.d.ts +11 -2
  66. package/dist/model.js +1 -1
  67. package/dist/notifications.d.ts +3 -16
  68. package/dist/notifications.js +41 -122
  69. package/dist/opencode-delivery.d.ts +14 -0
  70. package/dist/opencode-delivery.js +171 -0
  71. package/dist/opencode.d.ts +39 -18
  72. package/dist/opencode.js +145 -40
  73. package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
  74. package/dist/packages/agent-delivery/src/claude-auth.js +75 -0
  75. package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
  76. package/dist/packages/agent-delivery/src/claude-native.js +68 -0
  77. package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
  78. package/dist/packages/agent-delivery/src/claude.js +66 -0
  79. package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
  80. package/dist/packages/agent-delivery/src/deadline.js +41 -0
  81. package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
  82. package/dist/packages/agent-delivery/src/errors.js +41 -0
  83. package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
  84. package/dist/packages/agent-delivery/src/guards.js +6 -0
  85. package/dist/packages/agent-delivery/src/index.d.ts +83 -0
  86. package/dist/packages/agent-delivery/src/index.js +110 -0
  87. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
  88. package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
  89. package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
  90. package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
  91. package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
  92. package/dist/packages/agent-delivery/src/opencode.js +127 -0
  93. package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
  94. package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
  95. package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
  96. package/dist/packages/agent-delivery/src/paseo.js +106 -0
  97. package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
  98. package/dist/packages/agent-delivery/src/pi-client.js +106 -0
  99. package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
  100. package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
  101. package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
  102. package/dist/packages/agent-delivery/src/pi-send.js +4 -0
  103. package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
  104. package/dist/packages/agent-delivery/src/pi.js +209 -0
  105. package/dist/packages/agent-delivery/src/types.d.ts +120 -0
  106. package/dist/participant-host.js +12 -13
  107. package/dist/participant-identity.d.ts +21 -0
  108. package/dist/participant-identity.js +32 -11
  109. package/dist/paseo-delivery.d.ts +2 -2
  110. package/dist/paseo-delivery.js +35 -27
  111. package/dist/paseo-timeline.js +25 -21
  112. package/dist/paseo.d.ts +2 -2
  113. package/dist/paseo.js +1 -2
  114. package/dist/ports.d.ts +7 -20
  115. package/dist/presentation-operations.js +16 -4
  116. package/dist/presentation.d.ts +19 -10
  117. package/dist/presentation.js +99 -177
  118. package/dist/registry.d.ts +15 -66
  119. package/dist/registry.js +45 -123
  120. package/dist/routes.d.ts +1 -28
  121. package/dist/routes.js +22 -104
  122. package/dist/runtime.d.ts +3 -9
  123. package/dist/runtime.js +14 -67
  124. package/dist/search.d.ts +0 -2
  125. package/dist/search.js +3 -10
  126. package/dist/square-actions.js +48 -72
  127. package/dist/square-application.js +32 -30
  128. package/dist/square-core.d.ts +3 -0
  129. package/dist/square-core.js +22 -5
  130. package/dist/square-facade.d.ts +2 -0
  131. package/dist/square-file-adapter.d.ts +2 -1
  132. package/dist/square-file-adapter.js +6 -9
  133. package/dist/square-projections.d.ts +40 -45
  134. package/dist/square-projections.js +60 -74
  135. package/dist/square-state.js +1 -4
  136. package/dist/square-storage.d.ts +5 -8
  137. package/dist/square-storage.js +13 -36
  138. package/dist/square-wiring.d.ts +2 -7
  139. package/dist/square-wiring.js +6 -24
  140. package/dist/stream.js +4 -6
  141. package/dist/time.js +2 -7
  142. package/dist/views.d.ts +6 -18
  143. package/dist/views.js +14 -21
  144. package/dist/wake-attempts.d.ts +2 -56
  145. package/dist/wake-attempts.js +15 -110
  146. package/dist/wake-eligibility.d.ts +43 -0
  147. package/dist/wake-eligibility.js +106 -0
  148. package/dist/wake-evidence.d.ts +32 -9
  149. package/dist/wake-evidence.js +29 -24
  150. package/dist/wake-sink.d.ts +13 -9
  151. package/dist/wake-sink.js +35 -54
  152. package/dist/watch.js +13 -21
  153. package/extensions/square-pi.js +27 -13
  154. package/guides/change-waits.md +1 -1
  155. package/guides/claude-native.md +101 -0
  156. package/guides/host-ledger.md +3 -1
  157. package/guides/participant.md +14 -7
  158. package/guides/pi-native-delivery.md +49 -0
  159. package/package.json +4 -3
  160. package/dist/claude-hook.d.ts +0 -10
  161. package/dist/claude-hook.js +0 -60
  162. package/dist/paseo-connection.d.ts +0 -15
  163. package/dist/paseo-connection.js +0 -141
  164. package/dist/presence.d.ts +0 -2
  165. package/dist/presence.js +0 -24
  166. package/dist/presented.d.ts +0 -2
  167. package/dist/presented.js +0 -10
  168. package/dist/state-cell.d.ts +0 -14
  169. package/dist/wake-port.d.ts +0 -23
  170. package/dist/wake-port.js +0 -27
  171. /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
@@ -3,22 +3,23 @@ import { setTimeout as sleep } from 'node:timers/promises';
3
3
  import { SquareError } from './model.js';
4
4
  import { validateDoneBody } from './decisions.js';
5
5
  import { currentHold, HELD_WAIT_BUDGET_MS } from './runtime.js';
6
- import { Square, openParticipant } from './square-wiring.js';
6
+ import { Square } from './square-wiring.js';
7
7
  import { openSquare } from './square-file-adapter.js';
8
8
  import { closeOpenSquare } from './open-square.js';
9
- import { statusPresentation, participantsPresentation } from './views.js';
10
- import { hostLedgerForEnv, localParticipantName, localSessionIdentities, readParticipantOwner } from './registry.js';
9
+ import { catchUp, express, ignore, listen, listening } from './square-actions.js';
10
+ import { participantHistory, resolveParticipant, statusPresentation, participantsPresentation } from './views.js';
11
+ import { hostLedgerForEnv, localParticipantName, localSessionIdentities, presenceEpoch, readParticipantOwner } from './registry.js';
11
12
  function requireLocation(context) {
12
13
  const selected = context.squarePath ?? context.env.SQUARE_LOCATION;
13
14
  if (selected === undefined || selected.trim() === '')
14
15
  throw new SquareError('invalid_args', 'Square application context needs a square location');
15
16
  return path.resolve(context.cwd, selected);
16
17
  }
17
- async function requireParticipant(context, squarePath, env = context.env) {
18
+ async function requireParticipant(context, squarePath, hostLedger, env = context.env) {
18
19
  const selected = context.participant ?? env.SQUARE_PARTICIPANT_NAME;
19
20
  if (selected?.trim())
20
21
  return selected;
21
- const discovered = await localParticipantName(squarePath, env).catch(() => undefined);
22
+ const discovered = await localParticipantName(squarePath, hostLedger, env).catch(() => undefined);
22
23
  if (discovered !== undefined)
23
24
  return discovered;
24
25
  throw new SquareError('invalid_args', 'Square application context needs one unambiguous participant name');
@@ -31,25 +32,26 @@ export function createSquareApplication(context) {
31
32
  const env = { ...context.env };
32
33
  const location = () => requireLocation({ ...context, env });
33
34
  const hostLedger = context.hostLedger ?? hostLedgerForEnv(env);
34
- const open = () => Square.at({ path: location(), clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
35
+ const open = (control) => Square.at({ path: location(), clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport }, control);
35
36
  async function existing(operation, control) {
36
37
  checkControl(control);
37
38
  const squarePath = location();
38
- const participantName = await requireParticipant(context, squarePath, env);
39
- const facade = await openParticipant({ path: squarePath, clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport }, participantName);
39
+ const participantName = await requireParticipant(context, squarePath, hostLedger, env);
40
+ const square = await openSquare(squarePath, { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport }, control);
40
41
  try {
42
+ const { name } = await resolveParticipant(square, participantName, control);
41
43
  checkControl(control);
42
- return await operation(facade.participant);
44
+ return await operation(square, name);
43
45
  }
44
46
  finally {
45
- await facade.close();
47
+ await closeOpenSquare(square);
46
48
  }
47
49
  }
48
50
  async function joined(operation, control) {
49
51
  checkControl(control);
50
- const square = await open();
52
+ const square = await open(control);
51
53
  try {
52
- return await operation(await square.join(await requireParticipant(context, square.location, env), control));
54
+ return await operation(await square.join(await requireParticipant(context, square.location, hostLedger, env), control));
53
55
  }
54
56
  finally {
55
57
  await square.close();
@@ -59,14 +61,14 @@ export function createSquareApplication(context) {
59
61
  async join(options = {}, control = {}) {
60
62
  checkControl(control);
61
63
  const squarePath = location();
62
- const square = await open();
64
+ const square = await open(control);
63
65
  try {
64
- const participantName = await requireParticipant(context, squarePath, env);
65
- const before = await square.snapshot();
66
+ const participantName = await requireParticipant(context, squarePath, hostLedger, env);
67
+ const before = await square.snapshot(control);
66
68
  const standing = before.participants.some((item) => item.name.toLocaleLowerCase() === participantName.toLocaleLowerCase() && item.state === 'joined');
67
69
  const identity = localSessionIdentities(env)[0];
68
- const owner = standing ? await readParticipantOwner(squarePath, participantName, env).catch(() => undefined) : undefined;
69
- const reconnect = standing && identity !== undefined && owner?.sessionId === identity.sessionId;
70
+ const owner = standing ? await readParticipantOwner(squarePath, participantName, hostLedger).catch(() => undefined) : undefined;
71
+ const reconnect = standing && identity !== undefined && owner?.session === identity.sessionId;
70
72
  if (standing && owner !== undefined && identity !== undefined && !options.takeover && !reconnect) {
71
73
  throw new SquareError('already_joined', `✕ ${participantName} already stands here — another session holds the name`, { pending: before.participants.length });
72
74
  }
@@ -88,7 +90,7 @@ export function createSquareApplication(context) {
88
90
  while (true) {
89
91
  checkControl(control);
90
92
  try {
91
- const result = await existing((participant) => participant.express(body, options, control), control);
93
+ const result = await existing((square, name) => express(square, name, body, options, control), control);
92
94
  return { ...result, waited };
93
95
  }
94
96
  catch (error) {
@@ -111,7 +113,7 @@ export function createSquareApplication(context) {
111
113
  }
112
114
  else {
113
115
  const waitStarted = Date.now();
114
- const square = await openSquare(location(), { clock: context.clock, env, hostLedger, signal: control.signal });
116
+ const square = await openSquare(location(), { clock: context.clock, env, hostLedger }, control);
115
117
  try {
116
118
  const snapshot = await square.artifact.read(control.signal);
117
119
  if (currentHold(snapshot.state.acts).active)
@@ -125,12 +127,12 @@ export function createSquareApplication(context) {
125
127
  }
126
128
  }
127
129
  },
128
- catch(options, control) { return existing((participant) => participant.catch(options, control), control); },
130
+ catch(options, control) { return existing((square, name) => catchUp(square, name, options, undefined, control), control); },
129
131
  async history(query, control) {
130
132
  checkControl(control);
131
133
  if (context.participant !== undefined || env.SQUARE_PARTICIPANT_NAME !== undefined)
132
- return existing((participant) => participant.history(query, control), control);
133
- const square = await open();
134
+ return existing((square, name) => participantHistory(square, name, query, control), control);
135
+ const square = await open(control);
134
136
  try {
135
137
  checkControl(control);
136
138
  return await square.history(query, control);
@@ -139,21 +141,21 @@ export function createSquareApplication(context) {
139
141
  await square.close();
140
142
  }
141
143
  },
142
- listen(target, control) { return existing((participant) => participant.listen(target, control), control); },
143
- ignore(target, control) { return existing((participant) => participant.ignore(target, control), control); },
144
- listening(control) { return existing((participant) => participant.listening(control), control); },
144
+ listen(target, control) { return existing((square, name) => listen(square, name, target, control), control); },
145
+ ignore(target, control) { return existing((square, name) => ignore(square, name, target, control), control); },
146
+ listening(control) { return existing((square, name) => listening(square, name, control), control); },
145
147
  hold(reason, control) { return joined((participant) => participant.hold(reason, control), control); },
146
148
  resume(control) { return joined((participant) => participant.resume(control), control); },
147
149
  async done(body, control) {
148
150
  validateDoneBody(body);
149
151
  const squarePath = location();
150
- const participantName = await requireParticipant(context, squarePath, env);
152
+ const participantName = await requireParticipant(context, squarePath, hostLedger, env);
151
153
  const identity = localSessionIdentities(env)[0];
152
- const owner = identity === undefined ? undefined : await readParticipantOwner(squarePath, participantName, env).catch(() => undefined);
153
- if (identity !== undefined && owner?.sessionId === identity.sessionId && owner.epoch > 0) {
154
- const square = await open();
154
+ const owner = identity === undefined ? undefined : await readParticipantOwner(squarePath, participantName, hostLedger).catch(() => undefined);
155
+ if (identity !== undefined && owner?.session === identity.sessionId && presenceEpoch(owner) > 0) {
156
+ const square = await open(control);
155
157
  try {
156
- return await square.doneOwnedSession(participantName, body ?? '', owner.epoch, control);
158
+ return await square.doneOwnedSession(participantName, body ?? '', presenceEpoch(owner), control);
157
159
  }
158
160
  finally {
159
161
  await square.close();
@@ -139,8 +139,11 @@ export declare function audienceIncludes(audience: Audience, name: string): bool
139
139
  export declare function resolveAudience(audience: Audience, candidateNames: readonly string[]): string[];
140
140
  export declare function activeListeners(state: FoldedSquareState, sender: string): string[];
141
141
  export declare function isIgnored(state: FoldedSquareState, listener: string, sender: string): boolean;
142
+ /** The author of every landed activity, keyed by index; a reply audience derives from this. */
143
+ export declare function activityAuthors(acts: readonly Act[]): Map<number, Participant>;
142
144
  export declare function listeningTo(state: FoldedSquareState, listener: string): string[];
143
145
  export declare function isListening(state: FoldedSquareState, listener: string, sender: string): boolean;
146
+ export declare function actorOf(act: Act): Participant | undefined;
144
147
  export declare function fold(acts: readonly Act[]): FoldedSquareState;
145
148
  /** Replay the activity stream once to fix every say's audience at landing. */
146
149
  export declare function replayLandedAudiences(acts: readonly Act[]): LandedAudienceReplay;
@@ -60,12 +60,28 @@ export function activeListeners(state, sender) {
60
60
  export function isIgnored(state, listener, sender) {
61
61
  return (state.ignored.get(nameKey(listener)) ?? []).some((target) => sameName(target, sender));
62
62
  }
63
- function recipientsAtLanding(before, say) {
63
+ /** The author of every landed activity, keyed by index; a reply audience derives from this. */
64
+ export function activityAuthors(acts) {
65
+ const authors = new Map();
66
+ for (const act of acts) {
67
+ const actor = actorOf(act);
68
+ if (actor === undefined || !('index' in act) || typeof act.index !== 'number')
69
+ continue;
70
+ authors.set(act.index, actor);
71
+ }
72
+ return authors;
73
+ }
74
+ function recipientsAtLanding(before, say, authors) {
64
75
  const audience = audienceOf(say);
65
- const mentionTargets = resolveAudience(audience, before.participants.filter((participant) => participant.joined).map((participant) => participant.name));
76
+ const standing = before.participants.filter((participant) => participant.joined).map((participant) => participant.name);
77
+ const mentionTargets = resolveAudience(audience, standing);
78
+ // A reply is directed attention in its own right: it reaches the author of the
79
+ // replied-to activity under the same standing and ignore rules as a mention.
80
+ const replyAuthor = say.reply === undefined ? undefined : authors.get(say.reply);
81
+ const replyTargets = replyAuthor === undefined ? [] : resolveAudience({ kind: 'mentions', names: [replyAuthor] }, standing);
66
82
  const listeners = activeListeners(before, say.actor);
67
83
  const recipients = [];
68
- for (const name of [...mentionTargets, ...listeners]) {
84
+ for (const name of [...mentionTargets, ...replyTargets, ...listeners]) {
69
85
  if (sameName(name, say.actor) || recipients.some((existing) => sameName(existing, name)))
70
86
  continue;
71
87
  if (audience.kind !== 'bell' && isIgnored(before, name, say.actor))
@@ -80,7 +96,7 @@ export function listeningTo(state, listener) {
80
96
  export function isListening(state, listener, sender) {
81
97
  return listeningTo(state, listener).some((target) => sameName(target, sender));
82
98
  }
83
- function actorOf(act) {
99
+ export function actorOf(act) {
84
100
  if ('actor' in act && typeof act.actor === 'string')
85
101
  return act.actor;
86
102
  return undefined;
@@ -238,9 +254,10 @@ export function replayLandedAudiences(acts) {
238
254
  const byActivity = new Map();
239
255
  const byIndex = new Map();
240
256
  const lastJoinByKey = new Map();
257
+ const authors = activityAuthors(acts);
241
258
  for (const act of acts) {
242
259
  if (act.kind === 'say') {
243
- const recipients = recipientsAtLanding(accumulator.state, act);
260
+ const recipients = recipientsAtLanding(accumulator.state, act, authors);
244
261
  byActivity.set(act, recipients);
245
262
  const index = 'index' in act ? act.index : undefined;
246
263
  if (typeof index === 'number')
@@ -24,6 +24,8 @@ export interface ExpressOptions {
24
24
  export interface ExpressResult {
25
25
  readonly activity: Activity;
26
26
  readonly delivery?: DeliveryResult;
27
+ /** The replied-to author was not standing when the reply landed, so no notification reached them. */
28
+ readonly replyAuthorNotHere?: string;
27
29
  }
28
30
  export interface ListenerChangeResult {
29
31
  readonly activity: Activity | null;
@@ -1,6 +1,7 @@
1
1
  export { observeSquareRevision as observeSquareChanges } from './artifact.js';
2
2
  import { type BuildOptions, type HardCap } from './model.js';
3
3
  import type { OpenSquare } from './open-square.js';
4
+ import type { OperationControl } from './square-facade.js';
4
5
  import type { HostLedgerPort, SquareArtifactPort } from './ports.js';
5
6
  /** File-owned artifact creation for the CLI and path-backed public facade. */
6
7
  export declare function createSquare(squarePath: string, options: BuildOptions & {
@@ -17,7 +18,7 @@ export interface SquareBuildOptions {
17
18
  /** One caller deadline (e.g. a native hook budget) propagated into artifact busy retries. */
18
19
  signal?: AbortSignal;
19
20
  }
20
- export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env' | 'signal'>): Promise<OpenSquare>;
21
+ export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env' | 'signal'>, control?: OperationControl): Promise<OpenSquare>;
21
22
  export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
22
23
  export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
23
24
  export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
@@ -1,6 +1,6 @@
1
1
  export { observeSquareRevision as observeSquareChanges } from './artifact.js';
2
2
  import { hostLedgerRoot } from './host-ledger-root.js';
3
- import { createSquareState, createSquareSnapshot, probeSquareFile, writeSquareSnapshot, openSquareCell, createMemoryCell, } from './square-storage.js';
3
+ import { createSquareState, createSquareSnapshot, probeSquareFile, writeSquareSnapshot, createFileCell, createMemoryCell, } from './square-storage.js';
4
4
  import { InternalSquareError, SquareError, } from './model.js';
5
5
  import { createHostLedgerPort } from './host-ledger-file-adapter.js';
6
6
  /** File-owned artifact creation for the CLI and path-backed public facade. */
@@ -24,12 +24,12 @@ function validateBuildOptions(options) {
24
24
  throw new SquareError('invalid_args', 'throttlePerMinute must be a positive integer');
25
25
  }
26
26
  }
27
- export async function openSquare(squarePath, options = {}) {
27
+ export async function openSquare(squarePath, options = {}, control) {
28
28
  const env = options.env ?? process.env;
29
29
  const ledgerRoot = hostLedgerRoot(env);
30
30
  const artifact = openSquareArtifact(squarePath, options.signal);
31
31
  try {
32
- await artifact.read();
32
+ await artifact.read(control?.signal);
33
33
  return {
34
34
  artifact,
35
35
  clock: options.clock ?? Date.now,
@@ -51,7 +51,7 @@ export async function openSquare(squarePath, options = {}) {
51
51
  }
52
52
  export async function probeSquare(squarePath) {
53
53
  const state = await probeSquareFile(squarePath);
54
- return state === undefined ? undefined : { artifact: memoryArtifact(createMemoryCell(state)), clock: Date.now, location: squarePath };
54
+ return state === undefined ? undefined : { artifact: createMemoryCell(state), clock: Date.now, location: squarePath, hostLedger: createHostLedgerPort({ rootPath: hostLedgerRoot(process.env) }) };
55
55
  }
56
56
  export async function buildSquare(squarePath, options) {
57
57
  validateBuildOptions(options);
@@ -77,12 +77,9 @@ export function buildMemorySquare(options) {
77
77
  hardCap: options.hardCap ?? null,
78
78
  ...(options.throttlePerMinute === undefined ? {} : { throttlePerMinute: options.throttlePerMinute }),
79
79
  }, options.markdown);
80
- return { artifact: memoryArtifact(createMemoryCell(squareState)), clock: options.clock ?? Date.now, location: 'memory', hostLedger: options.hostLedger, wakeTransport: options.wakeTransport };
80
+ return { artifact: createMemoryCell(squareState), clock: options.clock ?? Date.now, location: 'memory', hostLedger: options.hostLedger, wakeTransport: options.wakeTransport };
81
81
  }
82
82
  /** A projection reads and validates its snapshot once; no preliminary duplicate read. */
83
83
  export function openSquareArtifact(squarePath, signal) {
84
- return memoryArtifact(openSquareCell(squarePath, signal));
85
- }
86
- function memoryArtifact(cell) {
87
- return { read: (signal) => cell.read(signal), transact: (fn, signal) => cell.transact(fn, signal), changed: (since, timeout, signal) => cell.changed(since, timeout, signal), close: () => cell.close() };
84
+ return createFileCell(squarePath, signal);
88
85
  }
@@ -1,28 +1,56 @@
1
1
  import { type InboxMembership, type SquareState } from './model.js';
2
2
  import type { HostLedgerPort, SquareArtifactPort, PresentationEvidenceProjection, PresentationProjection, SessionBindingProjection } from './ports.js';
3
3
  import { deriveDeliveryModel } from './delivery.js';
4
- import type { WakeRoute, WakeRouteKind } from './model.js';
4
+ import { type WakeEligibility } from './wake-eligibility.js';
5
5
  /** A fresh blocking catch owns only the notifications admitted by its filter. */
6
6
  export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
7
- export declare function projectSessionBindings(input: {
7
+ /**
8
+ * One eligibility projection for a freshly read session inbox: a fresh catch lease owns what it
9
+ * admitted, and a delivered (or clipped) boundary already raised awareness for its activity, so
10
+ * neither belongs at the next boundary. Ordering, session scoping, suppression outcomes and
11
+ * ledger errors stay exactly as the boundary callers read them.
12
+ */
13
+ export declare function projectBoundaryEligibility(input: {
8
14
  readonly hostLedger: HostLedgerPort;
9
15
  readonly sessionId: string;
16
+ readonly inbox: readonly InboxMembership[];
17
+ }): Promise<InboxMembership[]>;
18
+ export declare function projectSessionBindings(input: {
19
+ readonly hostLedger: HostLedgerPort;
20
+ readonly sessionId?: string;
10
21
  readonly location?: string;
11
22
  readonly now?: number;
12
23
  }): Promise<readonly SessionBindingProjection[]>;
13
- export declare function sessionIdsFromEnvironment(env?: NodeJS.ProcessEnv): readonly string[];
14
- export declare function projectLocalParticipantBinding(input: {
15
- readonly hostLedger: HostLedgerPort;
16
- readonly location: string;
17
- readonly participant: string;
18
- readonly sessionIds: readonly string[];
19
- readonly now?: number;
20
- }): Promise<SessionBindingProjection | undefined>;
21
24
  export declare function projectPresentation(input: {
22
25
  readonly artifact: SquareArtifactPort;
23
26
  readonly binding: SessionBindingProjection;
24
27
  readonly now?: number;
25
28
  }): Promise<PresentationProjection>;
29
+ export interface NativePendingPreview {
30
+ readonly payload: string;
31
+ readonly clipped: boolean;
32
+ }
33
+ /** The one native pending preview. `epoch` matches exactly, and each harness pins its own address entry. */
34
+ export type NativePendingPreviewInput = {
35
+ readonly location: string;
36
+ readonly sessionId: string;
37
+ readonly participant: string;
38
+ readonly actIndex: number;
39
+ readonly epoch?: number;
40
+ readonly cancelledThrough?: number;
41
+ readonly now: number;
42
+ } & ({
43
+ readonly routeKind: 'claude-native';
44
+ readonly address: {
45
+ readonly endpoint: string;
46
+ };
47
+ } | {
48
+ readonly routeKind: 'opencode-server';
49
+ readonly address: {
50
+ readonly sessionId: string;
51
+ };
52
+ });
53
+ export declare function nativePendingPreview(state: SquareState, input: NativePendingPreviewInput): NativePendingPreview | undefined;
26
54
  export declare function projectPresentationEvidence(input: {
27
55
  readonly hostLedger: HostLedgerPort;
28
56
  readonly location?: string;
@@ -33,44 +61,11 @@ export declare function projectPresentationEvidence(input: {
33
61
  }): Promise<readonly PresentationEvidenceProjection[]>;
34
62
  /** Successful boundary output, including a clipped preview, suppresses awareness wake. */
35
63
  export declare function presentationSuppressesWake(evidence: readonly Pick<PresentationEvidenceProjection, 'outcome'>[]): boolean;
36
- /** The most recently published session owns wake eligibility for its participant. */
37
- export declare function currentSessionBindings<T extends {
38
- readonly participant: string;
39
- readonly updatedAt?: number;
40
- }>(bindings: readonly T[]): T[];
41
- export interface WakeAttempt {
42
- readonly at: number;
43
- readonly attention: {
44
- readonly squarePath: string;
45
- readonly actIndex: number;
46
- readonly recipient: string;
47
- };
48
- readonly routeKind: WakeRouteKind;
49
- readonly outcome: 'accepted' | 'unknown' | 'failed';
50
- readonly signature?: string;
51
- readonly attemptN: number;
52
- readonly session?: string;
53
- readonly message?: string;
54
- readonly diagnostic?: unknown;
55
- }
56
- export declare function terminalWakeEvidence(attempts: readonly WakeAttempt[]): WakeAttempt | undefined;
57
- export declare function isWakeRouteAttemptable(route: Pick<WakeRoute, 'kind' | 'updatedAt'>, attempts: readonly WakeAttempt[]): boolean;
58
- export declare function hasAttemptableWakeRoute(routes: readonly Pick<WakeRoute, 'kind' | 'updatedAt'>[], attempts: readonly WakeAttempt[]): boolean;
59
- export interface WakeEvidence {
60
- readonly delivered: boolean;
61
- readonly presented: boolean;
62
- readonly attempts: readonly WakeAttempt[];
63
- readonly terminal?: WakeAttempt;
64
- readonly attemptableRoutes: readonly WakeRoute[];
65
- }
66
- export interface WakeEvidenceProjection {
67
- evidence(recipient: string, actIndex: number): WakeEvidence;
68
- }
64
+ /** Read primary evidence once; wake behavior belongs to the pure eligibility projection. */
69
65
  export declare function projectWakeEvidenceFromState(input: {
70
66
  readonly location: string;
71
67
  readonly state: SquareState;
72
68
  readonly hostLedger: HostLedgerPort;
73
69
  readonly now: number;
74
70
  readonly delivery?: ReturnType<typeof deriveDeliveryModel>;
75
- }): Promise<WakeEvidenceProjection>;
76
- export declare function wakeIsEligible(evidence: WakeEvidence): boolean;
71
+ }): Promise<WakeEligibility>;
@@ -1,8 +1,10 @@
1
- import { formatActivityId, parseActivityId } from './square-core.js';
1
+ import { formatActivityId } from './square-core.js';
2
2
  import { nameKey } from './model.js';
3
3
  import { deriveDeliveryModel, leaseOwnsNotification } from './delivery.js';
4
4
  import { freshWatchLease } from './runtime.js';
5
- import { canonicalRouteLocation } from './routes.js';
5
+ import { attentionBodyIsClipped, renderAttentionPreview } from './attention-presentation.js';
6
+ import { canonicalPath } from './canonical-path.js';
7
+ import { projectWakeEligibility } from './wake-eligibility.js';
6
8
  /** A fresh blocking catch owns only the notifications admitted by its filter. */
7
9
  export function pendingAtBoundary(inbox) {
8
10
  return inbox.map((membership) => {
@@ -12,37 +14,48 @@ export function pendingAtBoundary(inbox) {
12
14
  return { ...membership, notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })) };
13
15
  }).filter((membership) => membership.notifications.length > 0);
14
16
  }
15
- function bindingProjection(record) {
16
- return {
17
+ /**
18
+ * One eligibility projection for a freshly read session inbox: a fresh catch lease owns what it
19
+ * admitted, and a delivered (or clipped) boundary already raised awareness for its activity, so
20
+ * neither belongs at the next boundary. Ordering, session scoping, suppression outcomes and
21
+ * ledger errors stay exactly as the boundary callers read them.
22
+ */
23
+ export async function projectBoundaryEligibility(input) {
24
+ const memberships = pendingAtBoundary([...input.inbox]);
25
+ if (memberships.length === 0)
26
+ return [];
27
+ const evidence = await projectPresentationEvidence({ hostLedger: input.hostLedger, sessionId: input.sessionId });
28
+ const presented = new Map();
29
+ for (const row of evidence) {
30
+ if (row.outcome !== 'presented' && row.outcome !== 'clipped')
31
+ continue;
32
+ const key = JSON.stringify([row.location, nameKey(row.participant)]);
33
+ let activities = presented.get(key);
34
+ if (activities === undefined)
35
+ presented.set(key, activities = new Set());
36
+ activities.add(row.activity);
37
+ }
38
+ const pending = [];
39
+ for (const membership of memberships) {
40
+ const key = JSON.stringify([await canonicalPath(membership.squarePath), nameKey(membership.name)]);
41
+ const activities = presented.get(key);
42
+ const notifications = membership.notifications.filter((notification) => !activities?.has(formatActivityId(notification.actIndex)));
43
+ if (notifications.length > 0)
44
+ pending.push({ ...membership, notifications });
45
+ }
46
+ return pending;
47
+ }
48
+ export async function projectSessionBindings(input) {
49
+ const rows = await input.hostLedger.listPresence({ location: input.location, session: input.sessionId, now: input.now });
50
+ return rows.map((record) => ({
17
51
  location: record.location,
18
52
  participant: record.participant,
19
53
  sessionId: record.session,
20
54
  channel: record.channel,
21
- ...(record.route === undefined ? {} : {
22
- route: {
23
- location: record.location,
24
- participant: record.participant,
25
- sessionId: record.session,
26
- channel: record.channel,
27
- kind: record.route.kind,
28
- address: { ...record.route.address },
29
- updatedAt: record.updatedAt ?? 0,
30
- },
31
- }),
55
+ ...(record.epoch === undefined ? {} : { epoch: record.epoch }),
56
+ ...(record.cancelledThrough === undefined ? {} : { cancelledThrough: record.cancelledThrough }),
32
57
  updatedAt: record.updatedAt ?? 0,
33
- };
34
- }
35
- export async function projectSessionBindings(input) {
36
- const rows = await input.hostLedger.listPresence({ location: input.location, session: input.sessionId, now: input.now });
37
- return rows.map(bindingProjection);
38
- }
39
- export function sessionIdsFromEnvironment(env = process.env) {
40
- return [env.CLAUDE_CODE_SESSION_ID, env.CODEX_THREAD_ID, env.OPENCODE_SESSION_ID, env.PI_SESSION_ID, env.PASEO_AGENT_ID]
41
- .map((value) => value?.trim()).filter((value) => Boolean(value));
42
- }
43
- export async function projectLocalParticipantBinding(input) {
44
- const bindings = (await Promise.all(input.sessionIds.map((sessionId) => projectSessionBindings({ hostLedger: input.hostLedger, location: input.location, sessionId, now: input.now })))).flat();
45
- return bindings.find((binding) => binding.participant.toLocaleLowerCase() === input.participant.toLocaleLowerCase());
58
+ }));
46
59
  }
47
60
  export async function projectPresentation(input) {
48
61
  const { state } = await input.artifact.read();
@@ -55,6 +68,18 @@ export async function projectPresentation(input) {
55
68
  const lease = freshWatchLease(state, known, input.now ?? Date.now());
56
69
  return { binding, joined: true, notifications, ...(lease === undefined ? {} : { catchLease: lease }) };
57
70
  }
71
+ export function nativePendingPreview(state, input) {
72
+ const route = state.routes?.find((candidate) => candidate.kind === input.routeKind && candidate.sessionId === input.sessionId
73
+ && candidate.epoch === input.epoch && nameKey(candidate.participant) === nameKey(input.participant)
74
+ && (input.routeKind === 'claude-native' ? candidate.address.endpoint === input.address.endpoint : candidate.address.sessionId === input.address.sessionId));
75
+ if (!route || input.actIndex <= (input.cancelledThrough ?? -1))
76
+ return undefined;
77
+ const notification = deriveDeliveryModel(state).pendingFor(input.participant).find((entry) => entry.item.index === input.actIndex);
78
+ const lease = freshWatchLease(state, input.participant, input.now);
79
+ if (!notification || (lease !== undefined && leaseOwnsNotification(lease, { ...notification.item, recipient: input.participant, route: notification.route })))
80
+ return undefined;
81
+ return { payload: renderAttentionPreview({ squarePath: input.location, recipient: input.participant, actIndex: input.actIndex, actor: notification.item.actor, route: notification.route, body: notification.item.body }), clipped: attentionBodyIsClipped(notification.item.body) };
82
+ }
58
83
  export async function projectPresentationEvidence(input) {
59
84
  const rows = await input.hostLedger.listEvidence({ kind: 'presentation', location: input.location, participant: input.participant, session: input.sessionId, activity: input.activity, now: input.now });
60
85
  return rows.map((row) => ({ location: row.location, participant: row.participant, sessionId: row.session, activity: row.activity, outcome: row.outcome, ...(row.at === undefined ? {} : { at: row.at }) }));
@@ -63,52 +88,13 @@ export async function projectPresentationEvidence(input) {
63
88
  export function presentationSuppressesWake(evidence) {
64
89
  return evidence.some((row) => row.outcome === 'clipped' || row.outcome === 'presented');
65
90
  }
66
- /** The most recently published session owns wake eligibility for its participant. */
67
- export function currentSessionBindings(bindings) {
68
- const latest = new Map();
69
- for (const binding of bindings) {
70
- const participant = nameKey(binding.participant);
71
- latest.set(participant, Math.max(latest.get(participant) ?? Number.NEGATIVE_INFINITY, binding.updatedAt ?? 0));
72
- }
73
- return bindings.filter((binding) => (binding.updatedAt ?? 0) === latest.get(nameKey(binding.participant)));
74
- }
75
- export function terminalWakeEvidence(attempts) { return attempts.findLast((attempt) => attempt.outcome === 'accepted'); }
76
- export function isWakeRouteAttemptable(route, attempts) {
77
- if (terminalWakeEvidence(attempts) !== undefined)
78
- return false;
79
- if (attempts.some((attempt) => attempt.routeKind === route.kind && attempt.outcome === 'unknown'))
80
- return false;
81
- return true;
82
- }
83
- export function hasAttemptableWakeRoute(routes, attempts) { return routes.some((route) => isWakeRouteAttemptable(route, attempts)); }
91
+ /** Read primary evidence once; wake behavior belongs to the pure eligibility projection. */
84
92
  export async function projectWakeEvidenceFromState(input) {
85
- const canonicalLocation = await canonicalRouteLocation(input.location);
93
+ const canonicalLocation = await canonicalPath(input.location);
86
94
  const delivery = input.delivery ?? deriveDeliveryModel(input.state);
87
- const bindings = (input.state.routes ?? [])
88
- .filter((route) => route.location === canonicalLocation || route.location === input.location)
89
- .map((route) => ({ location: route.location, participant: route.participant, sessionId: route.sessionId, channel: route.channel, route: { ...route, address: { ...route.address } }, updatedAt: route.updatedAt }));
90
- const wakeRecords = await input.hostLedger.listEvidence({ location: canonicalLocation, kind: 'wake', now: input.now });
91
- const attemptsByBinding = new Map();
92
- for (const record of wakeRecords) {
93
- const actIndex = parseActivityId(record.activity);
94
- if (actIndex === undefined || record.routeKind === undefined || typeof record.attemptN !== 'number')
95
- continue;
96
- const attempt = { attention: { squarePath: record.location, recipient: record.participant, actIndex }, outcome: record.outcome, at: record.at ?? input.now, routeKind: record.routeKind, attemptN: record.attemptN, ...(record.signature === undefined ? {} : { signature: record.signature }), ...(record.session === undefined ? {} : { session: record.session }), ...(record.message === undefined ? {} : { message: record.message }), ...(record.diagnostic === undefined ? {} : { diagnostic: record.diagnostic }) };
97
- const key = JSON.stringify([nameKey(record.participant), actIndex, record.session]);
98
- const existing = attemptsByBinding.get(key) ?? [];
99
- existing.push(attempt);
100
- attemptsByBinding.set(key, existing);
101
- }
102
- const presentedRows = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: canonicalLocation, now: input.now });
103
- return {
104
- evidence(recipient, actIndex) {
105
- const recipientBindings = currentSessionBindings(bindings.filter((binding) => nameKey(binding.participant) === nameKey(recipient)));
106
- const routes = recipientBindings.flatMap((binding) => binding.route === undefined ? [] : [binding.route]);
107
- const attempts = recipientBindings.flatMap((binding) => attemptsByBinding.get(JSON.stringify([nameKey(recipient), actIndex, binding.sessionId])) ?? []);
108
- const terminal = terminalWakeEvidence(attempts);
109
- const presented = presentedRows.some((row) => row.activity === formatActivityId(actIndex) && row.participant.toLocaleLowerCase() === recipient.toLocaleLowerCase() && recipientBindings.some((binding) => binding.sessionId === row.sessionId) && presentationSuppressesWake([row]));
110
- return { delivered: delivery.isSeen(recipient, actIndex), presented, attempts, ...(terminal === undefined ? {} : { terminal }), attemptableRoutes: terminal === undefined ? routes.filter((route) => isWakeRouteAttemptable(route, attempts)) : [] };
111
- },
112
- };
95
+ const owners = await input.hostLedger.listPresence({ location: canonicalLocation, now: input.now });
96
+ const evidence = await input.hostLedger.listEvidence({ location: canonicalLocation, now: input.now });
97
+ const wakeRecords = evidence.filter((row) => row.kind === 'wake');
98
+ const presentations = evidence.filter((row) => row.kind === 'presentation').map((row) => ({ location: row.location, participant: row.participant, sessionId: row.session, activity: row.activity, outcome: row.outcome, ...(row.at === undefined ? {} : { at: row.at }) }));
99
+ return projectWakeEligibility({ state: input.state, location: canonicalLocation, owners, wakeRecords, presentations, now: input.now, delivery });
113
100
  }
114
- export function wakeIsEligible(evidence) { return !evidence.delivered && !evidence.presented && evidence.terminal === undefined && evidence.attemptableRoutes.length > 0; }
@@ -25,9 +25,6 @@ function isNonblankString(value) {
25
25
  function isStringArray(value) {
26
26
  return Array.isArray(value) && value.every((item) => typeof item === 'string');
27
27
  }
28
- function validateObservation(value) {
29
- return isObject(value) && hasExactKeys(value, ['state', 'at']) && value.state === 'seen' && isFiniteNumber(value.at);
30
- }
31
28
  function validateWatchLease(value) {
32
29
  if (!isObject(value) || !hasExactKeys(value, ['leaseId', 'heartbeatAt', 'expiresAt'], ['filter'])
33
30
  || !isNonblankString(value.leaseId) || !isFiniteNumber(value.heartbeatAt)
@@ -46,7 +43,7 @@ function validateRuntime(value) {
46
43
  return isObject(value) && hasExactKeys(value, ['nextActIndex', 'observations', 'leases'])
47
44
  && isNonNegativeInteger(value.nextActIndex)
48
45
  && validateRecord(value.observations, (candidate) => isObject(candidate)
49
- && Object.entries(candidate).every(([id, observation]) => parseActivityId(id) !== undefined && validateObservation(observation)))
46
+ && Object.entries(candidate).every(([id, observation]) => parseActivityId(id) !== undefined && isObject(observation) && hasExactKeys(observation, ['state', 'at']) && observation.state === 'seen' && isFiniteNumber(observation.at)))
50
47
  && validateRecord(value.leases, validateWatchLease);
51
48
  }
52
49
  function validateAssignedRuntimeReferences(runtime) {
@@ -1,17 +1,14 @@
1
1
  import { createSquareState, diagnoseSquareFile as diagnoseArtifactFile } from './artifact.js';
2
2
  import type { SquareState } from './model.js';
3
- import type { StateCell } from './state-cell.js';
3
+ import type { SquareArtifactPort } from './ports.js';
4
4
  export { createSquareState };
5
- export declare function readSquareFile(squarePath: string): Promise<SquareState>;
6
5
  export declare function probeSquareFile(squarePath: string): Promise<SquareState | undefined>;
7
6
  export declare function diagnoseSquareFile(squarePath: string): ReturnType<typeof diagnoseArtifactFile>;
8
7
  /** Test-helper snapshot replacement backed by one SQLite artifact. */
9
8
  export declare function writeSquareSnapshot(squarePath: string, squareState: SquareState): Promise<void>;
10
9
  /** Race-safe non-force creation for the file adapter. */
11
10
  export declare function createSquareSnapshot(squarePath: string, squareState: SquareState): Promise<boolean>;
12
- /** In-process cell for fast behavior tests and embedded consumers. */
13
- export declare function createMemoryCell(initial: SquareState): StateCell;
14
- /** SQLite-backed cell. Revisions are read from the authoritative database, not file metadata. */
15
- export declare function createFileCell(squarePath: string, externalSignal?: AbortSignal): StateCell;
16
- /** Consumer-facing file cell factory; keeps SQLite framing behind this module. */
17
- export declare function openSquareCell(squarePath: string, signal?: AbortSignal): StateCell;
11
+ /** In-process artifact for fast behavior tests and embedded consumers. */
12
+ export declare function createMemoryCell(initial: SquareState): SquareArtifactPort;
13
+ /** SQLite-backed artifact. Revisions are read from the authoritative database, not file metadata. */
14
+ export declare function createFileCell(squarePath: string, externalSignal?: AbortSignal): SquareArtifactPort;