@astrosheep/square 0.3.47 → 0.3.48
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.
|
@@ -30,6 +30,9 @@ export function pendingAtBoundary(inbox) {
|
|
|
30
30
|
export function renderPendingAtBoundary(inbox) {
|
|
31
31
|
return renderBoundary(inbox).context;
|
|
32
32
|
}
|
|
33
|
+
function frameBoundary(lines) {
|
|
34
|
+
return `\n${lines.join('\n')}\n`;
|
|
35
|
+
}
|
|
33
36
|
function renderBoundary(inbox) {
|
|
34
37
|
const count = pendingCount(inbox);
|
|
35
38
|
const noun = count === 1 ? 'notification' : 'notifications';
|
|
@@ -52,7 +55,7 @@ function renderBoundary(inbox) {
|
|
|
52
55
|
}),
|
|
53
56
|
].join('\n');
|
|
54
57
|
const omittedAfter = omitted + queued.length - index - 1;
|
|
55
|
-
const prospective = [
|
|
58
|
+
const prospective = frameBoundary([
|
|
56
59
|
header,
|
|
57
60
|
...blocks,
|
|
58
61
|
block,
|
|
@@ -60,7 +63,7 @@ function renderBoundary(inbox) {
|
|
|
60
63
|
? [`… ${omittedAfter} ${omittedAfter === 1 ? 'notification' : 'notifications'} omitted.`]
|
|
61
64
|
: []),
|
|
62
65
|
...footer,
|
|
63
|
-
]
|
|
66
|
+
]);
|
|
64
67
|
if (prospective.length > CONTEXT_MAX) {
|
|
65
68
|
omitted += 1;
|
|
66
69
|
continue;
|
|
@@ -73,14 +76,14 @@ function renderBoundary(inbox) {
|
|
|
73
76
|
});
|
|
74
77
|
}
|
|
75
78
|
return {
|
|
76
|
-
context: [
|
|
79
|
+
context: frameBoundary([
|
|
77
80
|
header,
|
|
78
81
|
...blocks,
|
|
79
82
|
...(omitted > 0
|
|
80
83
|
? [`… ${omitted} ${omitted === 1 ? 'notification' : 'notifications'} omitted.`]
|
|
81
84
|
: []),
|
|
82
85
|
...footer,
|
|
83
|
-
]
|
|
86
|
+
]),
|
|
84
87
|
complete,
|
|
85
88
|
};
|
|
86
89
|
}
|
package/extensions/square-pi.js
CHANGED
|
@@ -4,18 +4,17 @@ import { 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
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
class PiDeliveryDroppedError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super('Pi discarded the queued Square notification');
|
|
10
|
+
this.name = 'PiDeliveryDroppedError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
9
13
|
|
|
10
14
|
function sessionBindings(sessionId) {
|
|
11
15
|
return projectSessionBindings({ hostLedger: createHostLedgerPort(), sessionId });
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
function piBoundaryTimeoutMs() {
|
|
15
|
-
const configured = Number.parseInt(process.env.SQUARE_PI_BOUNDARY_TIMEOUT_MS || '', 10);
|
|
16
|
-
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PI_BOUNDARY_TIMEOUT_MS;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
18
|
export function pendingInbox(inbox) {
|
|
20
19
|
return inbox.filter((item) => item.notifications?.length > 0);
|
|
21
20
|
}
|
|
@@ -33,67 +32,87 @@ export function renderPiInbox(inbox) {
|
|
|
33
32
|
export default function squarePiExtension(pi) {
|
|
34
33
|
let sessionId;
|
|
35
34
|
let sessionCwd;
|
|
36
|
-
let joiningContext;
|
|
37
35
|
let previousSessionId;
|
|
38
36
|
let watcher;
|
|
39
37
|
let watcherAbort;
|
|
40
38
|
let generation = 0;
|
|
41
|
-
let presenting = false;
|
|
42
|
-
let settledSerial = 0;
|
|
43
|
-
let settledWaiters = [];
|
|
44
39
|
const handledPending = new Set();
|
|
40
|
+
const landingAcks = new Map();
|
|
45
41
|
let retryAfterChange = false;
|
|
46
42
|
let retryWait;
|
|
47
|
-
|
|
43
|
+
let turnIndex = 0;
|
|
44
|
+
let activeTurn;
|
|
45
|
+
let currentRunSignal;
|
|
46
|
+
const deferredSteers = new Set();
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
boundaryPending = pending;
|
|
70
|
-
void pending?.finally(() => {
|
|
71
|
-
if (boundaryPending === pending) boundaryPending = undefined;
|
|
72
|
-
}).catch(() => undefined);
|
|
73
|
-
return await Promise.race([pending, expired]);
|
|
74
|
-
} finally {
|
|
75
|
-
clearTimeout(timer);
|
|
76
|
-
boundaryControllers.delete(controller);
|
|
77
|
-
}
|
|
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();
|
|
78
68
|
};
|
|
79
69
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
70
|
+
const failAcks = (error) => {
|
|
71
|
+
const acks = [...landingAcks.values()].flatMap((waiters) => [...waiters]);
|
|
72
|
+
for (const ack of acks) ack.settle(error);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const acknowledgeLanding = (message) => {
|
|
76
|
+
if (message?.role !== 'custom' || message.customType !== 'square' || typeof message.content !== 'string') return;
|
|
77
|
+
const acks = landingAcks.get(message.content);
|
|
78
|
+
if (acks === undefined) return;
|
|
79
|
+
for (const ack of [...acks]) ack.settle();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const waitForLanding = (content, signal, kind) => {
|
|
83
|
+
let ack;
|
|
84
|
+
const promise = new Promise((resolve, reject) => {
|
|
85
|
+
const abort = () => ack.settle(signal.reason || new Error('Pi native injection aborted'));
|
|
86
|
+
ack = {
|
|
87
|
+
kind,
|
|
88
|
+
sentAfterTurn: turnIndex,
|
|
89
|
+
emptyTurnWindows: 0,
|
|
90
|
+
dropAfterSettled: false,
|
|
91
|
+
settle(error) {
|
|
92
|
+
if (ack.settled) return;
|
|
93
|
+
ack.settled = true;
|
|
94
|
+
signal.removeEventListener('abort', abort);
|
|
95
|
+
const acks = landingAcks.get(content);
|
|
96
|
+
if (acks?.delete(ack) && acks.size === 0) landingAcks.delete(content);
|
|
97
|
+
if (error === undefined) resolve();
|
|
98
|
+
else reject(error);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
let acks = landingAcks.get(content);
|
|
102
|
+
if (acks === undefined) {
|
|
103
|
+
acks = new Set();
|
|
104
|
+
landingAcks.set(content, acks);
|
|
105
|
+
}
|
|
106
|
+
acks.add(ack);
|
|
107
|
+
if (signal.aborted) abort();
|
|
108
|
+
else signal.addEventListener('abort', abort, { once: true });
|
|
90
109
|
});
|
|
110
|
+
return { promise, ack };
|
|
91
111
|
};
|
|
92
112
|
|
|
93
113
|
// A transport may keep its promise pending while Pi is shutting down or
|
|
94
114
|
// replacing a session. Never make a lifecycle hook wait for that transport.
|
|
95
115
|
const stopWatcher = () => {
|
|
96
|
-
for (const controller of boundaryControllers) controller.abort(new Error('Pi session ended'));
|
|
97
116
|
watcherAbort?.abort();
|
|
98
117
|
watcher = undefined;
|
|
99
118
|
watcherAbort = undefined;
|
|
@@ -109,7 +128,7 @@ export default function squarePiExtension(pi) {
|
|
|
109
128
|
signal.addEventListener('abort', finish, { once: true });
|
|
110
129
|
});
|
|
111
130
|
|
|
112
|
-
const wake = async (
|
|
131
|
+
const wake = async (token, signal) => {
|
|
113
132
|
const armDeferredRetry = () => {
|
|
114
133
|
const controller = new AbortController();
|
|
115
134
|
let resolveArmed;
|
|
@@ -129,14 +148,26 @@ export default function squarePiExtension(pi) {
|
|
|
129
148
|
};
|
|
130
149
|
|
|
131
150
|
while (sessionId !== undefined && token === generation && !signal.aborted) {
|
|
132
|
-
|
|
151
|
+
try {
|
|
152
|
+
if ((await sessionBindings(sessionId)).length === 0) {
|
|
153
|
+
await pause(signal, 1_000);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
133
157
|
await pause(signal, 1_000);
|
|
134
158
|
continue;
|
|
135
159
|
}
|
|
160
|
+
|
|
136
161
|
const deferredRetry = retryAfterChange;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
let pending;
|
|
163
|
+
try {
|
|
164
|
+
pending = deferredRetry && retryWait !== undefined
|
|
165
|
+
? await retryWait
|
|
166
|
+
: await waitForSessionPending(sessionId, 30_000, { signal, excludeKeys: handledPending });
|
|
167
|
+
} catch {
|
|
168
|
+
await pause(signal, 1_000);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
140
171
|
if (sessionId === undefined || token !== generation || signal.aborted) return;
|
|
141
172
|
if (pending.length === 0) {
|
|
142
173
|
if (deferredRetry) {
|
|
@@ -147,112 +178,136 @@ export default function squarePiExtension(pi) {
|
|
|
147
178
|
}
|
|
148
179
|
retryAfterChange = false;
|
|
149
180
|
retryWait = undefined;
|
|
150
|
-
|
|
151
|
-
const serial = settledSerial;
|
|
152
|
-
await waitForSettled(serial, signal);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (presenting) continue;
|
|
181
|
+
|
|
156
182
|
const keys = inboxKeys(pending);
|
|
157
|
-
presenting = true;
|
|
158
183
|
const deferred = armDeferredRetry();
|
|
159
184
|
const retryArmed = await deferred.armed;
|
|
160
185
|
if (!retryArmed) {
|
|
161
186
|
deferred.cancel();
|
|
162
|
-
presenting = false;
|
|
163
187
|
continue;
|
|
164
188
|
}
|
|
165
189
|
let keepDeferred = false;
|
|
166
190
|
try {
|
|
167
|
-
|
|
191
|
+
await presentPendingAtBoundary(
|
|
168
192
|
sessionId,
|
|
169
193
|
async (content) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
{ deliverAs: 'steer', triggerTurn: true },
|
|
173
|
-
));
|
|
174
|
-
let timer;
|
|
194
|
+
if (currentRunSignal?.aborted) await waitForAgentSettled(signal);
|
|
195
|
+
const landing = waitForLanding(content, signal, 'steer');
|
|
175
196
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
signal.addEventListener('abort', () => reject(signal.reason || new Error('Pi native injection aborted')), { once: true });
|
|
184
|
-
}),
|
|
185
|
-
new Promise((_, reject) => {
|
|
186
|
-
timer = setTimeout(() => reject(new Error('Pi native injection timed out')), PI_SEND_TIMEOUT_MS);
|
|
187
|
-
}),
|
|
188
|
-
]);
|
|
189
|
-
} finally {
|
|
190
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
197
|
+
Promise.resolve(pi.sendMessage(
|
|
198
|
+
{ customType: 'square', content, display: true },
|
|
199
|
+
{ deliverAs: 'steer', triggerTurn: true },
|
|
200
|
+
)).catch((error) => landing.ack.settle(error));
|
|
201
|
+
} catch (error) {
|
|
202
|
+
landing.ack.settle(error);
|
|
191
203
|
}
|
|
192
|
-
return
|
|
204
|
+
return landing.promise;
|
|
193
205
|
},
|
|
194
206
|
undefined,
|
|
195
207
|
undefined,
|
|
196
208
|
signal,
|
|
197
209
|
);
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
for (const key of keys) handledPending.add(key);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (error instanceof PiDeliveryDroppedError) {
|
|
213
|
+
deferred.cancel();
|
|
214
|
+
continue;
|
|
200
215
|
}
|
|
201
|
-
} catch {
|
|
202
216
|
// The next state edge is already being observed before native injection starts.
|
|
203
217
|
retryAfterChange = true;
|
|
204
218
|
retryWait = deferred.pending;
|
|
205
219
|
keepDeferred = true;
|
|
206
220
|
} finally {
|
|
207
221
|
if (!keepDeferred) deferred.cancel();
|
|
208
|
-
presenting = false;
|
|
209
222
|
}
|
|
210
223
|
}
|
|
211
224
|
};
|
|
212
225
|
|
|
213
226
|
pi.on('session_start', async (_event, ctx) => {
|
|
214
227
|
generation += 1;
|
|
228
|
+
failAcks(new Error('Pi session replaced'));
|
|
215
229
|
stopWatcher();
|
|
216
230
|
handledPending.clear();
|
|
217
231
|
retryAfterChange = false;
|
|
218
232
|
retryWait = undefined;
|
|
233
|
+
turnIndex = 0;
|
|
234
|
+
activeTurn = undefined;
|
|
235
|
+
currentRunSignal = undefined;
|
|
219
236
|
sessionId = ctx.sessionManager.getSessionId();
|
|
220
237
|
sessionCwd = ctx.cwd || process.cwd();
|
|
221
238
|
previousSessionId = process.env.SQUARE_PI_SESSION_ID;
|
|
222
239
|
process.env.SQUARE_PI_SESSION_ID = sessionId;
|
|
223
|
-
joiningContext = undefined;
|
|
224
|
-
void automaticSessionStart('pi', sessionId, sessionCwd).catch(() => undefined);
|
|
225
|
-
watcherAbort = new AbortController();
|
|
226
240
|
const token = generation;
|
|
227
|
-
|
|
241
|
+
watcherAbort = new AbortController();
|
|
242
|
+
const signal = watcherAbort.signal;
|
|
243
|
+
void automaticSessionStart('pi', sessionId, sessionCwd).then((context) => {
|
|
244
|
+
if (context === undefined || sessionId === undefined || token !== generation) return;
|
|
245
|
+
const landing = waitForLanding(context, signal, 'nextTurn');
|
|
246
|
+
try {
|
|
247
|
+
Promise.resolve(pi.sendMessage(
|
|
248
|
+
{ customType: 'square', content: context, display: true },
|
|
249
|
+
{ deliverAs: 'nextTurn' },
|
|
250
|
+
)).catch((error) => landing.ack.settle(error));
|
|
251
|
+
} catch {
|
|
252
|
+
// Joining context is advisory; an unavailable Pi transport does not block startup.
|
|
253
|
+
landing.ack.settle(new Error('Pi joining-context injection failed'));
|
|
254
|
+
}
|
|
255
|
+
void landing.promise.catch(() => undefined);
|
|
256
|
+
}).catch(() => undefined);
|
|
257
|
+
watcher = wake(token, signal).catch(() => undefined);
|
|
228
258
|
});
|
|
229
259
|
|
|
230
|
-
pi.on('
|
|
231
|
-
|
|
232
|
-
const waiters = settledWaiters;
|
|
233
|
-
settledWaiters = [];
|
|
234
|
-
for (const waiter of waiters) waiter.resolve();
|
|
260
|
+
pi.on('agent_start', async (_event, ctx) => {
|
|
261
|
+
currentRunSignal = ctx?.getSignal?.();
|
|
235
262
|
});
|
|
236
263
|
|
|
237
|
-
pi.on('
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
264
|
+
pi.on('message_end', async (event) => {
|
|
265
|
+
if (activeTurn !== undefined && (event.message?.role === 'user' || event.message?.role === 'custom')) {
|
|
266
|
+
activeTurn.hasInput = true;
|
|
267
|
+
}
|
|
268
|
+
acknowledgeLanding(event.message);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
pi.on('turn_start', async () => {
|
|
272
|
+
turnIndex += 1;
|
|
273
|
+
activeTurn = { index: turnIndex, hasInput: false, signal: currentRunSignal };
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
pi.on('turn_end', async (event, ctx) => {
|
|
277
|
+
const turn = activeTurn;
|
|
278
|
+
activeTurn = undefined;
|
|
279
|
+
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;
|
|
243
283
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (turn === undefined) return;
|
|
287
|
+
for (const ack of acks) {
|
|
288
|
+
if (ack.kind !== 'steer' || ack.dropAfterSettled || turn.index <= ack.sentAfterTurn) continue;
|
|
289
|
+
if (turn.hasInput) {
|
|
290
|
+
ack.emptyTurnWindows = 0;
|
|
291
|
+
} else {
|
|
292
|
+
ack.emptyTurnWindows += 1;
|
|
293
|
+
if (ack.emptyTurnWindows >= 2) ack.settle(new PiDeliveryDroppedError());
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
pi.on('agent_settled', async () => {
|
|
299
|
+
currentRunSignal = undefined;
|
|
300
|
+
const acks = [...landingAcks.values()].flatMap((waiters) => [...waiters]);
|
|
301
|
+
for (const ack of acks) {
|
|
302
|
+
if (ack.dropAfterSettled) ack.settle(new PiDeliveryDroppedError());
|
|
248
303
|
}
|
|
304
|
+
releaseDeferredSteers();
|
|
249
305
|
});
|
|
250
306
|
|
|
251
307
|
pi.on('session_shutdown', async () => {
|
|
252
308
|
generation += 1;
|
|
309
|
+
failAcks(new Error('Pi session ended'));
|
|
253
310
|
stopWatcher();
|
|
254
|
-
for (const waiter of settledWaiters) waiter.resolve();
|
|
255
|
-
settledWaiters = [];
|
|
256
311
|
if (sessionId && sessionCwd) void automaticSessionEnd('pi', sessionId, sessionCwd).catch(() => undefined);
|
|
257
312
|
if (process.env.SQUARE_PI_SESSION_ID === sessionId) {
|
|
258
313
|
if (previousSessionId === undefined) delete process.env.SQUARE_PI_SESSION_ID;
|
|
@@ -260,8 +315,8 @@ export default function squarePiExtension(pi) {
|
|
|
260
315
|
}
|
|
261
316
|
sessionId = undefined;
|
|
262
317
|
sessionCwd = undefined;
|
|
263
|
-
joiningContext = undefined;
|
|
264
|
-
presenting = false;
|
|
265
318
|
retryWait = undefined;
|
|
319
|
+
activeTurn = undefined;
|
|
320
|
+
currentRunSignal = undefined;
|
|
266
321
|
});
|
|
267
322
|
}
|