@astrosheep/square 0.3.49 → 0.3.51

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.49",
3
+ "version": "0.3.51",
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.49",
3
+ "version": "0.3.51",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -7,8 +7,8 @@ export interface AttentionPreview {
7
7
  actor: string;
8
8
  route: DirectedNotificationRoute;
9
9
  body: string;
10
- compact?: boolean;
11
10
  }
12
11
  export declare function previewAttentionBody(body: string): string;
12
+ export declare function attentionBodyIsClipped(body: string): boolean;
13
13
  export declare function displayAttentionPath(squarePath: string): string;
14
14
  export declare function renderAttentionPreview(attention: AttentionPreview): string;
@@ -1,7 +1,6 @@
1
1
  import { homedir } from 'node:os';
2
- import { notificationMessageId } from './delivery.js';
3
- import { participantIdentity } from './presentation.js';
4
2
  import { formatActivityId } from './square-core.js';
3
+ import { participantCommandPrefix } from './presentation.js';
5
4
  export const ATTENTION_BODY_MAX = 200;
6
5
  export function previewAttentionBody(body) {
7
6
  const compact = body.replace(/\r\n/g, '\n');
@@ -9,6 +8,9 @@ export function previewAttentionBody(body) {
9
8
  return compact;
10
9
  return `${compact.slice(0, ATTENTION_BODY_MAX).trimEnd()}…`;
11
10
  }
11
+ export function attentionBodyIsClipped(body) {
12
+ return body.replace(/\r\n/g, '\n').length > ATTENTION_BODY_MAX;
13
+ }
12
14
  export function displayAttentionPath(squarePath) {
13
15
  return squarePath.startsWith(homedir())
14
16
  ? `~${squarePath.slice(homedir().length)}`
@@ -16,12 +18,25 @@ export function displayAttentionPath(squarePath) {
16
18
  }
17
19
  export function renderAttentionPreview(attention) {
18
20
  const attentionKind = attention.route === 'bell' ? 'bell' : 'attention';
19
- const id = attention.compact
20
- ? formatActivityId(attention.actIndex)
21
- : `${notificationMessageId(attention.squarePath, attention.actIndex)} · ${displayAttentionPath(attention.squarePath)}`;
22
- const normalizedBody = attention.body.replace(/\r\n/g, '\n');
21
+ const attributes = {
22
+ location: displayAttentionPath(attention.squarePath),
23
+ id: formatActivityId(attention.actIndex),
24
+ from: attention.actor,
25
+ to: attention.recipient,
26
+ kind: attentionKind,
27
+ };
23
28
  return [
24
- `${id}: ${participantIdentity(attention.recipient)} from ${participantIdentity(attention.actor)} (${attentionKind})`,
25
- previewAttentionBody(normalizedBody),
29
+ '<square-activity',
30
+ ...Object.entries(attributes).map(([key, value]) => ` ${key}="${escapeAttribute(value)}"`),
31
+ '>',
32
+ previewAttentionBody(attention.body),
33
+ '</square-activity>',
34
+ ...(attentionBodyIsClipped(attention.body)
35
+ ? [`» ${participantCommandPrefix(attention.squarePath, attention.recipient)} catch --id ${formatActivityId(attention.actIndex)}`]
36
+ : []),
26
37
  ].join('\n');
27
38
  }
39
+ function escapeAttribute(value) {
40
+ return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
41
+ .replace(/\r/g, '&#13;').replace(/\n/g, '&#10;').replace(/\t/g, '&#9;');
42
+ }
@@ -15,7 +15,7 @@ const providerEnv = {
15
15
  codex: 'CODEX_THREAD_ID',
16
16
  claude: 'CLAUDE_CODE_SESSION_ID',
17
17
  opencode: 'OPENCODE_SESSION_ID',
18
- pi: 'SQUARE_PI_SESSION_ID',
18
+ pi: 'PI_SESSION_ID',
19
19
  };
20
20
  function operationEnv(provider, sessionId, env) {
21
21
  return {
@@ -24,7 +24,7 @@ function operationEnv(provider, sessionId, env) {
24
24
  CLAUDE_CODE_CHILD_SESSION: '',
25
25
  CODEX_THREAD_ID: '',
26
26
  OPENCODE_SESSION_ID: '',
27
- SQUARE_PI_SESSION_ID: '',
27
+ PI_SESSION_ID: '',
28
28
  [providerEnv[provider]]: sessionId,
29
29
  };
30
30
  }
@@ -6,8 +6,10 @@ import { withFileLock } from './file-lock.js';
6
6
  import { openSquare } from './square-file-adapter.js';
7
7
  import { closeOpenSquare } from './open-square.js';
8
8
  import { presentPending } from './presentation-operations.js';
9
- import { sessionInbox } from './inbox.js';
10
- import { ATTENTION_BODY_MAX, renderAttentionPreview } from './attention-presentation.js';
9
+ import { hostLedgerForEnv, sessionInbox } from './inbox.js';
10
+ import { attentionBodyIsClipped, renderAttentionPreview } from './attention-presentation.js';
11
+ import { presentationSuppressesWake, projectPresentationEvidence } from './square-projections.js';
12
+ import { formatActivityId } from './square-core.js';
11
13
  const CONTEXT_MAX = 1200;
12
14
  const presentationLocks = new Map();
13
15
  function pendingCount(inbox) {
@@ -72,7 +74,7 @@ function renderBoundary(inbox) {
72
74
  complete.push({
73
75
  membership,
74
76
  actIndexes: [notification.actIndex],
75
- markSeen: notification.body.replace(/\r\n/g, '\n').length <= ATTENTION_BODY_MAX,
77
+ markSeen: !attentionBodyIsClipped(notification.body),
76
78
  });
77
79
  }
78
80
  return {
@@ -91,7 +93,17 @@ async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = ses
91
93
  const inbox = await lookup(sessionId, env);
92
94
  if (signal?.aborted)
93
95
  return undefined;
94
- const delivered = renderBoundary(pendingAtBoundary(inbox));
96
+ const hostLedger = hostLedgerForEnv(env);
97
+ const pending = [];
98
+ for (const membership of pendingAtBoundary(inbox)) {
99
+ const evidence = await projectPresentationEvidence({ hostLedger, location: membership.squarePath, participant: membership.name, sessionId });
100
+ const notifications = membership.notifications.filter((notification) => !presentationSuppressesWake(evidence.filter((row) => row.activity === formatActivityId(notification.actIndex))));
101
+ if (notifications.length > 0)
102
+ pending.push({ ...membership, notifications });
103
+ }
104
+ if (pending.length === 0 || signal?.aborted)
105
+ return undefined;
106
+ const delivered = renderBoundary(pending);
95
107
  if (delivered.context === '')
96
108
  return undefined;
97
109
  let result;
@@ -467,13 +467,7 @@ export const statusCommand = {
467
467
  try {
468
468
  const presentation = await statusPresentation(square);
469
469
  const result = presentation.status;
470
- const active = result.participants.filter((participant) => participant.state === 'active').sort((a, b) => {
471
- const aViewer = context.name !== undefined && sameName(a.name, context.name);
472
- const bViewer = context.name !== undefined && sameName(b.name, context.name);
473
- if (aViewer !== bViewer)
474
- return aViewer ? -1 : 1;
475
- return (b.lastActiveAt ?? -Infinity) - (a.lastActiveAt ?? -Infinity) || a.name.localeCompare(b.name);
476
- });
470
+ const active = result.participants.filter((participant) => participant.state === 'active');
477
471
  const people = active.length === 0 ? [' ○ nobody in the square'] : active.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map((participant) => {
478
472
  const glyph = participant.presence === 'watching'
479
473
  ? '◎'
@@ -90,11 +90,11 @@ export interface ParticipantStatus {
90
90
  activityCount: number;
91
91
  lastActiveAt: number | undefined;
92
92
  presence: CorePresenceState;
93
- presenceAt: number | undefined;
94
93
  unreadActivityCount: number;
95
94
  pendingMentionCount: number;
96
95
  }
97
96
  export type CorePresenceState = 'never-joined' | 'active' | 'watching' | 'done';
97
+ export declare function compareParticipantActivity(a: Pick<ParticipantStatus, 'name' | 'lastActiveAt'>, b: Pick<ParticipantStatus, 'name' | 'lastActiveAt'>): number;
98
98
  export interface StatusResult {
99
99
  hardCap: HardCap;
100
100
  throttlePerMinute: number | undefined;
package/dist/decisions.js CHANGED
@@ -212,23 +212,23 @@ export function coreResume(squareState, actor, now) {
212
212
  requireStanding(squareState, act);
213
213
  return act;
214
214
  }
215
- function presenceFor(squareState, snapshot, name, now, delivery) {
216
- if (snapshot?.done)
217
- return { state: 'done', lastAt: snapshot.lastActiveAt };
218
- const cursorAt = squareState.acts.findLast((act) => act.index <= delivery.cursorFor(name))?.at;
219
- const lease = freshWatchLease(squareState, name, now);
220
- if (lease !== undefined)
221
- return { state: 'watching', lastAt: cursorAt ?? lease.heartbeatAt };
222
- const lastAt = cursorAt ?? (snapshot?.joined ? snapshot.lastActiveAt : undefined);
223
- return lastAt === undefined
224
- ? { state: 'never-joined', lastAt: undefined }
225
- : { state: 'active', lastAt };
215
+ export function compareParticipantActivity(a, b) {
216
+ return (b.lastActiveAt ?? -Infinity) - (a.lastActiveAt ?? -Infinity) || a.name.localeCompare(b.name);
226
217
  }
227
218
  function buildParticipantStatuses(squareState, now, state = foldedState(squareState), suppliedDelivery) {
228
219
  const delivery = suppliedDelivery ?? deriveDeliveryModel(squareState);
229
220
  return state.participants.map((snapshot) => {
230
221
  const participant = snapshot.name;
231
- const presence = presenceFor(squareState, snapshot, participant, now, delivery);
222
+ let lastActiveAt = snapshot.lastActiveAt;
223
+ // Receipt time is evidence of catching; the consumed activity's time is not.
224
+ if (snapshot.joined) {
225
+ for (const observation of Object.values(squareState.runtime.observations[participant] ?? {})) {
226
+ if (observation.state === 'seen')
227
+ lastActiveAt = Math.max(lastActiveAt ?? -Infinity, observation.at);
228
+ }
229
+ }
230
+ const presence = snapshot.done ? 'done' : !snapshot.joined ? 'never-joined'
231
+ : freshWatchLease(squareState, participant, now) !== undefined ? 'watching' : 'active';
232
232
  const participantStatus = snapshot?.done ? 'done' : snapshot?.joined ? 'active' : 'not joined';
233
233
  const consumedThrough = delivery.cursorFor(participant);
234
234
  let unreadActivityCount = 0;
@@ -245,13 +245,12 @@ function buildParticipantStatuses(squareState, now, state = foldedState(squareSt
245
245
  state: participantStatus,
246
246
  listening: listeningTo(state, participant),
247
247
  activityCount: snapshot?.activityCount ?? 0,
248
- lastActiveAt: snapshot?.lastActiveAt,
249
- presence: presence.state,
250
- presenceAt: presence.lastAt,
248
+ lastActiveAt,
249
+ presence,
251
250
  unreadActivityCount,
252
251
  pendingMentionCount: snapshot?.joined ? delivery.pendingFor(participant).length : 0,
253
252
  };
254
- });
253
+ }).sort(compareParticipantActivity);
255
254
  }
256
255
  export function coreStatus(squareState, now, delivery) {
257
256
  const state = foldedState(squareState);
package/dist/inbox.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type InboxMembership } from './model.js';
2
+ export declare function hostLedgerForEnv(env: NodeJS.ProcessEnv): import("./host-ledger.js").HostLedgerPort;
2
3
  export interface PendingWaitOptions {
3
4
  signal?: AbortSignal;
4
5
  /** Ephemeral watcher de-duplication; never persisted or used by delivery derivation. */
package/dist/inbox.js CHANGED
@@ -4,7 +4,7 @@ import { waitForSquareChanges } from './square-file-adapter.js';
4
4
  import path from 'node:path';
5
5
  import { createHostLedgerPort } from './host-ledger-file-adapter.js';
6
6
  import { projectPresentation, projectSessionBindings } from './square-projections.js';
7
- function hostLedgerForEnv(env) {
7
+ export function hostLedgerForEnv(env) {
8
8
  const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
9
9
  return createHostLedgerPort({
10
10
  userPath: env.SQUARE_HOST_LEDGER_USER ?? root,
@@ -3,7 +3,7 @@ import { defaultWakeRouteCapabilities, publishWakeRoute, retireWakeRouteFromArti
3
3
  export function processIdentity(env) {
4
4
  const choices = [
5
5
  [env.CLAUDE_CODE_SESSION_ID, 'claude-code'], [env.CODEX_THREAD_ID, 'codex'],
6
- [env.OPENCODE_SESSION_ID, 'opencode'], [env.SQUARE_PI_SESSION_ID, 'pi'], [env.PASEO_AGENT_ID, 'paseo'],
6
+ [env.OPENCODE_SESSION_ID, 'opencode'], [env.PI_SESSION_ID, 'pi'], [env.PASEO_AGENT_ID, 'paseo'],
7
7
  ];
8
8
  const found = choices.find(([session]) => session?.trim());
9
9
  return found === undefined ? { session: `process:${process.pid}`, channel: 'unknown' } : { session: found[0].trim(), channel: found[1] };
@@ -7,7 +7,7 @@ const nativeSessions = [
7
7
  { variable: 'CLAUDE_CODE_SESSION_ID', provider: 'claude' },
8
8
  { variable: 'CODEX_THREAD_ID', provider: 'codex' },
9
9
  { variable: 'OPENCODE_SESSION_ID', provider: 'opencode' },
10
- { variable: 'SQUARE_PI_SESSION_ID', provider: 'pi' },
10
+ { variable: 'PI_SESSION_ID', provider: 'pi' },
11
11
  ];
12
12
  export function automaticParticipant(provider, sessionId, env) {
13
13
  const configured = env.SQUARE_PARTICIPANT_NAME?.trim();
@@ -7,6 +7,7 @@ import { audienceIncludes, audienceOf, MAX_IDENTITY_SET_SIZE } from './square-co
7
7
  import { perceiveActivity } from './delivery.js';
8
8
  import { actId, publicActs, readCursor, rosterNames, sayNumberFor } from './runtime.js';
9
9
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
10
+ import { compareParticipantActivity } from './decisions.js';
10
11
  import { grepSnippet } from './search.js';
11
12
  function headerLine(squarePath, opts = {}) {
12
13
  const count = opts.participantCount ?? 0;
@@ -56,7 +57,7 @@ function presenceGlyph(participant) {
56
57
  return '○';
57
58
  if (participant.presence === 'watching')
58
59
  return '◎';
59
- if (participant.presenceAt !== undefined)
60
+ if (participant.lastActiveAt !== undefined)
60
61
  return '●';
61
62
  return '○';
62
63
  }
@@ -65,19 +66,17 @@ function presenceText(participant, now) {
65
66
  return participant.lastActiveAt === undefined ? 'stepped out of the square' : `stepped out of the square · ${formatRelativeTime(participant.lastActiveAt, now)}`;
66
67
  }
67
68
  if (participant.presence === 'watching') {
68
- const at = participant.presenceAt ?? participant.lastActiveAt;
69
+ const at = participant.lastActiveAt;
69
70
  return at === undefined ? 'catching' : `catching · ${formatRelativeTime(at, now)}`;
70
71
  }
71
- if (participant.presenceAt === undefined)
72
+ if (participant.lastActiveAt === undefined)
72
73
  return 'quiet';
73
- return participant.activityCount > 0 ? `${formatRelativeTime(participant.presenceAt, now)}` : `quiet · ${formatRelativeTime(participant.presenceAt, now)}`;
74
+ return participant.activityCount > 0 ? `${formatRelativeTime(participant.lastActiveAt, now)}` : `quiet · ${formatRelativeTime(participant.lastActiveAt, now)}`;
74
75
  }
75
76
  export function renderPresenceLines(participants, now, limit = 5) {
76
77
  const recent = participants
77
- .map((p) => ({ p, at: p.presenceAt ?? p.lastActiveAt ?? -Infinity }))
78
- .filter(({ p, at }) => p.state === 'done' || p.presence === 'watching' || (at !== -Infinity && now - at <= PRESENCE_WINDOW_MS))
79
- .sort((a, b) => b.at - a.at || a.p.name.localeCompare(b.p.name))
80
- .map(({ p }) => p);
78
+ .filter((p) => p.state === 'done' || p.presence === 'watching' || (p.lastActiveAt !== undefined && now - p.lastActiveAt <= PRESENCE_WINDOW_MS))
79
+ .sort(compareParticipantActivity);
81
80
  const shown = recent.slice(0, limit);
82
81
  if (shown.length === 0)
83
82
  return [' ○ nobody nearby'];
@@ -90,8 +89,7 @@ export function renderPresenceLines(participants, now, limit = 5) {
90
89
  const EXPRESS_HINTS = [
91
90
  '*asterisks* are your body — *slams table*, *sketches in the air*, *shrugs*',
92
91
  "you're standing in a square — words and gestures both land",
93
- 'half-shaped is welcome — a sketch, an objection, a joke, a fragment',
94
- '@ only who you need — step back with catch --mention',
92
+ 'the square is for conversation — share thoughts and engage with others, beyond status updates and empty acknowledgments',
95
93
  ];
96
94
  export function expressHintLine(ownActivityCount) {
97
95
  if (ownActivityCount !== 1 && ownActivityCount % 5 !== 0)
package/dist/registry.js CHANGED
@@ -12,7 +12,7 @@ const LOCAL_SESSION_SOURCES = [
12
12
  { variable: 'CLAUDE_CODE_SESSION_ID', channel: 'claude-code', child: 'CLAUDE_CODE_CHILD_SESSION' },
13
13
  { variable: 'CODEX_THREAD_ID', channel: 'codex' },
14
14
  { variable: 'OPENCODE_SESSION_ID', channel: 'opencode' },
15
- { variable: 'SQUARE_PI_SESSION_ID', channel: 'pi' },
15
+ { variable: 'PI_SESSION_ID', channel: 'pi' },
16
16
  ];
17
17
  export function registryPath(env = process.env) { return env.SQUARE_REGISTRY || path.join(homedir(), '.square', 'sessions.ndjsonl'); }
18
18
  export async function canonicalSquarePath(squarePath) { const absolute = path.resolve(squarePath); try {
@@ -150,19 +150,20 @@ function applyActivity(accumulator, act) {
150
150
  const { state, byKey, ordered } = accumulator;
151
151
  const actor = actorOf(act);
152
152
  const snapshot = actor === undefined ? undefined : touchParticipant(byKey, ordered, actor);
153
+ if (snapshot !== undefined && act.at !== undefined) {
154
+ snapshot.lastActiveAt = Math.max(snapshot.lastActiveAt ?? -Infinity, act.at);
155
+ }
153
156
  switch (act.kind) {
154
157
  case 'join':
155
158
  if (snapshot !== undefined) {
156
159
  snapshot.joined = true;
157
160
  snapshot.done = false;
158
- snapshot.lastActiveAt = act.at ?? snapshot.lastActiveAt;
159
161
  }
160
162
  break;
161
163
  case 'done':
162
164
  if (snapshot !== undefined) {
163
165
  snapshot.joined = false;
164
166
  snapshot.done = true;
165
- snapshot.lastActiveAt = act.at ?? snapshot.lastActiveAt;
166
167
  state.listening.delete(nameKey(snapshot.name));
167
168
  }
168
169
  break;
@@ -195,7 +196,6 @@ function applyActivity(accumulator, act) {
195
196
  case 'say':
196
197
  if (snapshot !== undefined) {
197
198
  snapshot.activityCount += 1;
198
- snapshot.lastActiveAt = act.at ?? snapshot.lastActiveAt;
199
199
  }
200
200
  pushThrottleAt(state, act.at);
201
201
  if (act.reach === 'bell')
@@ -28,7 +28,7 @@ export async function projectSessionBindings(input) {
28
28
  return rows.map(bindingProjection);
29
29
  }
30
30
  export function sessionIdsFromEnvironment(env = process.env) {
31
- return [env.CLAUDE_CODE_SESSION_ID, env.CODEX_THREAD_ID, env.OPENCODE_SESSION_ID, env.SQUARE_PI_SESSION_ID, env.PASEO_AGENT_ID]
31
+ return [env.CLAUDE_CODE_SESSION_ID, env.CODEX_THREAD_ID, env.OPENCODE_SESSION_ID, env.PI_SESSION_ID, env.PASEO_AGENT_ID]
32
32
  .map((value) => value?.trim()).filter((value) => Boolean(value));
33
33
  }
34
34
  export async function projectLocalParticipantBinding(input) {
@@ -85,7 +85,7 @@ export class Square {
85
85
  async recognize(env) {
86
86
  if (this.square.hostLedger === undefined)
87
87
  return null;
88
- const sessions = [env.CLAUDE_CODE_SESSION_ID, env.CODEX_THREAD_ID, env.OPENCODE_SESSION_ID, env.SQUARE_PI_SESSION_ID, env.PASEO_AGENT_ID]
88
+ const sessions = [env.CLAUDE_CODE_SESSION_ID, env.CODEX_THREAD_ID, env.OPENCODE_SESSION_ID, env.PI_SESSION_ID, env.PASEO_AGENT_ID]
89
89
  .map((value) => value?.trim()).filter((value) => Boolean(value));
90
90
  if (sessions.length === 0)
91
91
  return null;
package/dist/views.js CHANGED
@@ -68,7 +68,7 @@ export async function activityPresentation(square, name) { const { state } = awa
68
68
  export async function entryPresentation(square, name, lastN = 10) { const { state } = await square.artifact.read(); const known = resolveRosterName(state, name) ?? name; const publicActivities = state.acts.filter((activity) => activity.kind === 'say' || activity.kind === 'done'); return { joined: isCurrentlyJoined(state.acts, known), scene: state.warmup.join('\n').trim(), context: state.preamble.join('\n').trim(), joinContext: (state.preamble.at(-1) === '---' ? state.preamble.slice(0, -1) : state.preamble).join('\n').trim(), recentActivities: lastN === null ? publicActivities : publicActivities.slice(-lastN), state, sayNumbers: sayNumbers(state), participantCount: inSquareCount(state) }; }
69
69
  export async function historyPresentation(square, options) { const { state } = await square.artifact.read(); const delivery = deriveDeliveryModel(state); return { activities: coreActivities(state, options, delivery), sayNumbers: sayNumbers(state), presenceAnchors: anchors(state, delivery), participantCount: inSquareCount(state) }; }
70
70
  export async function participantsPresentation(square) { const { state } = await square.artifact.read(); const delivery = deriveDeliveryModel(state); return coreParticipants(state, square.clock(), delivery); }
71
- export async function listPresentation(square) { const { state } = await square.artifact.read(); return { context: state.preamble, participants: foldedState(state).participants.filter((participant) => participant.joined).sort((left, right) => (right.lastActiveAt ?? -Infinity) - (left.lastActiveAt ?? -Infinity) || left.name.localeCompare(right.name)).map((participant) => participant.name), activities: state.acts.filter((activity) => activity.kind === 'say').length }; }
71
+ export async function listPresentation(square) { const { state } = await square.artifact.read(); return { context: state.preamble, participants: coreParticipants(state, square.clock()).filter((participant) => participant.state === 'active').map((participant) => participant.name), activities: state.acts.filter((activity) => activity.kind === 'say').length }; }
72
72
  export async function statusPresentation(square) { const { state } = await square.artifact.read(); const delivery = deriveDeliveryModel(state); const status = coreStatus(state, square.clock(), delivery); return { state, status, ...(status.latestAct?.kind === 'say' ? { latestActNumber: countSays(state.acts, status.latestAct.actor) } : {}) }; }
73
73
  export async function eventPresentation(square, id) { const { state } = await square.artifact.read(); const activity = state.acts.find((candidate) => candidate.index === parseRequiredActivityId(id)); if (activity === undefined)
74
74
  throw new SquareError('invalid_args', `Unknown activity id: ${id}`); return { activity, participantCount: inSquareCount(state), held: currentHold(state.acts).active }; }
@@ -32,7 +32,6 @@ export function renderPiInbox(inbox) {
32
32
  export default function squarePiExtension(pi) {
33
33
  let sessionId;
34
34
  let sessionCwd;
35
- let previousSessionId;
36
35
  let watcher;
37
36
  let watcherAbort;
38
37
  let generation = 0;
@@ -99,6 +98,10 @@ export default function squarePiExtension(pi) {
99
98
  return { promise, ack };
100
99
  };
101
100
 
101
+ // Each operation carries its own session coordinate. Never rewrite the process environment:
102
+ // Pi owns PI_SESSION_ID and several SDK sessions may coexist in one process.
103
+ const sessionEnv = (id) => ({ ...process.env, ...(id === undefined ? {} : { PI_SESSION_ID: id }) });
104
+
102
105
  // A transport may keep its promise pending while Pi is shutting down or
103
106
  // replacing a session. Never make a lifecycle hook wait for that transport.
104
107
  const stopWatcher = () => {
@@ -129,7 +132,7 @@ export default function squarePiExtension(pi) {
129
132
  excludeKeys: handledPending,
130
133
  skipImmediate: true,
131
134
  onChangeArmed: resolveArmed,
132
- }).catch(() => {
135
+ }, sessionEnv(sessionId)).catch(() => {
133
136
  resolveArmed(false);
134
137
  return [];
135
138
  }).finally(() => signal.removeEventListener('abort', abort));
@@ -152,7 +155,7 @@ export default function squarePiExtension(pi) {
152
155
  try {
153
156
  pending = deferredRetry && retryWait !== undefined
154
157
  ? await retryWait
155
- : await waitForSessionPending(sessionId, 30_000, { signal, excludeKeys: handledPending });
158
+ : await waitForSessionPending(sessionId, 30_000, { signal, excludeKeys: handledPending }, sessionEnv(sessionId));
156
159
  } catch {
157
160
  await pause(signal, 1_000);
158
161
  continue;
@@ -194,7 +197,7 @@ export default function squarePiExtension(pi) {
194
197
  return landing.promise;
195
198
  },
196
199
  async (id, env) => {
197
- const inbox = await sessionInbox(id, env);
200
+ const inbox = await sessionInbox(id, env ?? sessionEnv(id));
198
201
  if (signal.aborted || token !== generation) return [];
199
202
  for (const key of inboxKeys(inbox)) observedPending.add(key);
200
203
  return inbox.map((membership) => ({
@@ -204,7 +207,7 @@ export default function squarePiExtension(pi) {
204
207
  )),
205
208
  }));
206
209
  },
207
- undefined,
210
+ sessionEnv(sessionId),
208
211
  signal,
209
212
  );
210
213
  for (const key of keys) handledPending.add(key);
@@ -239,12 +242,10 @@ export default function squarePiExtension(pi) {
239
242
  currentRunSignal = undefined;
240
243
  sessionId = ctx.sessionManager.getSessionId();
241
244
  sessionCwd = ctx.cwd || process.cwd();
242
- previousSessionId = process.env.SQUARE_PI_SESSION_ID;
243
- process.env.SQUARE_PI_SESSION_ID = sessionId;
244
245
  const token = generation;
245
246
  watcherAbort = new AbortController();
246
247
  const signal = watcherAbort.signal;
247
- void automaticSessionStart('pi', sessionId, sessionCwd).then((context) => {
248
+ void automaticSessionStart('pi', sessionId, sessionCwd, sessionEnv(sessionId)).then((context) => {
248
249
  if (context === undefined || sessionId === undefined || token !== generation) return;
249
250
  const landing = waitForLanding(context, signal, 'nextTurn');
250
251
  try {
@@ -311,7 +312,7 @@ export default function squarePiExtension(pi) {
311
312
  const token = generation;
312
313
  // Cancel the entire pending batch, including entries not yet sent to Pi.
313
314
  // This is local suppression, never evidence that the model saw the activity.
314
- const cancelled = await sessionInbox(sessionId);
315
+ const cancelled = await sessionInbox(sessionId, sessionEnv(sessionId));
315
316
  if (token !== generation || sessionId === undefined) return;
316
317
  for (const key of inboxKeys(cancelled)) handledPending.add(key);
317
318
  cancelledRun = false;
@@ -326,11 +327,7 @@ export default function squarePiExtension(pi) {
326
327
  generation += 1;
327
328
  failAcks(new Error('Pi session ended'));
328
329
  stopWatcher();
329
- if (sessionId && sessionCwd) void automaticSessionEnd('pi', sessionId, sessionCwd).catch(() => undefined);
330
- if (process.env.SQUARE_PI_SESSION_ID === sessionId) {
331
- if (previousSessionId === undefined) delete process.env.SQUARE_PI_SESSION_ID;
332
- else process.env.SQUARE_PI_SESSION_ID = previousSessionId;
333
- }
330
+ if (sessionId && sessionCwd) void automaticSessionEnd('pi', sessionId, sessionCwd, sessionEnv(sessionId)).catch(() => undefined);
334
331
  sessionId = undefined;
335
332
  sessionCwd = undefined;
336
333
  retryWait = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.49",
3
+ "version": "0.3.51",
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": {
@@ -18,6 +18,7 @@
18
18
  "clean": "node --input-type=module -e \"import fs from 'node:fs'; fs.rmSync('dist',{recursive:true,force:true})\"",
19
19
  "build": "npm run clean && tsc && node --input-type=module -e \"import fs from 'node:fs'; fs.chmodSync('dist/square.js',0o755)\"",
20
20
  "verify-release": "node test/release.test.js",
21
+ "release": "npm version patch && npm publish && git push --follow-tags",
21
22
  "prepublishOnly": "npm run build && npm run verify-release",
22
23
  "test": "npm run build && node --test test/*.test.js",
23
24
  "test:cli-process": "npm run build && node --test test/process/square-cli-render.test.js"