@astrosheep/square 0.3.48 → 0.3.50

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.48",
3
+ "version": "0.3.50",
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.48",
3
+ "version": "0.3.50",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -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
  }
@@ -7,7 +7,7 @@ import { openSquare } from './square-file-adapter.js';
7
7
  import { closeOpenSquare } from './open-square.js';
8
8
  import { presentPending } from './presentation-operations.js';
9
9
  import { sessionInbox } from './inbox.js';
10
- import { ATTENTION_BODY_MAX, renderAttentionPreview } from './attention-presentation.js';
10
+ import { renderAttentionPreview } from './attention-presentation.js';
11
11
  const CONTEXT_MAX = 1200;
12
12
  const presentationLocks = new Map();
13
13
  function pendingCount(inbox) {
@@ -72,7 +72,9 @@ function renderBoundary(inbox) {
72
72
  complete.push({
73
73
  membership,
74
74
  actIndexes: [notification.actIndex],
75
- markSeen: notification.body.replace(/\r\n/g, '\n').length <= ATTENTION_BODY_MAX,
75
+ // The hook output is itself a user-visible presentation, even when the
76
+ // preview had to clip the body. Keep clipped items from being replayed.
77
+ markSeen: true,
76
78
  });
77
79
  }
78
80
  return {
@@ -17,9 +17,11 @@ export async function codexHookResponse(input, lookup = sessionInbox, env = proc
17
17
  if (hookEventName === undefined)
18
18
  return undefined;
19
19
  await recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env);
20
- const response = await presentPendingAtBoundary(input.session_id, (context) => hookEventName === 'Stop'
21
- ? { systemMessage: context }
22
- : { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }, lookup, env);
20
+ if (hookEventName === 'Stop') {
21
+ await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters, sweepDeadline).catch(() => undefined);
22
+ return undefined;
23
+ }
24
+ const response = await presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }), lookup, env);
23
25
  await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters, sweepDeadline).catch(() => undefined);
24
26
  return response;
25
27
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { Square } from './square-wiring.js';
2
2
  export { SquareError } from './model.js';
3
3
  export { bindCurrentParticipant, squareAssignedParticipantName, unbindCurrentParticipant } from './registry.js';
4
+ export { createDefaultWakeTransport } from './notifications.js';
4
5
  export { createHostLedgerPort, FileHostLedgerPort } from './host-ledger-file-adapter.js';
5
6
  export type { HostLedgerPort, PresenceRecord, EvidenceRecord } from './host-ledger.js';
6
7
  export type { PresentationSinkPort } from './ports.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Square } from './square-wiring.js';
2
2
  export { SquareError } from './model.js';
3
3
  export { bindCurrentParticipant, squareAssignedParticipantName, unbindCurrentParticipant } from './registry.js';
4
+ export { createDefaultWakeTransport } from './notifications.js';
4
5
  export { createHostLedgerPort, FileHostLedgerPort } from './host-ledger-file-adapter.js';
@@ -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();
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 {
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import { presentPendingAtBoundary, renderPendingAtBoundary } from '../dist/boundary-presentation.js';
2
2
  import { automaticSessionEnd, automaticSessionStart } from '../dist/automatic-session.js';
3
- import { waitForSessionPending } from '../dist/inbox.js';
3
+ import { sessionInbox, waitForSessionPending } from '../dist/inbox.js';
4
4
  import { projectSessionBindings } from '../dist/square-projections.js';
5
5
  import { createHostLedgerPort } from '../dist/host-ledger-file-adapter.js';
6
6
 
@@ -32,39 +32,28 @@ 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;
39
38
  const handledPending = new Set();
39
+ const observedPending = new Set();
40
40
  const landingAcks = new Map();
41
41
  let retryAfterChange = false;
42
42
  let retryWait;
43
43
  let turnIndex = 0;
44
44
  let activeTurn;
45
45
  let currentRunSignal;
46
- const deferredSteers = new Set();
46
+ let detachRunAbort;
47
+ let cancelledRun = false;
47
48
 
48
- const waitForAgentSettled = (signal) => new Promise((resolve, reject) => {
49
- const deferred = {
50
- release() {
51
- deferredSteers.delete(deferred);
52
- signal.removeEventListener('abort', abort);
53
- resolve();
54
- },
55
- };
56
- const abort = () => {
57
- deferredSteers.delete(deferred);
58
- signal.removeEventListener('abort', abort);
59
- reject(signal.reason || new Error('Pi native injection aborted'));
60
- };
61
- deferredSteers.add(deferred);
62
- if (signal.aborted) abort();
63
- else signal.addEventListener('abort', abort, { once: true });
64
- });
65
-
66
- const releaseDeferredSteers = () => {
67
- for (const deferred of [...deferredSteers]) deferred.release();
49
+ const cancelRunDelivery = () => {
50
+ cancelledRun = true;
51
+ for (const key of observedPending) handledPending.add(key);
52
+ generation += 1;
53
+ stopWatcher();
54
+ failAcks(new Error('Pi notification delivery cancelled'));
55
+ retryAfterChange = false;
56
+ retryWait = undefined;
68
57
  };
69
58
 
70
59
  const failAcks = (error) => {
@@ -87,7 +76,6 @@ export default function squarePiExtension(pi) {
87
76
  kind,
88
77
  sentAfterTurn: turnIndex,
89
78
  emptyTurnWindows: 0,
90
- dropAfterSettled: false,
91
79
  settle(error) {
92
80
  if (ack.settled) return;
93
81
  ack.settled = true;
@@ -110,6 +98,10 @@ export default function squarePiExtension(pi) {
110
98
  return { promise, ack };
111
99
  };
112
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
+
113
105
  // A transport may keep its promise pending while Pi is shutting down or
114
106
  // replacing a session. Never make a lifecycle hook wait for that transport.
115
107
  const stopWatcher = () => {
@@ -140,7 +132,7 @@ export default function squarePiExtension(pi) {
140
132
  excludeKeys: handledPending,
141
133
  skipImmediate: true,
142
134
  onChangeArmed: resolveArmed,
143
- }).catch(() => {
135
+ }, sessionEnv(sessionId)).catch(() => {
144
136
  resolveArmed(false);
145
137
  return [];
146
138
  }).finally(() => signal.removeEventListener('abort', abort));
@@ -163,7 +155,7 @@ export default function squarePiExtension(pi) {
163
155
  try {
164
156
  pending = deferredRetry && retryWait !== undefined
165
157
  ? await retryWait
166
- : await waitForSessionPending(sessionId, 30_000, { signal, excludeKeys: handledPending });
158
+ : await waitForSessionPending(sessionId, 30_000, { signal, excludeKeys: handledPending }, sessionEnv(sessionId));
167
159
  } catch {
168
160
  await pause(signal, 1_000);
169
161
  continue;
@@ -180,6 +172,7 @@ export default function squarePiExtension(pi) {
180
172
  retryWait = undefined;
181
173
 
182
174
  const keys = inboxKeys(pending);
175
+ for (const key of keys) observedPending.add(key);
183
176
  const deferred = armDeferredRetry();
184
177
  const retryArmed = await deferred.armed;
185
178
  if (!retryArmed) {
@@ -191,7 +184,7 @@ export default function squarePiExtension(pi) {
191
184
  await presentPendingAtBoundary(
192
185
  sessionId,
193
186
  async (content) => {
194
- if (currentRunSignal?.aborted) await waitForAgentSettled(signal);
187
+ if (signal.aborted || currentRunSignal?.aborted) throw new Error('Pi notification delivery cancelled');
195
188
  const landing = waitForLanding(content, signal, 'steer');
196
189
  try {
197
190
  Promise.resolve(pi.sendMessage(
@@ -203,12 +196,23 @@ export default function squarePiExtension(pi) {
203
196
  }
204
197
  return landing.promise;
205
198
  },
206
- undefined,
207
- undefined,
199
+ async (id, env) => {
200
+ const inbox = await sessionInbox(id, env ?? sessionEnv(id));
201
+ if (signal.aborted || token !== generation) return [];
202
+ for (const key of inboxKeys(inbox)) observedPending.add(key);
203
+ return inbox.map((membership) => ({
204
+ ...membership,
205
+ notifications: membership.notifications.filter((note) => !handledPending.has(
206
+ `${membership.squarePath}\u0000${membership.name.toLocaleLowerCase()}\u0000${note.actIndex}`,
207
+ )),
208
+ }));
209
+ },
210
+ sessionEnv(sessionId),
208
211
  signal,
209
212
  );
210
213
  for (const key of keys) handledPending.add(key);
211
214
  } catch (error) {
215
+ if (signal.aborted || token !== generation) return;
212
216
  if (error instanceof PiDeliveryDroppedError) {
213
217
  deferred.cancel();
214
218
  continue;
@@ -224,10 +228,13 @@ export default function squarePiExtension(pi) {
224
228
  };
225
229
 
226
230
  pi.on('session_start', async (_event, ctx) => {
231
+ detachRunAbort?.();
232
+ cancelledRun = false;
227
233
  generation += 1;
228
234
  failAcks(new Error('Pi session replaced'));
229
235
  stopWatcher();
230
236
  handledPending.clear();
237
+ observedPending.clear();
231
238
  retryAfterChange = false;
232
239
  retryWait = undefined;
233
240
  turnIndex = 0;
@@ -235,12 +242,10 @@ export default function squarePiExtension(pi) {
235
242
  currentRunSignal = undefined;
236
243
  sessionId = ctx.sessionManager.getSessionId();
237
244
  sessionCwd = ctx.cwd || process.cwd();
238
- previousSessionId = process.env.SQUARE_PI_SESSION_ID;
239
- process.env.SQUARE_PI_SESSION_ID = sessionId;
240
245
  const token = generation;
241
246
  watcherAbort = new AbortController();
242
247
  const signal = watcherAbort.signal;
243
- void automaticSessionStart('pi', sessionId, sessionCwd).then((context) => {
248
+ void automaticSessionStart('pi', sessionId, sessionCwd, sessionEnv(sessionId)).then((context) => {
244
249
  if (context === undefined || sessionId === undefined || token !== generation) return;
245
250
  const landing = waitForLanding(context, signal, 'nextTurn');
246
251
  try {
@@ -258,7 +263,13 @@ export default function squarePiExtension(pi) {
258
263
  });
259
264
 
260
265
  pi.on('agent_start', async (_event, ctx) => {
266
+ detachRunAbort?.();
261
267
  currentRunSignal = ctx?.getSignal?.();
268
+ if (ctx?.mode !== 'tui' || currentRunSignal === undefined) return;
269
+ const runSignal = currentRunSignal;
270
+ detachRunAbort = () => runSignal.removeEventListener('abort', cancelRunDelivery);
271
+ if (runSignal.aborted) cancelRunDelivery();
272
+ else runSignal.addEventListener('abort', cancelRunDelivery, { once: true });
262
273
  });
263
274
 
264
275
  pi.on('message_end', async (event) => {
@@ -277,15 +288,13 @@ export default function squarePiExtension(pi) {
277
288
  const turn = activeTurn;
278
289
  activeTurn = undefined;
279
290
  const acks = [...landingAcks.values()].flatMap((waiters) => [...waiters]);
280
- if (event.message?.stopReason === 'aborted' && ctx?.mode === 'tui' && turn?.signal !== undefined) {
281
- for (const ack of acks) {
282
- if (ack.kind === 'steer') ack.dropAfterSettled = true;
283
- }
291
+ if (event.message?.stopReason === 'aborted' && ctx?.mode === 'tui') {
292
+ if (!cancelledRun) cancelRunDelivery();
284
293
  return;
285
294
  }
286
295
  if (turn === undefined) return;
287
296
  for (const ack of acks) {
288
- if (ack.kind !== 'steer' || ack.dropAfterSettled || turn.index <= ack.sentAfterTurn) continue;
297
+ if (ack.kind !== 'steer' || turn.index <= ack.sentAfterTurn) continue;
289
298
  if (turn.hasInput) {
290
299
  ack.emptyTurnWindows = 0;
291
300
  } else {
@@ -296,23 +305,29 @@ export default function squarePiExtension(pi) {
296
305
  });
297
306
 
298
307
  pi.on('agent_settled', async () => {
308
+ detachRunAbort?.();
309
+ detachRunAbort = undefined;
299
310
  currentRunSignal = undefined;
300
- const acks = [...landingAcks.values()].flatMap((waiters) => [...waiters]);
301
- for (const ack of acks) {
302
- if (ack.dropAfterSettled) ack.settle(new PiDeliveryDroppedError());
303
- }
304
- releaseDeferredSteers();
311
+ if (!cancelledRun || sessionId === undefined) return;
312
+ const token = generation;
313
+ // Cancel the entire pending batch, including entries not yet sent to Pi.
314
+ // This is local suppression, never evidence that the model saw the activity.
315
+ const cancelled = await sessionInbox(sessionId, sessionEnv(sessionId));
316
+ if (token !== generation || sessionId === undefined) return;
317
+ for (const key of inboxKeys(cancelled)) handledPending.add(key);
318
+ cancelledRun = false;
319
+ watcherAbort = new AbortController();
320
+ watcher = wake(token, watcherAbort.signal).catch(() => undefined);
305
321
  });
306
322
 
307
323
  pi.on('session_shutdown', async () => {
324
+ detachRunAbort?.();
325
+ detachRunAbort = undefined;
326
+ cancelledRun = false;
308
327
  generation += 1;
309
328
  failAcks(new Error('Pi session ended'));
310
329
  stopWatcher();
311
- if (sessionId && sessionCwd) void automaticSessionEnd('pi', sessionId, sessionCwd).catch(() => undefined);
312
- if (process.env.SQUARE_PI_SESSION_ID === sessionId) {
313
- if (previousSessionId === undefined) delete process.env.SQUARE_PI_SESSION_ID;
314
- else process.env.SQUARE_PI_SESSION_ID = previousSessionId;
315
- }
330
+ if (sessionId && sessionCwd) void automaticSessionEnd('pi', sessionId, sessionCwd, sessionEnv(sessionId)).catch(() => undefined);
316
331
  sessionId = undefined;
317
332
  sessionCwd = undefined;
318
333
  retryWait = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.48",
3
+ "version": "0.3.50",
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": {