@astrosheep/square 0.3.22 → 0.3.24

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "Native Claude Code turn-boundary delivery for Square participants",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -5,7 +5,6 @@ import { closeOpenSquare } from './open-square.js';
5
5
  import { Square } from './square-wiring.js';
6
6
  import { entryPresentation } from './views.js';
7
7
  import { canonicalSquarePath, lookupSessionBindings, recordSessionDone, recordSessionJoin } from './registry.js';
8
- import { participantIdentity, renderAmbientEvent } from './presentation.js';
9
8
  import { automaticParticipant } from './participant-identity.js';
10
9
  export { automaticParticipant } from './participant-identity.js';
11
10
  const providerEnv = {
@@ -39,15 +38,7 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
39
38
  return undefined;
40
39
  const channel = provider === 'claude' ? 'claude-code' : provider;
41
40
  recordSessionJoin(sessionId, name, squarePath, channel, { ...env, [providerEnv[provider]]: sessionId });
42
- const afterSquare = await openSquare(squarePath);
43
- const after = await entryPresentation(afterSquare, name, 10).finally(() => closeOpenSquare(afterSquare));
44
- const activity = after.recentActivities.map((event) => renderAmbientEvent(event, name, {
45
- now: Date.now(),
46
- preview: 200,
47
- actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
48
- squareState: after.state,
49
- })).filter(Boolean).join('\n\n');
50
- return [`You joined the public square as ${participantIdentity(name)}.`, after.scene, after.context ? `context\n${after.context}` : '', activity ? `recent activity\n${activity}` : ''].filter(Boolean).join('\n\n');
41
+ return undefined;
51
42
  }
52
43
  finally {
53
44
  await square.close();
@@ -4,7 +4,7 @@ import { sessionInbox } from '../inbox.js';
4
4
  import { sweepPendingNotifications } from '../notifications.js';
5
5
  import { cmdListSquares } from '../list.js';
6
6
  import { parseActivityId, sameName } from '../model.js';
7
- import { commandPrefix, participantCommandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEvent, withPathOutput, } from '../presentation.js';
7
+ import { commandPrefix, participantCommandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEvent, renderPresenceAnchor, withPathOutput, } from '../presentation.js';
8
8
  import { actId, nowMs } from '../runtime.js';
9
9
  import { cmdStream, cmdStreamNdjson } from '../stream.js';
10
10
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
@@ -293,8 +293,9 @@ function renderHistoryProjection(projection, visible, full, squarePath, viewer,
293
293
  : renderAmbientEvent(activity, viewer, options);
294
294
  if (rendered !== '')
295
295
  chunks.push(rendered);
296
- for (const name of projection.presenceAnchors[activity.index] ?? [])
297
- chunks.push(`→ ${participantIdentity(name)} was here`);
296
+ const participants = projection.presenceAnchors[activity.index];
297
+ if (participants !== undefined)
298
+ chunks.push(renderPresenceAnchor(participants));
298
299
  }
299
300
  if (chunks.length === 0)
300
301
  return 'latest\n ○ no public activity in this view';
@@ -14,10 +14,10 @@ import { retireWakeRoute } from './routes.js';
14
14
  import { openSquare } from './square-file-adapter.js';
15
15
  import { markNotificationNotified } from './square-wiring.js';
16
16
  import { closeOpenSquare } from './open-square.js';
17
- import { entryPresentation, notificationDelivered, notificationForAct, pendingDeliveries, resolveParticipant } from './views.js';
17
+ import { entryPresentation, notificationDelivered, notificationForAct, pendingDeliveriesFromState, resolveParticipant } from './views.js';
18
18
  import { claimNotificationLease, releaseNotificationLease, transitionNotificationLease } from './wakes.js';
19
19
  import { nextWakeAttemptNumber, recordRecoveredUnknown, recordWakeAttempt, } from './wake-attempts.js';
20
- import { wakeEvidence, wakeIsEligible } from './wake-evidence.js';
20
+ import { wakeEvidence, wakeEvidenceProjectionFromState, wakeIsEligible } from './wake-evidence.js';
21
21
  import { WakePort } from './wake-port.js';
22
22
  const NOTIFY_LEASE_MS = 5 * 60 * 1000;
23
23
  export { planActNotifications, matchesMentionTarget };
@@ -264,13 +264,21 @@ export async function sweepPendingNotifications(squarePath, opts = {}) {
264
264
  const now = opts.now ?? Date.now();
265
265
  const limit = opts.limit ?? 8;
266
266
  const square = await openSquare(squarePath, { clock: () => now });
267
- const pending = await pendingDeliveries(square).finally(() => closeOpenSquare(square));
267
+ let state;
268
+ try {
269
+ ({ state } = await entryPresentation(square, ''));
270
+ }
271
+ finally {
272
+ await closeOpenSquare(square);
273
+ }
274
+ const pending = pendingDeliveriesFromState(state);
275
+ const evidence = wakeEvidenceProjectionFromState(squarePath, state, now, env);
268
276
  const indexes = new Set();
269
277
  for (const delivery of pending) {
270
278
  for (const note of delivery.notifications) {
271
279
  if (now - note.item.at <= wakeGraceMs(env))
272
280
  continue;
273
- if (!wakeIsEligible(await wakeEvidence(squarePath, delivery.recipient, note.item.index, now, env)))
281
+ if (!wakeIsEligible(evidence.evidence(delivery.recipient, note.item.index)))
274
282
  continue;
275
283
  indexes.add(note.item.index);
276
284
  }
@@ -75,6 +75,7 @@ export declare function renderActivityBlocked(opts: ActivityBlockedOptions): str
75
75
  export declare function renderExpressWaiting(opts: ExpressWaitingOptions): string;
76
76
  export declare function renderExpressNoWait(opts: ExpressNoWaitOptions): string;
77
77
  export declare function renderPublicTail(squareState: SquareState, events: StoredAct[], lastN: number | null | undefined, now?: number, viewer?: string): string;
78
+ export declare function renderPresenceAnchor(names: readonly string[]): string;
78
79
  export declare function renderActivitiesView(squareState: SquareState, visible: StoredAct[], lastN: number | null | undefined, full: boolean | undefined, squarePath: string, viewer?: string, mode?: 'ambient' | 'archive'): string;
79
80
  export declare function renderGrepActivitiesView(visible: StoredAct[], totalMatches: number, full: boolean | undefined, squarePath: string, pattern: string, fixed?: boolean): string;
80
81
  export declare function renderActivityLimit(opts: ActivityLimitOptions): string;
@@ -291,8 +291,9 @@ function lastPresenceAnchor(squareState, name) {
291
291
  }
292
292
  return -1;
293
293
  }
294
- function renderLastPresenceMarker(name) {
295
- return `→ ${participantIdentity(name)} was here`;
294
+ export function renderPresenceAnchor(names) {
295
+ const participants = names.map((name) => participantIdentity(name)).join(', ');
296
+ return names.length === 1 ? `→ ${participants} was here` : `→ ${participants} were here`;
296
297
  }
297
298
  export function renderActivitiesView(squareState, visible, lastN, full, squarePath, viewer = '', mode = 'ambient') {
298
299
  const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
@@ -315,9 +316,9 @@ export function renderActivitiesView(squareState, visible, lastN, full, squarePa
315
316
  : renderAmbientEvent(act, viewer, { ...opts, squareState });
316
317
  if (rendered !== '')
317
318
  chunks.push(rendered);
318
- for (const participant of markers.get(act.index) ?? []) {
319
- chunks.push(renderLastPresenceMarker(participant));
320
- }
319
+ const participants = markers.get(act.index);
320
+ if (participants !== undefined)
321
+ chunks.push(renderPresenceAnchor(participants));
321
322
  }
322
323
  if (chunks.length === 0)
323
324
  return 'latest\n ○ no public activity in this view';
@@ -1,5 +1,13 @@
1
1
  import { type InboxMembership } from './model.js';
2
+ export interface PresentedAttention {
3
+ ownerId: string;
4
+ squarePath: string;
5
+ name: string;
6
+ actIndex: number;
7
+ }
2
8
  export declare function presentedPath(env?: NodeJS.ProcessEnv): string;
9
+ /** Read the current presentation facts once for a derived evidence projection. */
10
+ export declare function readPresentedAttentions(env?: NodeJS.ProcessEnv, now?: number): PresentedAttention[];
3
11
  export declare function hasPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): boolean;
4
12
  /** True when any current participant owner has already received this attention. */
5
13
  export declare function hasPresentedAttention(squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): boolean;
package/dist/presented.js CHANGED
@@ -48,6 +48,15 @@ function readRows(filePath, now = Date.now()) {
48
48
  }
49
49
  return rows;
50
50
  }
51
+ /** Read the current presentation facts once for a derived evidence projection. */
52
+ export function readPresentedAttentions(env = process.env, now = Date.now()) {
53
+ return readRows(presentedPath(env), now).map((row) => ({
54
+ ownerId: row.owner_id,
55
+ squarePath: canonicalSquarePath(row.square_path),
56
+ name: row.name,
57
+ actIndex: row.act_index,
58
+ }));
59
+ }
51
60
  function writeRows(filePath, rows) {
52
61
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
53
62
  const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
@@ -28,6 +28,7 @@ export declare function registryPath(): string;
28
28
  export declare function canonicalSquarePath(squarePath: string): string;
29
29
  export declare function recordJoin(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): void;
30
30
  export declare function recordDone(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): void;
31
+ export declare function readActiveBindings(now?: number): RegistryBinding[];
31
32
  export declare function lookupSessionBindings(sessionId: string, now?: number): RegistryBinding[];
32
33
  export declare function lookupSession(sessionId: string, now?: number): Array<{
33
34
  name: string;
package/dist/registry.js CHANGED
@@ -205,7 +205,7 @@ export function recordJoin(sessionId, name, squarePath, options = {}) {
205
205
  export function recordDone(sessionId, name, squarePath, options = {}) {
206
206
  writeLifecycle('done', sessionId, name, squarePath, options);
207
207
  }
208
- function readActiveBindings(now = Date.now()) {
208
+ export function readActiveBindings(now = Date.now()) {
209
209
  try {
210
210
  return foldRegistry(fs.readFileSync(registryPath(), 'utf8'), now);
211
211
  }
@@ -116,25 +116,36 @@ export function createFileCell(squarePath) {
116
116
  let closed = false;
117
117
  let version = 0;
118
118
  let fingerprint = fileFingerprint(squarePath);
119
+ let cached;
119
120
  function observe() {
120
121
  const next = fileFingerprint(squarePath);
121
- if (next === fingerprint)
122
- return;
123
- fingerprint = next;
124
- version += 1;
122
+ if (next !== fingerprint) {
123
+ fingerprint = next;
124
+ cached = undefined;
125
+ version += 1;
126
+ }
127
+ return fingerprint;
128
+ }
129
+ function currentState() {
130
+ const observed = observe();
131
+ if (cached?.fingerprint === observed)
132
+ return cloneState(cached.state);
133
+ const decoded = readSquareFile(squarePath);
134
+ cached = { fingerprint: observed, state: cloneState(decoded) };
135
+ return cloneState(cached.state);
125
136
  }
126
137
  return {
127
138
  async transact(fn) {
128
139
  assertCellOpen(closed);
129
140
  return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
130
141
  assertCellOpen(closed);
131
- observe();
132
- const current = readSquareFile(squarePath);
142
+ const current = currentState();
133
143
  const working = cloneState(current);
134
144
  const outcome = fn(working, version);
135
145
  if (outcome.state !== undefined) {
136
146
  writeSquareSnapshot(squarePath, outcome.state);
137
147
  fingerprint = fileFingerprint(squarePath);
148
+ cached = { fingerprint, state: cloneState(outcome.state) };
138
149
  version += 1;
139
150
  }
140
151
  return outcome.result;
@@ -142,9 +153,7 @@ export function createFileCell(squarePath) {
142
153
  },
143
154
  async read() {
144
155
  assertCellOpen(closed);
145
- observe();
146
- const state = readSquareFile(squarePath);
147
- return { state: cloneState(state), version };
156
+ return { state: currentState(), version };
148
157
  },
149
158
  async changed(sinceVersion, timeoutMs) {
150
159
  assertCellOpen(closed);
package/dist/views.d.ts CHANGED
@@ -96,6 +96,7 @@ export declare function watchPresentation(square: OpenSquare, name: string): Pro
96
96
  export declare function inboxProjection(square: OpenSquare, name: string, ownerId: string): Promise<InboxProjection>;
97
97
  export declare function streamProjection(square: OpenSquare, cursor: number, recipient?: string): Promise<StreamProjection>;
98
98
  export declare function notificationForAct(square: OpenSquare, actIndex: number): Promise<readonly PlannedNotification[]>;
99
+ export declare function pendingDeliveriesFromState(state: SquareState): readonly PendingDeliveryProjection[];
99
100
  export declare function pendingDeliveries(square: OpenSquare): Promise<readonly PendingDeliveryProjection[]>;
100
101
  export declare function notificationEvidence(square: OpenSquare, recipient: string, actIndex: number): Promise<{
101
102
  readonly delivered: boolean;
package/dist/views.js CHANGED
@@ -78,6 +78,11 @@ export async function inboxProjection(square, name, ownerId) { const { state } =
78
78
  export async function streamProjection(square, cursor, recipient) { const { state } = await square.cell.read(); return { activities: state.acts.filter((activity) => activity.index > cursor).flatMap((activity) => { if (recipient === undefined)
79
79
  return [{ activity }]; const notification = planActNotifications(state, activity).find((candidate) => nameKey(candidate.recipient) === nameKey(recipient)); return notification === undefined ? [] : [{ activity, route: notification.route }]; }), cursor: Math.max(cursor, ...state.acts.map((activity) => activity.index)) }; }
80
80
  export async function notificationForAct(square, actIndex) { const { state } = await square.cell.read(); const activity = state.acts.find((candidate) => candidate.index === actIndex); return activity === undefined ? [] : planActNotifications(state, activity); }
81
- export async function pendingDeliveries(square) { const { state } = await square.cell.read(); return [...new Set(state.acts.filter((activity) => activity.kind === 'join').map((activity) => activity.actor))].filter((name) => isCurrentlyJoined(state.acts, name)).map((recipient) => ({ recipient, notifications: deriveDeliveryModel(state).pendingFor(recipient) })); }
81
+ export function pendingDeliveriesFromState(state) {
82
+ return [...new Set(state.acts.filter((activity) => activity.kind === 'join').map((activity) => activity.actor))]
83
+ .filter((name) => isCurrentlyJoined(state.acts, name))
84
+ .map((recipient) => ({ recipient, notifications: deriveDeliveryModel(state).pendingFor(recipient) }));
85
+ }
86
+ export async function pendingDeliveries(square) { const { state } = await square.cell.read(); return pendingDeliveriesFromState(state); }
82
87
  export async function notificationEvidence(square, recipient, actIndex) { const { state } = await square.cell.read(); return { delivered: isActivitySeen(state, recipient, actIndex), observation: observationFor(state, recipient, actIndex) }; }
83
88
  export async function notificationDelivered(square, recipient, actIndex) { const { state } = await square.cell.read(); return isActivitySeen(state, recipient, actIndex); }
@@ -1,4 +1,5 @@
1
1
  import { type WakeRoute } from './model.js';
2
+ import { type SquareState } from './model.js';
2
3
  import { type WakeAttempt } from './wake-attempts.js';
3
4
  export interface WakeEvidence {
4
5
  delivered: boolean;
@@ -8,6 +9,12 @@ export interface WakeEvidence {
8
9
  terminal?: WakeAttempt;
9
10
  attemptableRoutes: WakeRoute[];
10
11
  }
12
+ export interface WakeEvidenceProjection {
13
+ evidence(recipient: string, actIndex: number): WakeEvidence;
14
+ }
15
+ /** Capture the primary wake facts once and derive any number of eligibility decisions from them. */
16
+ export declare function wakeEvidenceProjection(squarePath: string, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidenceProjection>;
17
+ export declare function wakeEvidenceProjectionFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv): WakeEvidenceProjection;
11
18
  /** Project every wake decision from the same primary evidence. */
12
19
  export declare function wakeEvidence(squarePath: string, recipient: string, actIndex: number, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidence>;
13
20
  export declare function wakeIsEligible(evidence: WakeEvidence): boolean;
@@ -1,38 +1,92 @@
1
- import { hasPresentedAttention } from './presented.js';
2
- import { lookupParticipant } from './registry.js';
1
+ import { isActivitySeen } from './delivery.js';
2
+ import { nameKey } from './model.js';
3
+ import { readPresentedAttentions } from './presented.js';
4
+ import { canonicalSquarePath, readActiveBindings } from './registry.js';
3
5
  import { readWakeRoutes } from './routes.js';
4
6
  import { isWakeRouteAttemptable, readWakeAttempts, terminalWakeEvidence, } from './wake-attempts.js';
5
7
  import { openSquare } from './square-file-adapter.js';
6
8
  import { closeOpenSquare } from './open-square.js';
7
- import { notificationEvidence } from './views.js';
8
- /** Project every wake decision from the same primary evidence. */
9
- export async function wakeEvidence(squarePath, recipient, actIndex, now, env) {
9
+ import { observationFor } from './runtime.js';
10
+ import { entryPresentation } from './views.js';
11
+ function attentionKey(squarePath, recipient, actIndex) {
12
+ return JSON.stringify([canonicalSquarePath(squarePath), nameKey(recipient), actIndex]);
13
+ }
14
+ function projectionFromState(squarePath, state, now, env) {
15
+ const canonicalPath = canonicalSquarePath(squarePath);
16
+ const owners = new Map();
17
+ for (const binding of readActiveBindings(now)) {
18
+ if (binding.squarePath !== canonicalPath)
19
+ continue;
20
+ const key = nameKey(binding.name);
21
+ const recipientOwners = owners.get(key) ?? new Set();
22
+ recipientOwners.add(binding.ownerId);
23
+ owners.set(key, recipientOwners);
24
+ }
25
+ const routesByOwner = new Map();
26
+ for (const route of readWakeRoutes({ freshOnly: true, now, env })) {
27
+ const routes = routesByOwner.get(route.ownerId) ?? [];
28
+ routes.push(route);
29
+ routesByOwner.set(route.ownerId, routes);
30
+ }
31
+ const attemptsByAttention = new Map();
32
+ for (const attempt of readWakeAttempts({ env, now })) {
33
+ const key = attentionKey(attempt.attention.squarePath, attempt.attention.recipient, attempt.attention.actIndex);
34
+ const attempts = attemptsByAttention.get(key) ?? [];
35
+ attempts.push(attempt);
36
+ attemptsByAttention.set(key, attempts);
37
+ }
38
+ const presentedByAttention = new Map();
39
+ for (const presented of readPresentedAttentions(env, now)) {
40
+ const key = attentionKey(presented.squarePath, presented.name, presented.actIndex);
41
+ const presentedOwners = presentedByAttention.get(key) ?? new Set();
42
+ presentedOwners.add(presented.ownerId);
43
+ presentedByAttention.set(key, presentedOwners);
44
+ }
45
+ return {
46
+ evidence(recipient, actIndex) {
47
+ const recipientOwners = owners.get(nameKey(recipient)) ?? new Set();
48
+ const key = attentionKey(squarePath, recipient, actIndex);
49
+ const attempts = attemptsByAttention.get(key) ?? [];
50
+ const terminal = terminalWakeEvidence(attempts);
51
+ const routes = [...recipientOwners].flatMap((ownerId) => routesByOwner.get(ownerId) ?? []);
52
+ const presented = [...(presentedByAttention.get(key) ?? [])]
53
+ .some((ownerId) => recipientOwners.has(ownerId));
54
+ return {
55
+ delivered: isActivitySeen(state, recipient, actIndex),
56
+ notified: (() => {
57
+ const observation = observationFor(state, recipient, actIndex);
58
+ return observation?.state === 'notified'
59
+ && observation.ownerId !== undefined
60
+ && recipientOwners.has(observation.ownerId);
61
+ })(),
62
+ presented,
63
+ attempts,
64
+ ...(terminal === undefined ? {} : { terminal }),
65
+ attemptableRoutes: terminal === undefined
66
+ ? routes.filter((route) => isWakeRouteAttemptable(route, attempts))
67
+ : [],
68
+ };
69
+ },
70
+ };
71
+ }
72
+ /** Capture the primary wake facts once and derive any number of eligibility decisions from them. */
73
+ export async function wakeEvidenceProjection(squarePath, now, env) {
10
74
  const square = await openSquare(squarePath, { clock: () => now });
11
75
  try {
12
- const { delivered, observation } = await notificationEvidence(square, recipient, actIndex);
13
- const owners = new Set(lookupParticipant(squarePath, recipient, now).map((binding) => binding.ownerId));
14
- const notified = observation?.state === 'notified'
15
- && observation.ownerId !== undefined
16
- && owners.has(observation.ownerId);
17
- const attempts = readWakeAttempts({ attention: { squarePath, recipient, actIndex }, env, now });
18
- const terminal = terminalWakeEvidence(attempts);
19
- const routes = readWakeRoutes({ freshOnly: true, now, env })
20
- .filter((route) => owners.has(route.ownerId));
21
- return {
22
- delivered,
23
- notified,
24
- presented: hasPresentedAttention(squarePath, recipient, actIndex, env, now),
25
- attempts,
26
- ...(terminal === undefined ? {} : { terminal }),
27
- attemptableRoutes: terminal === undefined
28
- ? routes.filter((route) => isWakeRouteAttemptable(route, attempts))
29
- : [],
30
- };
76
+ const { state } = await entryPresentation(square, '');
77
+ return projectionFromState(squarePath, state, now, env);
31
78
  }
32
79
  finally {
33
80
  await closeOpenSquare(square);
34
81
  }
35
82
  }
83
+ export function wakeEvidenceProjectionFromState(squarePath, state, now, env) {
84
+ return projectionFromState(squarePath, state, now, env);
85
+ }
86
+ /** Project every wake decision from the same primary evidence. */
87
+ export async function wakeEvidence(squarePath, recipient, actIndex, now, env) {
88
+ return (await wakeEvidenceProjection(squarePath, now, env)).evidence(recipient, actIndex);
89
+ }
36
90
  export function wakeIsEligible(evidence) {
37
91
  return !evidence.delivered
38
92
  && !evidence.notified
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {