@astrosheep/square 0.3.30 → 0.3.32

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 (84) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/hooks/hooks.json +0 -3
  3. package/claude-plugin/skills/square/SKILL.md +5 -4
  4. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  5. package/dist/activity.js +8 -3
  6. package/dist/artifact.js +10 -51
  7. package/dist/automatic-session.js +49 -14
  8. package/dist/boundary-presentation.d.ts +1 -1
  9. package/dist/boundary-presentation.js +58 -14
  10. package/dist/catch-decisions.d.ts +17 -0
  11. package/dist/catch-decisions.js +53 -0
  12. package/dist/claude-hook.d.ts +2 -1
  13. package/dist/claude-hook.js +5 -2
  14. package/dist/cli/context.js +10 -2
  15. package/dist/cli/observation-commands.d.ts +5 -1
  16. package/dist/cli/observation-commands.js +68 -33
  17. package/dist/cli/square-commands.js +18 -12
  18. package/dist/codex-hook.d.ts +2 -1
  19. package/dist/codex-hook.js +5 -2
  20. package/dist/codex-queue.js +1 -1
  21. package/dist/decisions.js +2 -2
  22. package/dist/delivery-health.d.ts +1 -1
  23. package/dist/delivery-operations.d.ts +25 -0
  24. package/dist/delivery-operations.js +207 -0
  25. package/dist/delivery.d.ts +1 -0
  26. package/dist/help.js +1 -1
  27. package/dist/host-ledger-file-adapter.d.ts +34 -0
  28. package/dist/host-ledger-file-adapter.js +156 -0
  29. package/dist/host-ledger.d.ts +160 -0
  30. package/dist/host-ledger.js +1 -0
  31. package/dist/inbox.d.ts +2 -2
  32. package/dist/inbox.js +22 -14
  33. package/dist/index.d.ts +4 -1
  34. package/dist/index.js +1 -0
  35. package/dist/landing.d.ts +18 -14
  36. package/dist/landing.js +29 -113
  37. package/dist/model.d.ts +9 -17
  38. package/dist/model.js +2 -2
  39. package/dist/notifications.d.ts +12 -10
  40. package/dist/notifications.js +122 -190
  41. package/dist/open-square.d.ts +5 -4
  42. package/dist/open-square.js +1 -1
  43. package/dist/paseo-delivery.js +2 -0
  44. package/dist/ports.d.ts +138 -0
  45. package/dist/ports.js +1 -0
  46. package/dist/presence.d.ts +1 -2
  47. package/dist/presence.js +13 -46
  48. package/dist/presentation-operations.d.ts +3 -0
  49. package/dist/presentation-operations.js +51 -0
  50. package/dist/presentation.d.ts +2 -2
  51. package/dist/presentation.js +11 -7
  52. package/dist/presented.d.ts +1 -12
  53. package/dist/presented.js +6 -75
  54. package/dist/registry.d.ts +10 -10
  55. package/dist/registry.js +47 -113
  56. package/dist/routes.d.ts +27 -30
  57. package/dist/routes.js +88 -151
  58. package/dist/runtime.d.ts +1 -1
  59. package/dist/runtime.js +3 -4
  60. package/dist/square-actions.d.ts +33 -0
  61. package/dist/square-actions.js +204 -0
  62. package/dist/square-facade.d.ts +10 -7
  63. package/dist/square-file-adapter.d.ts +5 -3
  64. package/dist/square-file-adapter.js +23 -5
  65. package/dist/square-projections.d.ts +68 -0
  66. package/dist/square-projections.js +92 -0
  67. package/dist/square-storage.d.ts +2 -2
  68. package/dist/square-storage.js +2 -2
  69. package/dist/square-wiring.d.ts +3 -3
  70. package/dist/square-wiring.js +44 -29
  71. package/dist/views.d.ts +8 -2
  72. package/dist/views.js +28 -24
  73. package/dist/wake-attempts.d.ts +29 -22
  74. package/dist/wake-attempts.js +36 -119
  75. package/dist/wake-evidence.d.ts +6 -18
  76. package/dist/wake-evidence.js +17 -80
  77. package/dist/wake-port.js +1 -1
  78. package/dist/wakes.d.ts +2 -16
  79. package/dist/wakes.js +7 -27
  80. package/dist/watch.js +2 -3
  81. package/extensions/square-pi.js +7 -2
  82. package/package.json +1 -1
  83. package/skills/brainstorm/SKILL.md +2 -2
  84. package/skills/square/SKILL.md +5 -4
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.30",
3
+ "version": "0.3.32",
4
4
  "description": "Native Claude Code turn-boundary delivery for Square participants",
5
5
  "author": {
6
6
  "name": "Square"
@@ -4,9 +4,6 @@
4
4
  "SessionStart": [
5
5
  { "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }
6
6
  ],
7
- "SessionResume": [
8
- { "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }
9
- ],
10
7
  "SessionEnd": [
11
8
  { "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }
12
9
  ],
@@ -61,7 +61,7 @@ The ownership boundary belongs here. @bob, does this match your read?
61
61
  EOF
62
62
  ```
63
63
 
64
- **Addressing.** Normally address whoever needs the activity with `@name`: mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. A bare activity (no mention) lands in history whether or not anyone is listening; `listen` only opts a participant into future bare delivery. Use `--bell` only when every participant needs it. Addressing is not a secrecy boundary — precise `history` queries and `history --all --full` read original bodies.
64
+ **Addressing.** Normally address whoever needs the activity with `@name`: mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. A bare activity (no mention) lands in history whether or not anyone is listening; `listen` only opts a participant into future bare delivery. Use `--bell` only when every participant needs it. Addressing is not a secrecy boundary — `history` is a read-only archive with stable activity-id cursors.
65
65
 
66
66
  **Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter out — express only when another participant needs the thought, question, or decision.
67
67
 
@@ -98,13 +98,14 @@ square --location <square> --as <name> ignore <participant>
98
98
 
99
99
  ```bash
100
100
  square history --limit 5 # most recent 5, oldest to newest
101
+ square history --before act/12 --limit 5 # the page before act/12
102
+ square history --after act/12 --limit 5 # the page after act/12
101
103
  square history --limit 5 --order desc # newest first
102
- square history --all # every entry
103
- square history --full # expand bodies in range
104
+ square history --no-truncate # expand preview bodies
104
105
  square history --grep 'term' # search
105
106
  ```
106
107
 
107
- See `square history --help` for advanced usage. Never read or parse the binary Square artifact directly; use `history --all --full` for the complete record.
108
+ See `square history --help` for advanced usage. Never read or parse the binary Square artifact directly. Bodies are previews by default; follow the printed activity-id command to continue page by page.
108
109
 
109
110
  ## Hold
110
111
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.30",
3
+ "version": "0.3.32",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
package/dist/activity.js CHANGED
@@ -5,13 +5,14 @@ import { setTimeout as sleep } from 'node:timers/promises';
5
5
  import { SquareError, isSquareError, validateName } from './model.js';
6
6
  import { expressHintLine, renderActivityBlocked, renderActivityLimit, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, withPathOutput, } from './presentation.js';
7
7
  import { nowMs, SLEEP_MS } from './runtime.js';
8
- import { wakeNotifierForSquare } from './notifications.js';
9
8
  import { openSquare } from './square-file-adapter.js';
10
9
  import { closeOpenSquare } from './open-square.js';
11
10
  import { Square } from './square-wiring.js';
12
11
  import { activityPresentation, resolveParticipant } from './views.js';
13
12
  import { formatActivityId } from './square-core.js';
14
13
  import { formatTimestamp } from './time.js';
14
+ import { createHostLedgerPort } from './host-ledger-file-adapter.js';
15
+ import { createDefaultWakeTransport } from './notifications.js';
15
16
  function draftDirFor(squarePath) {
16
17
  return path.join(path.dirname(squarePath), 'drafts');
17
18
  }
@@ -67,7 +68,9 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
67
68
  const noWait = opts.noWait ?? false;
68
69
  const reach = opts.reach === 'bell' ? 'bell' : undefined;
69
70
  let announcedWait;
70
- const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
71
+ const ledgerRoot = process.env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(process.env.SQUARE_REGISTRY);
72
+ const hostLedger = createHostLedgerPort({ userPath: process.env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot, localPath: process.env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot });
73
+ const square = await Square.at({ path: squarePath, clock: nowMs, hostLedger, wakeTransport: await createDefaultWakeTransport(hostLedger, nowMs) });
71
74
  try {
72
75
  const participant = await square.join(name);
73
76
  while (true) {
@@ -76,11 +79,13 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
76
79
  const pendingPublic = before.pendingPublic;
77
80
  const pendingRoomChanges = before.pendingRoomChanges;
78
81
  try {
79
- await participant.express(body, {
82
+ const result = await participant.express(body, {
80
83
  force,
81
84
  ...(reach === undefined ? {} : { reach }),
82
85
  ...(opts.reply === undefined ? {} : { reply: formatActivityId(opts.reply) }),
83
86
  });
87
+ if (result.delivery?.notCapable)
88
+ process.stderr.write(`! wake not-capable: ${result.delivery.notCapable}\n`);
84
89
  const freshSquare = await openSquare(squarePath, { clock: nowMs });
85
90
  const fresh = await activityPresentation(freshSquare, knownName).finally(() => closeOpenSquare(freshSquare));
86
91
  const headerCount = fresh.participantCount;
package/dist/artifact.js CHANGED
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { TextDecoder } from 'node:util';
5
5
  import zlib from 'node:zlib';
6
- import { isWakeRouteKind, InternalSquareError, nameKey, SquareError, } from './model.js';
6
+ import { isWakeRouteKind, InternalSquareError, SquareError, } from './model.js';
7
7
  import { parseActivityId } from './square-core.js';
8
8
  const SQUARE_MAGIC = Buffer.from('SQUARE01', 'ascii');
9
9
  const LENGTH_BYTES = 4;
@@ -37,16 +37,14 @@ function isStringArray(value) {
37
37
  }
38
38
  function validateObservation(value) {
39
39
  return isObject(value)
40
- && hasExactKeys(value, ['state', 'at'], ['ownerId'])
41
- && (value.state === 'notified' || value.state === 'seen')
42
- && isFiniteNumber(value.at)
43
- && (value.ownerId === undefined || isNonblankString(value.ownerId));
40
+ && hasExactKeys(value, ['state', 'at'])
41
+ && value.state === 'seen'
42
+ && isFiniteNumber(value.at);
44
43
  }
45
44
  function validateWatchLease(value) {
46
45
  if (!isObject(value)
47
- || !hasExactKeys(value, ['leaseId', 'heartbeatAt', 'expiresAt'], ['ownerId', 'filter'])
46
+ || !hasExactKeys(value, ['leaseId', 'heartbeatAt', 'expiresAt'], ['filter'])
48
47
  || !isNonblankString(value.leaseId)
49
- || (value.ownerId !== undefined && !isNonblankString(value.ownerId))
50
48
  || !isFiniteNumber(value.heartbeatAt)
51
49
  || !isFiniteNumber(value.expiresAt)
52
50
  || value.expiresAt < value.heartbeatAt)
@@ -58,49 +56,16 @@ function validateWatchLease(value) {
58
56
  && (value.filter.participants === undefined || isStringArray(value.filter.participants))
59
57
  && (value.filter.mention === undefined || typeof value.filter.mention === 'string');
60
58
  }
61
- function validateNotifyLease(value) {
62
- if (!isObject(value)
63
- || !hasExactKeys(value, ['leaseId', 'expiresAt', 'phase'], ['attemptN', 'routeKind'])
64
- || !isNonblankString(value.leaseId)
65
- || !isFiniteNumber(value.expiresAt)
66
- || (value.phase !== 'claimed' && value.phase !== 'dispatching')
67
- || (value.attemptN !== undefined && (!Number.isSafeInteger(value.attemptN) || value.attemptN <= 0))
68
- || (value.routeKind !== undefined && !isWakeRouteKind(value.routeKind)))
69
- return false;
70
- return value.phase !== 'dispatching' || (value.attemptN !== undefined && value.routeKind !== undefined);
71
- }
72
59
  function validateRecord(value, item) {
73
60
  return isObject(value)
74
61
  && Object.entries(value).every(([key, candidate]) => key.length > 0 && item(candidate));
75
62
  }
76
- function parseNotifyLeaseKey(key) {
77
- let parsed;
78
- try {
79
- parsed = JSON.parse(key);
80
- }
81
- catch {
82
- return undefined;
83
- }
84
- if (!Array.isArray(parsed) || parsed.length !== 2)
85
- return undefined;
86
- const [id, name] = parsed;
87
- if (typeof id !== 'string' || typeof name !== 'string' || name.length === 0 || nameKey(name) !== name) {
88
- return undefined;
89
- }
90
- const index = parseActivityId(id);
91
- if (index === undefined)
92
- return undefined;
93
- if (JSON.stringify([id, name]) !== key)
94
- return undefined;
95
- return { index, name };
96
- }
97
63
  function validateRuntime(value) {
98
64
  if (!isObject(value)
99
- || !hasExactKeys(value, ['nextActIndex', 'observations', 'leases', 'notifyLeases'])
65
+ || !hasExactKeys(value, ['nextActIndex', 'observations', 'leases'])
100
66
  || !isNonNegativeInteger(value.nextActIndex)
101
67
  || !validateRecord(value.observations, (candidate) => isObject(candidate) && Object.entries(candidate).every(([id, observation]) => parseActivityId(id) !== undefined && validateObservation(observation)))
102
- || !validateRecord(value.leases, validateWatchLease)
103
- || !validateRecord(value.notifyLeases, validateNotifyLease))
68
+ || !validateRecord(value.leases, validateWatchLease))
104
69
  return false;
105
70
  return true;
106
71
  }
@@ -115,13 +80,6 @@ function validateAssignedRuntimeReferences(runtime) {
115
80
  return 'future';
116
81
  }
117
82
  }
118
- for (const key of Object.keys(runtime.notifyLeases)) {
119
- const parsed = parseNotifyLeaseKey(key);
120
- if (parsed === undefined)
121
- return 'malformed';
122
- if (parsed.index >= bound)
123
- return 'future';
124
- }
125
83
  return 'ok';
126
84
  }
127
85
  function validateActor(value, required) {
@@ -180,13 +138,14 @@ function validateActs(value) {
180
138
  }
181
139
  function validateSquareState(value) {
182
140
  if (!isObject(value)
183
- || !hasExactKeys(value, ['hardCap', 'preamble', 'warmup', 'acts', 'runtime'], ['throttlePerMinute'])
141
+ || !hasExactKeys(value, ['hardCap', 'preamble', 'warmup', 'acts', 'runtime'], ['throttlePerMinute', 'routes'])
184
142
  || !(value.hardCap === null || (Number.isSafeInteger(value.hardCap) && value.hardCap > 0))
185
143
  || (value.throttlePerMinute !== undefined
186
144
  && (!Number.isSafeInteger(value.throttlePerMinute) || value.throttlePerMinute <= 0))
187
145
  || !isStringArray(value.preamble)
188
146
  || !isStringArray(value.warmup)
189
147
  || !validateActs(value.acts)
148
+ || (value.routes !== undefined && (!Array.isArray(value.routes) || !value.routes.every((route) => isObject(route) && isWakeRouteKind(route.kind) && typeof route.location === 'string' && typeof route.participant === 'string' && typeof route.sessionId === 'string' && typeof route.channel === 'string' && isObject(route.address) && Object.values(route.address).every((item) => typeof item === 'string') && typeof route.updatedAt === 'number')))
190
149
  || !validateRuntime(value.runtime)) {
191
150
  throw invalidArtifact('snapshot schema is malformed.');
192
151
  }
@@ -253,7 +212,6 @@ export function emptyRuntimeState(nextActIndex = 0) {
253
212
  nextActIndex,
254
213
  observations: {},
255
214
  leases: {},
256
- notifyLeases: {},
257
215
  };
258
216
  }
259
217
  function normalizedLines(value) {
@@ -284,6 +242,7 @@ export function createSquareState(options, snippet) {
284
242
  preamble: normalizedLines(snippet),
285
243
  warmup: normalizedLines(guides.join('\n\n')),
286
244
  acts: [],
245
+ routes: [],
287
246
  runtime: emptyRuntimeState(),
288
247
  };
289
248
  }
@@ -4,8 +4,10 @@ import { openSquare } from './square-file-adapter.js';
4
4
  import { closeOpenSquare } from './open-square.js';
5
5
  import { Square } from './square-wiring.js';
6
6
  import { entryPresentation } from './views.js';
7
- import { canonicalSquarePath, lookupSessionBindings, recordSessionDone, recordSessionJoin } from './registry.js';
8
7
  import { automaticParticipant } from './participant-identity.js';
8
+ import { createHostLedgerPort } from './host-ledger-file-adapter.js';
9
+ import { projectSessionBindings } from './square-projections.js';
10
+ import { publishWakeRoute, retireWakeRouteFromArtifact, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
9
11
  export { automaticParticipant } from './participant-identity.js';
10
12
  const providerEnv = {
11
13
  codex: 'CODEX_THREAD_ID',
@@ -13,6 +15,16 @@ const providerEnv = {
13
15
  opencode: 'OPENCODE_SESSION_ID',
14
16
  pi: 'SQUARE_PI_SESSION_ID',
15
17
  };
18
+ function operationEnv(provider, sessionId, env) {
19
+ return { ...env, [providerEnv[provider]]: sessionId };
20
+ }
21
+ function hostLedgerForEnv(env) {
22
+ const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
23
+ return createHostLedgerPort({
24
+ userPath: env.SQUARE_HOST_LEDGER_USER ?? root,
25
+ localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root,
26
+ });
27
+ }
16
28
  export function publicSquarePath(cwd) {
17
29
  return path.join(cwd, '.square', 'PUBLIC.square');
18
30
  }
@@ -38,16 +50,25 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
38
50
  return undefined;
39
51
  }
40
52
  const name = automaticParticipant(provider, sessionId, env);
41
- const canonicalPath = await canonicalSquarePath(squarePath);
42
- const alreadyBound = (await lookupSessionBindings(sessionId)).some((binding) => binding.squarePath === canonicalPath && binding.name === name);
43
53
  await closeOpenSquare(reader);
44
- const square = await Square.at({ path: squarePath });
54
+ const scopedEnv = operationEnv(provider, sessionId, env);
55
+ const square = await Square.at({ path: squarePath, hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv });
45
56
  try {
46
57
  const implicit = await square.implicitJoin(name);
47
- if (implicit.state === 'done' || (implicit.state === 'active' && alreadyBound))
58
+ if (implicit.state === 'done')
48
59
  return undefined;
49
- const channel = provider === 'claude' ? 'claude-code' : provider;
50
- await recordSessionJoin(sessionId, name, squarePath, channel, { ...env, [providerEnv[provider]]: sessionId });
60
+ const route = resolvePrimaryWakeRoute({ location: squarePath, participant: name, sessionId, provider }, env, await defaultWakeRouteCapabilities(hostLedgerForEnv(env)));
61
+ if (route !== undefined) {
62
+ const publisher = await openSquare(squarePath, { hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv });
63
+ try {
64
+ await publishWakeRoute(publisher.artifact, route, { at: Date.now() });
65
+ }
66
+ finally {
67
+ await closeOpenSquare(publisher);
68
+ }
69
+ }
70
+ await hostLedgerForEnv(env).ensurePresence({ location: squarePath, participant: name, session: sessionId, channel: provider === 'claude' ? 'claude-code' : provider, updatedAt: Date.now() }, 'user');
71
+ await square.reconcileBinding();
51
72
  return undefined;
52
73
  }
53
74
  finally {
@@ -56,22 +77,36 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
56
77
  }
57
78
  export async function automaticSessionEnd(provider, sessionId, cwd, env = process.env) {
58
79
  const squarePath = publicSquarePath(cwd);
59
- const channel = provider === 'claude' ? 'claude-code' : provider;
60
- const canonicalPath = await canonicalSquarePath(squarePath);
61
- const binding = (await lookupSessionBindings(sessionId)).find((item) => item.squarePath === canonicalPath && item.channel === channel);
80
+ const scopedEnv = operationEnv(provider, sessionId, env);
81
+ const hostLedger = hostLedgerForEnv(scopedEnv);
82
+ const probe = await openSquare(squarePath, { hostLedger, env: scopedEnv });
83
+ const binding = (await projectSessionBindings({
84
+ hostLedger,
85
+ sessionId,
86
+ location: probe.location,
87
+ scopes: ['user', 'local'],
88
+ }))[0];
89
+ await closeOpenSquare(probe);
62
90
  if (binding === undefined || !await squareExists(squarePath))
63
91
  return;
64
92
  const reader = await openSquare(squarePath);
65
- const joined = await entryPresentation(reader, binding.name).finally(() => closeOpenSquare(reader));
93
+ const joined = await entryPresentation(reader, binding.participant).finally(() => closeOpenSquare(reader));
66
94
  if (!joined.joined)
67
95
  return;
68
- const square = await Square.at({ path: squarePath });
96
+ const square = await Square.at({ path: squarePath, hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv });
69
97
  try {
70
- const participant = await square.join(binding.name);
98
+ const participant = await square.join(binding.participant);
71
99
  await participant.done();
100
+ await square.reconcileBinding();
101
+ const cleanup = await openSquare(squarePath);
102
+ try {
103
+ await retireWakeRouteFromArtifact(cleanup.artifact, { location: squarePath, participant: binding.participant, sessionId });
104
+ }
105
+ finally {
106
+ await closeOpenSquare(cleanup);
107
+ }
72
108
  }
73
109
  finally {
74
110
  await square.close();
75
111
  }
76
- await recordSessionDone(sessionId, binding.name, squarePath, channel, env);
77
112
  }
@@ -2,4 +2,4 @@ import type { InboxMembership } from './model.js';
2
2
  /** A fresh blocking catch owns only the notifications admitted by its filter. */
3
3
  export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
4
4
  export declare function renderPendingAtBoundary(inbox: InboxMembership[]): string;
5
- export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
5
+ export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
@@ -1,9 +1,11 @@
1
1
  import { leaseOwnsNotification } from './delivery.js';
2
- import { markBoundarySeen } from './square-wiring.js';
2
+ import { openSquare } from './square-file-adapter.js';
3
+ import { closeOpenSquare } from './open-square.js';
4
+ import { presentPending } from './presentation-operations.js';
3
5
  import { sessionInbox } from './inbox.js';
4
6
  import { ATTENTION_BODY_MAX, renderAttentionPreview } from './attention-presentation.js';
5
- import { presentOnce } from './presented.js';
6
7
  const CONTEXT_MAX = 1200;
8
+ const presentationLocks = new Map();
7
9
  function pendingCount(inbox) {
8
10
  return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
9
11
  }
@@ -60,9 +62,11 @@ function renderBoundary(inbox) {
60
62
  continue;
61
63
  }
62
64
  blocks.push(block);
63
- if (notification.body.replace(/\r\n/g, '\n').length <= ATTENTION_BODY_MAX) {
64
- complete.push({ membership, actIndexes: [notification.actIndex] });
65
- }
65
+ complete.push({
66
+ membership,
67
+ actIndexes: [notification.actIndex],
68
+ markSeen: notification.body.replace(/\r\n/g, '\n').length <= ATTENTION_BODY_MAX,
69
+ });
66
70
  }
67
71
  return {
68
72
  context: [
@@ -77,15 +81,55 @@ function renderBoundary(inbox) {
77
81
  };
78
82
  }
79
83
  export async function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env, signal) {
80
- const inbox = await lookup(sessionId);
81
- let delivered;
82
- const result = await presentOnce(sessionId, () => pendingAtBoundary(inbox), (inbox) => {
83
- delivered = renderBoundary(inbox);
84
- return present(delivered.context);
85
- }, env, Date.now(), signal);
86
- if (result !== undefined && delivered !== undefined) {
87
- for (const entry of delivered.complete) {
88
- await markBoundarySeen(entry.membership.squarePath, entry.membership.name, entry.membership.ownerId, entry.actIndexes);
84
+ const inbox = await lookup(sessionId, env);
85
+ if (signal?.aborted)
86
+ return undefined;
87
+ const delivered = renderBoundary(pendingAtBoundary(inbox));
88
+ if (delivered.context === '')
89
+ return undefined;
90
+ let result;
91
+ let rendered = false;
92
+ const renderOnce = async () => { if (!rendered) {
93
+ rendered = true;
94
+ result = await present(delivered.context);
95
+ } };
96
+ for (const entry of delivered.complete) {
97
+ let square;
98
+ try {
99
+ try {
100
+ square = await openSquare(entry.membership.squarePath, { env });
101
+ }
102
+ catch {
103
+ continue;
104
+ }
105
+ for (const index of entry.actIndexes) {
106
+ const key = `${entry.membership.squarePath}\u0000${entry.membership.name.toLocaleLowerCase()}\u0000${index}`;
107
+ const prior = presentationLocks.get(key);
108
+ if (prior !== undefined) {
109
+ await prior;
110
+ continue;
111
+ }
112
+ const work = presentPending({ artifact: square.artifact, location: entry.membership.squarePath, participant: entry.membership.name, activity: index, hostLedger: square.hostLedger, session: sessionId, sink: { present: renderOnce }, markSeen: entry.markSeen, now: Date.now() }).then(async (outcome) => {
113
+ // A stale projection can outlive its activity; still surface the bounded preview,
114
+ // but there is no artifact observation to commit.
115
+ if (!outcome.presented) {
116
+ const snapshot = await square.artifact.read().catch(() => undefined);
117
+ if (snapshot?.state.acts.every((activity) => activity.index !== index))
118
+ await renderOnce();
119
+ }
120
+ });
121
+ presentationLocks.set(key, work);
122
+ try {
123
+ await work;
124
+ }
125
+ finally {
126
+ presentationLocks.delete(key);
127
+ }
128
+ }
129
+ }
130
+ finally {
131
+ if (square !== undefined)
132
+ await closeOpenSquare(square);
89
133
  }
90
134
  }
91
135
  return result;
@@ -0,0 +1,17 @@
1
+ import { type Perception } from './square-core.js';
2
+ import { type SquareState, type StoredAct } from './model.js';
3
+ import type { CatchOptions } from './square-facade.js';
4
+ export interface CatchDecision {
5
+ readonly viewer: string;
6
+ readonly delivered: readonly StoredAct[];
7
+ readonly perceptions: ReadonlyMap<number, Perception>;
8
+ readonly consumedThrough: string | null;
9
+ readonly changed: boolean;
10
+ }
11
+ export interface CatchProjection {
12
+ cursorFor(name: string): number;
13
+ directedTo(activity: StoredAct, name: string): boolean;
14
+ perceive(activity: StoredAct, name: string): Perception;
15
+ }
16
+ /** Pure catch selection and acknowledgement over one SquareState snapshot. */
17
+ export declare function decideCatch(state: SquareState, requestedName: string, options: CatchOptions, at: number, project?: (state: SquareState) => CatchProjection): CatchDecision;
@@ -0,0 +1,53 @@
1
+ import { audienceOf, formatActivityId, replayLandedAudiences } from './square-core.js';
2
+ import { matchesMentionTarget, readCursor, recordObservation } from './runtime.js';
3
+ import { participantIdentity } from './participant-identity.js';
4
+ import { SquareError, sameName, validateName } from './model.js';
5
+ import { resolveRosterName, rosterNames } from './runtime.js';
6
+ function defaultProjection(state) {
7
+ const landed = replayLandedAudiences(state.acts);
8
+ return {
9
+ cursorFor: (name) => readCursor(state, name, landed),
10
+ directedTo: (activity, name) => activity.kind === 'say' && landed.includes(activity, name),
11
+ perceive: (activity, name) => activity.kind !== 'say' || sameName(activity.actor, name) || landed.includes(activity, name) ? 'full' : 'presence',
12
+ };
13
+ }
14
+ function resolveCatchName(state, requestedName) {
15
+ validateName(requestedName);
16
+ const known = resolveRosterName(state, requestedName);
17
+ if (known === undefined) {
18
+ throw new SquareError('invalid_args', `Unknown participant "${participantIdentity(requestedName)}". Expected one of: ${rosterNames(state).map(participantIdentity).join(', ')}.`);
19
+ }
20
+ return known;
21
+ }
22
+ /** Pure catch selection and acknowledgement over one SquareState snapshot. */
23
+ export function decideCatch(state, requestedName, options, at, project = defaultProjection) {
24
+ const viewer = resolveCatchName(state, requestedName);
25
+ const delivery = project(state);
26
+ const cursor = delivery.cursorFor(viewer);
27
+ const from = options.from;
28
+ const mentionOnly = options.mention === true;
29
+ const delivered = [];
30
+ const perceptions = new Map();
31
+ for (const activity of state.acts) {
32
+ if (activity.index <= cursor || activity.kind !== 'say' || !delivery.directedTo(activity, viewer))
33
+ continue;
34
+ if (from !== undefined && !from.some((participant) => sameName(participant, activity.actor)))
35
+ continue;
36
+ if (mentionOnly && audienceOf(activity).kind !== 'bell' && !matchesMentionTarget(activity, viewer))
37
+ continue;
38
+ delivered.push(activity);
39
+ perceptions.set(activity.index, delivery.perceive(activity, viewer));
40
+ }
41
+ let changed = false;
42
+ for (const activity of delivered) {
43
+ changed = recordObservation(state, viewer, activity.index, 'seen', at) || changed;
44
+ }
45
+ const consumed = delivery.cursorFor(viewer);
46
+ return {
47
+ viewer,
48
+ delivered,
49
+ perceptions,
50
+ consumedThrough: consumed < 0 ? null : formatActivityId(consumed),
51
+ changed,
52
+ };
53
+ }
@@ -1,9 +1,10 @@
1
1
  import type { InboxMembership } from './model.js';
2
+ import type { WakeAdapter } from './delivery.js';
2
3
  export interface NativeHookInput {
3
4
  session_id?: unknown;
4
5
  hook_event_name?: unknown;
5
6
  cwd?: unknown;
6
7
  }
7
8
  export declare function runClaudeHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
8
- export declare function claudeHookResponse(input: NativeHookInput, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
9
+ export declare function claudeHookResponse(input: NativeHookInput, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, deliveryAdapters?: WakeAdapter[]): Promise<object | undefined>;
9
10
  export declare function runClaudeHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
@@ -1,6 +1,7 @@
1
1
  import { presentPendingAtBoundary } from './boundary-presentation.js';
2
2
  import { sessionInbox } from './inbox.js';
3
3
  import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
4
+ import { sweepPrivilegedPending } from './notifications.js';
4
5
  export async function runClaudeHookAsync(inputText, env = process.env) {
5
6
  let input;
6
7
  try {
@@ -34,12 +35,14 @@ export async function runClaudeHookAsync(inputText, env = process.env) {
34
35
  }
35
36
  return runClaudeHook(inputText, env);
36
37
  }
37
- export async function claudeHookResponse(input, lookup = sessionInbox, env = process.env) {
38
+ export async function claudeHookResponse(input, lookup = sessionInbox, env = process.env, deliveryAdapters) {
38
39
  if (typeof input.session_id !== 'string' || input.session_id === '')
39
40
  return undefined;
40
41
  if (input.hook_event_name !== 'PostToolBatch')
41
42
  return undefined;
42
- return presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: context } }), lookup, env);
43
+ const response = await presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: context } }), lookup, env);
44
+ await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters).catch(() => undefined);
45
+ return response;
43
46
  }
44
47
  export async function runClaudeHook(inputText, env = process.env) {
45
48
  let input;
@@ -1,7 +1,8 @@
1
1
  import os from 'node:os';
2
2
  import { commandUsageHint } from '../help.js';
3
3
  import { parseParticipantList, validateName } from '../model.js';
4
- import { localParticipantName } from '../registry.js';
4
+ import { createHostLedgerPort } from '../host-ledger-file-adapter.js';
5
+ import { projectSessionBindings, sessionIdsFromEnvironment } from '../square-projections.js';
5
6
  export async function readStdin() {
6
7
  try {
7
8
  let content = '';
@@ -144,7 +145,14 @@ export async function parseGlobalArgs(rawArgs) {
144
145
  }
145
146
  const squarePath = requestedPath ?? configured;
146
147
  if (name === undefined && squarePath !== undefined && command !== undefined && locationIsRequired(command)) {
147
- name = await localParticipantName(squarePath);
148
+ const bindings = (await Promise.all(sessionIdsFromEnvironment().map((sessionId) => projectSessionBindings({
149
+ hostLedger: createHostLedgerPort(),
150
+ location: squarePath,
151
+ sessionId,
152
+ scopes: ['user', 'local'],
153
+ })))).flat();
154
+ const names = new Set(bindings.map((binding) => binding.participant));
155
+ name = names.size === 1 ? [...names][0] : undefined;
148
156
  }
149
157
  return { squarePath, explicitSquarePath: explicitSquarePath || configured !== undefined, multipleSquares: false, name, args };
150
158
  }
@@ -1,5 +1,9 @@
1
1
  import { type ActivitiesOptions, type WatchOptions } from '../model.js';
2
2
  import { type CommandSpec } from './context.js';
3
+ interface HistoryCommandOptions extends ActivitiesOptions {
4
+ noTruncate: boolean;
5
+ continuationArgs: string[];
6
+ }
3
7
  export declare const listCommand: CommandSpec<string[]>;
4
8
  interface StreamIntent {
5
9
  ndjson: boolean;
@@ -7,7 +11,7 @@ interface StreamIntent {
7
11
  }
8
12
  export declare const streamCommand: CommandSpec<StreamIntent>;
9
13
  export declare const catchCommand: CommandSpec<WatchOptions>;
10
- export declare const historyCommand: CommandSpec<ActivitiesOptions, string>;
14
+ export declare const historyCommand: CommandSpec<HistoryCommandOptions, string>;
11
15
  export declare const participantsCommand: CommandSpec<undefined, string>;
12
16
  export declare const statusCommand: CommandSpec<undefined, string>;
13
17
  interface InboxIntent {