@astrosheep/square 0.3.5 → 0.3.7
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.
- package/codex-plugin/.codex-plugin/plugin.json +3 -2
- package/codex-plugin/hooks/hooks.json +3 -14
- package/dist/activity-feed.js +26 -18
- package/dist/activity.js +10 -10
- package/dist/artifact.js +138 -203
- package/dist/boundary-presentation.js +77 -0
- package/dist/claude-hook.js +4 -94
- package/dist/cli/context.js +7 -7
- package/dist/cli/maintenance-commands.js +10 -26
- package/dist/cli/meta-commands.js +3 -6
- package/dist/cli/observation-commands.js +48 -53
- package/dist/cli/program.js +4 -4
- package/dist/cli/registry.js +5 -5
- package/dist/cli/square-commands.js +27 -20
- package/dist/cmd/notify-once.js +23 -21
- package/dist/codex-hook.js +22 -0
- package/dist/compact.js +1 -1
- package/dist/decisions.js +61 -88
- package/dist/delivery-health.js +104 -210
- package/dist/delivery.js +68 -18
- package/dist/doctor.js +9 -8
- package/dist/harness-claude.js +38 -245
- package/dist/harness-codex.js +82 -616
- package/dist/harness-stage.js +36 -0
- package/dist/harness.js +3 -5
- package/dist/help.js +43 -35
- package/dist/inbox.js +12 -11
- package/dist/index.js +10 -121
- package/dist/list.js +1 -1
- package/dist/model.js +0 -6
- package/dist/notification-failures.js +54 -0
- package/dist/notifications.js +47 -62
- package/dist/paseo-delivery.js +160 -0
- package/dist/paseo-state.js +31 -0
- package/dist/paseo-timeline.js +58 -188
- package/dist/presentation.js +57 -64
- package/dist/presented.js +9 -8
- package/dist/registry.js +55 -45
- package/dist/runtime.js +27 -84
- package/dist/square-application.js +135 -130
- package/dist/square-core.js +3 -11
- package/dist/stream.js +27 -126
- package/dist/wake-sink.js +3 -214
- package/dist/watch.js +65 -122
- package/extensions/square-opencode.js +8 -73
- package/extensions/square-pi.js +8 -132
- package/guides/architect.md +3 -3
- package/guides/participant.md +25 -16
- package/package.json +2 -2
- package/skills/brainstorm/SKILL.md +25 -32
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +39 -107
- package/skills/square/hooks/hooks.json +2 -13
- package/skills/square-feedback/SKILL.md +4 -4
- package/dist/harness-lifecycle.js +0 -102
- package/dist/square-store.js +0 -111
- package/dist/terminal.js +0 -125
package/dist/notifications.js
CHANGED
|
@@ -2,56 +2,40 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { loadSquare } from './artifact.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isDeliveryDelivered, isPendingNotification, planActNotifications, } from './delivery.js';
|
|
6
|
+
import { recordNotificationFailure } from './notification-failures.js';
|
|
6
7
|
import { hasPresentedAttention } from './presented.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { defaultWakeSinks
|
|
10
|
-
export { planActNotifications }
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (raw === undefined)
|
|
15
|
-
return fallback;
|
|
16
|
-
const value = Number.parseInt(raw, 10);
|
|
17
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
18
|
-
throw new SquareError('invalid_args', `Invalid ${name}: expected a positive integer.`);
|
|
19
|
-
}
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
function resolveKnownParticipant(doc, name) {
|
|
23
|
-
const known = resolveRosterName(doc, name);
|
|
24
|
-
if (known === undefined) {
|
|
8
|
+
import { SquareError } from './model.js';
|
|
9
|
+
import { SLEEP_MS, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
|
|
10
|
+
import { defaultWakeSinks } from './paseo-delivery.js';
|
|
11
|
+
export { planActNotifications, matchesMentionTarget };
|
|
12
|
+
function known(doc, name) {
|
|
13
|
+
const value = resolveRosterName(doc, name);
|
|
14
|
+
if (value === undefined)
|
|
25
15
|
throw new SquareError('invalid_args', `Unknown participant "${name}". Expected one of: ${rosterNames(doc).join(', ')}.`);
|
|
26
|
-
|
|
27
|
-
return known;
|
|
16
|
+
return value;
|
|
28
17
|
}
|
|
18
|
+
export { notificationMessageId } from './delivery.js';
|
|
29
19
|
export function notificationDeliveryWaitMs() {
|
|
30
|
-
|
|
20
|
+
const value = Number.parseInt(process.env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? '5000', 10);
|
|
21
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
22
|
+
throw new SquareError('invalid_args', 'Invalid SQUARE_NOTIFY_DELIVERY_WAIT_MS: expected a positive integer.');
|
|
23
|
+
return value;
|
|
31
24
|
}
|
|
32
|
-
export function
|
|
25
|
+
export function hasDeliveredNotification(squarePath, name, ref) {
|
|
33
26
|
const doc = loadSquare(squarePath);
|
|
34
|
-
|
|
35
|
-
const index = typeof ref === 'number' ? ref : Number(ref.slice(4));
|
|
36
|
-
return isDeliveryDelivered(doc, known, index);
|
|
27
|
+
return isDeliveryDelivered(doc, known(doc, name), typeof ref === 'number' ? ref : Number(ref.slice(4)));
|
|
37
28
|
}
|
|
38
|
-
|
|
39
|
-
* Duplicate-wake suppression only. Delivery remains pending until the canonical
|
|
40
|
-
* recipient/act receipt is delivered; presented is a machine-local cache.
|
|
41
|
-
*/
|
|
42
|
-
export function hasAttentionMention(squarePath, name, ref, env = process.env) {
|
|
29
|
+
export function hasAttentionNotification(squarePath, name, ref, env = process.env) {
|
|
43
30
|
const doc = loadSquare(squarePath);
|
|
44
|
-
const
|
|
31
|
+
const recipient = known(doc, name);
|
|
45
32
|
const index = typeof ref === 'number' ? ref : Number(ref.slice(4));
|
|
46
|
-
|
|
47
|
-
return true;
|
|
48
|
-
return hasPresentedAttention(squarePath, known, index, env);
|
|
33
|
+
return isDeliveryDelivered(doc, recipient, index) || hasPresentedAttention(squarePath, recipient, index, env);
|
|
49
34
|
}
|
|
50
|
-
export async function
|
|
51
|
-
const
|
|
52
|
-
const deadline = Date.now() + timeoutMs;
|
|
35
|
+
export async function waitForDeliveredNotification(squarePath, name, ref, opts = {}) {
|
|
36
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 30000);
|
|
53
37
|
while (Date.now() <= deadline) {
|
|
54
|
-
if (
|
|
38
|
+
if (hasDeliveredNotification(squarePath, name, ref))
|
|
55
39
|
return true;
|
|
56
40
|
await sleep(Math.min(SLEEP_MS, Math.max(1, deadline - Date.now())));
|
|
57
41
|
}
|
|
@@ -59,39 +43,40 @@ export async function waitForDeliveredMention(squarePath, name, ref, opts = {})
|
|
|
59
43
|
}
|
|
60
44
|
export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
|
|
61
45
|
const doc = loadSquare(squarePath);
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
return;
|
|
65
|
-
const item = { act, index: actIndex };
|
|
66
|
-
const notifications = planActNotifications(doc, item).filter((notification) => notification.via === 'mention' || notification.via === 'bell');
|
|
67
|
-
const sinks = opts.sinks ?? defaultWakeSinks();
|
|
68
|
-
if (sinks.length === 0)
|
|
46
|
+
const item = doc.acts.find((candidate) => candidate.index === actIndex);
|
|
47
|
+
if (item === undefined)
|
|
69
48
|
return;
|
|
49
|
+
const notifications = planActNotifications(doc, item).filter(isPendingNotification);
|
|
70
50
|
for (const notification of notifications) {
|
|
71
|
-
|
|
72
|
-
if (hasAttentionMention(squarePath, notification.recipient, actIndex))
|
|
51
|
+
if (hasAttentionNotification(squarePath, notification.recipient, notification.item.index))
|
|
73
52
|
continue;
|
|
74
|
-
for (const sink of sinks) {
|
|
75
|
-
|
|
53
|
+
for (const sink of opts.sinks ?? defaultWakeSinks()) {
|
|
54
|
+
try {
|
|
55
|
+
await sink.dispatch(notification, { squarePath });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
recordNotificationFailure(squarePath, {
|
|
59
|
+
actIndex: notification.item.index,
|
|
60
|
+
recipient: notification.recipient,
|
|
61
|
+
route: notification.route,
|
|
62
|
+
sink: sink.name,
|
|
63
|
+
message: error instanceof Error ? error.message : String(error),
|
|
64
|
+
...(error instanceof Error && 'diagnostic' in error ? { diagnostic: error.diagnostic } : {}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
76
67
|
}
|
|
77
68
|
}
|
|
78
69
|
}
|
|
79
|
-
function
|
|
80
|
-
const child = spawn(process.execPath, [workerPath, ...args], {
|
|
81
|
-
detached: true,
|
|
82
|
-
stdio: 'ignore',
|
|
83
|
-
env: process.env,
|
|
84
|
-
});
|
|
70
|
+
function launchWorker(workerPath, args) {
|
|
71
|
+
const child = spawn(process.execPath, [workerPath, ...args], { detached: true, stdio: 'ignore', env: process.env });
|
|
85
72
|
child.unref();
|
|
86
73
|
}
|
|
74
|
+
/** Start one detached worker only when this act contains directed attention. */
|
|
87
75
|
export async function dispatchActNotifications(squarePath, item, opts = {}) {
|
|
88
|
-
if (process.env
|
|
76
|
+
if (process.env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
89
77
|
return;
|
|
90
78
|
const doc = loadSquare(squarePath);
|
|
91
|
-
|
|
92
|
-
if (notifications.length === 0)
|
|
79
|
+
if (!planActNotifications(doc, item).some(isPendingNotification))
|
|
93
80
|
return;
|
|
94
|
-
|
|
95
|
-
const launch = opts.launchWorker ?? launchDetachedWorker;
|
|
96
|
-
launch(workerPath, ['--square-path', squarePath, '--act-index', String(item.index)]);
|
|
81
|
+
(opts.launchWorker ?? launchWorker)(fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url)), ['--square-path', squarePath, '--act-index', String(item.index)]);
|
|
97
82
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { loadSquare } from './artifact.js';
|
|
4
|
+
import { isDeliveryDelivered, leaseOwnsNotification, } from './delivery.js';
|
|
5
|
+
import { sessionInbox } from './inbox.js';
|
|
6
|
+
import { hasPresentedForOwner, presentOnce } from './presented.js';
|
|
7
|
+
import { lookupParticipant } from './registry.js';
|
|
8
|
+
import { quoteShell } from './presentation.js';
|
|
9
|
+
import { isCurrentlyJoined, resolveRosterName } from './runtime.js';
|
|
10
|
+
import { discoverPaseoAgents, waitForPaseoWakeBoundary, } from './paseo-state.js';
|
|
11
|
+
import { sendPaseoWake } from './wake-sink.js';
|
|
12
|
+
export class PaseoWakeError extends Error {
|
|
13
|
+
diagnostic;
|
|
14
|
+
constructor(message, diagnostic) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.diagnostic = diagnostic;
|
|
17
|
+
this.name = 'PaseoWakeError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function endpoint() {
|
|
21
|
+
return process.env.SQUARE_PASEO_WS_URL ?? process.env.PASEO_LISTEN ?? '127.0.0.1:6767';
|
|
22
|
+
}
|
|
23
|
+
function diagnostic(phase, ownership, code) {
|
|
24
|
+
return {
|
|
25
|
+
phase,
|
|
26
|
+
code,
|
|
27
|
+
command: phase === 'discovery' ? 'paseo ls --json' : 'paseo send <agent-id> --prompt <prompt> --no-wait',
|
|
28
|
+
endpoint: endpoint(),
|
|
29
|
+
paseoAgentIds: ownership.map((item) => item.agentId),
|
|
30
|
+
ownerIds: [...new Set(ownership.map((item) => item.ownerId))],
|
|
31
|
+
passwordPresent: Boolean(process.env.PASEO_PASSWORD),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function native(binding) {
|
|
35
|
+
return ['claude-code', 'codex', 'opencode', 'pi'].includes(binding.channel);
|
|
36
|
+
}
|
|
37
|
+
function ownershipSnapshot(bindings) {
|
|
38
|
+
const out = new Map();
|
|
39
|
+
for (const binding of bindings) {
|
|
40
|
+
if (!binding.paseoAgentId)
|
|
41
|
+
continue;
|
|
42
|
+
const owner = bindings.filter((item) => item.ownerId === binding.ownerId);
|
|
43
|
+
out.set(`${binding.ownerId}\0${binding.paseoAgentId}`, {
|
|
44
|
+
agentId: binding.paseoAgentId,
|
|
45
|
+
ownerId: binding.ownerId,
|
|
46
|
+
sessionId: owner.find(native)?.sessionId ?? binding.sessionId,
|
|
47
|
+
nativeGuarantee: owner.some(native),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return [...out.values()];
|
|
51
|
+
}
|
|
52
|
+
function selectActiveAgents(ownership, agents) {
|
|
53
|
+
const ids = new Set(ownership.map((item) => item.agentId));
|
|
54
|
+
return agents.filter((agent) => ids.has(agent.id) && (agent.status === 'idle' || agent.status === 'running'));
|
|
55
|
+
}
|
|
56
|
+
function catchCommand(squarePath, recipient) {
|
|
57
|
+
return `square --as ${quoteShell(recipient)} --square-path ${quoteShell(squarePath)} catch --now`;
|
|
58
|
+
}
|
|
59
|
+
function prompt(notification, squarePath, nativeWake) {
|
|
60
|
+
const display = squarePath.startsWith(homedir()) ? `~${squarePath.slice(homedir().length)}` : squarePath;
|
|
61
|
+
const body = notification.item.body.length > 200 ? `${notification.item.body.slice(0, 197)}...` : notification.item.body;
|
|
62
|
+
return [
|
|
63
|
+
'<system-reminder source="square">',
|
|
64
|
+
`${notification.route === 'bell' ? 'Bell' : notification.route === 'beside' ? 'Beside' : 'Mention'} from @${notification.item.actor} in \`${display}\``,
|
|
65
|
+
nativeWake ? 'The native adapter will present it at the next boundary.' : `> ${body.replace(/\n/g, '\n> ')}`,
|
|
66
|
+
`\`${catchCommand(squarePath, notification.recipient)}\``,
|
|
67
|
+
'</system-reminder>',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
async function waitForCatch(squarePath, recipient, actIndex, ownerId) {
|
|
71
|
+
const deadline = Date.now() + 180_000;
|
|
72
|
+
while (Date.now() < deadline) {
|
|
73
|
+
const doc = loadSquare(squarePath);
|
|
74
|
+
if (isDeliveryDelivered(doc, recipient, actIndex))
|
|
75
|
+
return true;
|
|
76
|
+
const binding = lookupParticipant(squarePath, recipient).find((item) => item.ownerId === ownerId);
|
|
77
|
+
const lease = binding && sessionInbox(binding.sessionId).find((item) => item.name === recipient)?.catchLease;
|
|
78
|
+
if (!lease || lease.expiresAt <= Date.now())
|
|
79
|
+
return false;
|
|
80
|
+
await sleep(Math.min(250, lease.expiresAt - Date.now()));
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
function send(request, ownership) {
|
|
85
|
+
try {
|
|
86
|
+
sendPaseoWake(request);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new PaseoWakeError(error instanceof Error ? error.message : String(error), diagnostic('send', ownership, 'failed'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function dispatchPaseoNotification(notification, ctx) {
|
|
93
|
+
const initial = loadSquare(ctx.squarePath);
|
|
94
|
+
const recipient = resolveRosterName(initial, notification.recipient);
|
|
95
|
+
if (!recipient || !isCurrentlyJoined(initial.acts, recipient))
|
|
96
|
+
return;
|
|
97
|
+
const ownership = ownershipSnapshot(lookupParticipant(ctx.squarePath, recipient));
|
|
98
|
+
if (ownership.length === 0)
|
|
99
|
+
return;
|
|
100
|
+
const discovery = discoverPaseoAgents();
|
|
101
|
+
if (discovery.error && discovery.agents.length === 0) {
|
|
102
|
+
throw new PaseoWakeError(`Paseo unavailable: ${discovery.error}`, diagnostic('discovery', ownership, 'unavailable'));
|
|
103
|
+
}
|
|
104
|
+
const active = selectActiveAgents(ownership, discovery.agents);
|
|
105
|
+
if (active.length === 0) {
|
|
106
|
+
throw new PaseoWakeError('No registered Paseo agent is idle or running.', diagnostic('selection', ownership, 'not_active'));
|
|
107
|
+
}
|
|
108
|
+
let boundaryTimedOut = false;
|
|
109
|
+
for (const agent of active) {
|
|
110
|
+
const owner = ownership.find((item) => item.agentId === agent.id);
|
|
111
|
+
if (!owner || hasPresentedForOwner(owner.ownerId, ctx.squarePath, recipient, notification.item.index))
|
|
112
|
+
continue;
|
|
113
|
+
if (!(await waitForPaseoWakeBoundary(agent))) {
|
|
114
|
+
boundaryTimedOut = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const latest = loadSquare(ctx.squarePath);
|
|
118
|
+
if (!isCurrentlyJoined(latest.acts, recipient) || isDeliveryDelivered(latest, recipient, notification.item.index))
|
|
119
|
+
return;
|
|
120
|
+
const current = lookupParticipant(ctx.squarePath, recipient).find((item) => item.ownerId === owner.ownerId && item.paseoAgentId === owner.agentId);
|
|
121
|
+
if (!current)
|
|
122
|
+
continue;
|
|
123
|
+
const activeCatch = sessionInbox(current.sessionId).find((item) => item.name === recipient)?.catchLease;
|
|
124
|
+
if (activeCatch &&
|
|
125
|
+
leaseOwnsNotification(activeCatch, {
|
|
126
|
+
actor: notification.item.actor,
|
|
127
|
+
body: notification.item.body,
|
|
128
|
+
route: notification.route,
|
|
129
|
+
recipient,
|
|
130
|
+
}) &&
|
|
131
|
+
(await waitForCatch(ctx.squarePath, recipient, notification.item.index, owner.ownerId))) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const nativeWake = owner.nativeGuarantee;
|
|
135
|
+
const request = {
|
|
136
|
+
agentId: agent.id,
|
|
137
|
+
prompt: prompt({ ...notification, recipient }, ctx.squarePath, nativeWake),
|
|
138
|
+
};
|
|
139
|
+
if (nativeWake) {
|
|
140
|
+
send(request, ownership);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
presentOnce(current.sessionId, (id) => sessionInbox(id)
|
|
144
|
+
.map((item) => ({ ...item, notifications: item.notifications.filter((note) => note.actIndex === notification.item.index) }))
|
|
145
|
+
.filter((item) => item.notifications.length > 0), () => {
|
|
146
|
+
send(request, ownership);
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (boundaryTimedOut) {
|
|
152
|
+
throw new PaseoWakeError('Paseo did not reach the current tool boundary before the wake timeout.', diagnostic('boundary', ownership, 'timeout'));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export function paseoWakeSink() {
|
|
156
|
+
return { name: 'paseo', dispatch: dispatchPaseoNotification };
|
|
157
|
+
}
|
|
158
|
+
export function defaultWakeSinks() {
|
|
159
|
+
return process.env.SQUARE_DISABLE_PASEO_WAKE === '1' ? [] : [paseoWakeSink()];
|
|
160
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { waitForPaseoToolBoundary } from './paseo-timeline.js';
|
|
3
|
+
export function discoverPaseoAgents(timeoutMs = 5000) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = execFileSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['ls', '--json'], {
|
|
6
|
+
encoding: 'utf8',
|
|
7
|
+
timeout: timeoutMs,
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
});
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
const agents = Array.isArray(parsed) ? parsed : parsed?.agents;
|
|
12
|
+
if (!Array.isArray(agents))
|
|
13
|
+
return { agents: [], error: 'Paseo returned malformed agent inventory.' };
|
|
14
|
+
return {
|
|
15
|
+
agents: agents.filter((item) => item !== null &&
|
|
16
|
+
typeof item === 'object' &&
|
|
17
|
+
typeof item.id === 'string' &&
|
|
18
|
+
typeof item.status === 'string'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return { agents: [], error: error instanceof Error ? error.message : String(error) };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function waitForPaseoWakeBoundary(agent) {
|
|
26
|
+
if (agent.status === 'idle')
|
|
27
|
+
return true;
|
|
28
|
+
if (agent.status !== 'running')
|
|
29
|
+
return false;
|
|
30
|
+
return waitForPaseoToolBoundary(agent.id);
|
|
31
|
+
}
|
package/dist/paseo-timeline.js
CHANGED
|
@@ -1,206 +1,76 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
function paseoWebSocketUrl() {
|
|
6
|
-
const override = process.env['SQUARE_PASEO_WS_URL']?.trim();
|
|
7
|
-
if (override)
|
|
8
|
-
return override;
|
|
9
|
-
const listen = process.env['PASEO_LISTEN']?.trim();
|
|
10
|
-
if (!listen)
|
|
11
|
-
return 'ws://127.0.0.1:6767/ws';
|
|
12
|
-
if (/^wss?:\/\//i.test(listen)) {
|
|
13
|
-
const url = new URL(listen);
|
|
14
|
-
if (url.pathname === '/' || url.pathname === '')
|
|
15
|
-
url.pathname = '/ws';
|
|
16
|
-
return url.toString();
|
|
17
|
-
}
|
|
18
|
-
if (/^tcp:\/\//i.test(listen)) {
|
|
19
|
-
const url = new URL(listen);
|
|
20
|
-
const secure = url.searchParams.get('ssl') === 'true';
|
|
21
|
-
url.protocol = secure ? 'wss:' : 'ws:';
|
|
22
|
-
url.pathname = '/ws';
|
|
23
|
-
return url.toString();
|
|
24
|
-
}
|
|
25
|
-
if (/^\d+$/.test(listen))
|
|
26
|
-
return `ws://127.0.0.1:${listen}/ws`;
|
|
27
|
-
return `ws://${listen.replace(/\/$/, '')}/ws`;
|
|
28
|
-
}
|
|
29
|
-
function parseSnapshot(payload) {
|
|
30
|
-
if (payload === null || typeof payload !== 'object') {
|
|
31
|
-
throw new Error('Invalid Paseo timeline response.');
|
|
32
|
-
}
|
|
33
|
-
const response = payload;
|
|
34
|
-
if (typeof response.error === 'string' && response.error) {
|
|
35
|
-
throw new Error(response.error);
|
|
36
|
-
}
|
|
37
|
-
const latestTools = new Map();
|
|
38
|
-
for (const entry of response.entries ?? []) {
|
|
39
|
-
const item = entry.item;
|
|
40
|
-
if (item?.['type'] !== 'tool_call' ||
|
|
41
|
-
typeof item['callId'] !== 'string' ||
|
|
42
|
-
(item['status'] !== 'running' && item['status'] !== 'completed' && item['status'] !== 'failed')) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
latestTools.set(item['callId'], item['status']);
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
agentStatus: typeof response.agent?.status === 'string' ? response.agent.status : 'unknown',
|
|
49
|
-
toolCalls: Array.from(latestTools, ([callId, status]) => ({ callId, status })),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
class PaseoTimelineProbe {
|
|
53
|
-
socket;
|
|
54
|
-
pending = new Map();
|
|
55
|
-
constructor(socket) {
|
|
56
|
-
this.socket = socket;
|
|
57
|
-
socket.addEventListener('message', (event) => {
|
|
58
|
-
let envelope;
|
|
59
|
-
try {
|
|
60
|
-
envelope = JSON.parse(String(event.data));
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (envelope === null || typeof envelope !== 'object')
|
|
66
|
-
return;
|
|
67
|
-
const outer = envelope;
|
|
68
|
-
if (outer.type !== 'session' || outer.message === null || typeof outer.message !== 'object') {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
const message = outer.message;
|
|
72
|
-
if (message.type !== 'fetch_agent_timeline_response' ||
|
|
73
|
-
message.payload === null ||
|
|
74
|
-
typeof message.payload !== 'object') {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const requestId = message.payload.requestId;
|
|
78
|
-
if (typeof requestId !== 'string')
|
|
79
|
-
return;
|
|
80
|
-
const request = this.pending.get(requestId);
|
|
81
|
-
if (!request)
|
|
82
|
-
return;
|
|
83
|
-
clearTimeout(request.timer);
|
|
84
|
-
this.pending.delete(requestId);
|
|
85
|
-
try {
|
|
86
|
-
request.resolve(parseSnapshot(message.payload));
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
request.reject(error instanceof Error ? error : new Error(String(error)));
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
const rejectPending = () => {
|
|
93
|
-
for (const [requestId, request] of this.pending) {
|
|
94
|
-
clearTimeout(request.timer);
|
|
95
|
-
request.reject(new Error('Paseo timeline connection closed.'));
|
|
96
|
-
this.pending.delete(requestId);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
socket.addEventListener('close', rejectPending);
|
|
100
|
-
socket.addEventListener('error', rejectPending);
|
|
101
|
-
}
|
|
102
|
-
static async connect(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
103
|
-
const socket = new WebSocket(paseoWebSocketUrl());
|
|
104
|
-
await new Promise((resolvePromise, reject) => {
|
|
105
|
-
const timer = setTimeout(() => {
|
|
106
|
-
socket.close();
|
|
107
|
-
reject(new Error('Timed out connecting to Paseo.'));
|
|
108
|
-
}, timeoutMs);
|
|
109
|
-
socket.addEventListener('open', () => {
|
|
110
|
-
clearTimeout(timer);
|
|
111
|
-
resolvePromise();
|
|
112
|
-
}, { once: true });
|
|
113
|
-
socket.addEventListener('error', () => {
|
|
114
|
-
clearTimeout(timer);
|
|
115
|
-
reject(new Error('Could not connect to Paseo.'));
|
|
116
|
-
}, { once: true });
|
|
117
|
-
});
|
|
118
|
-
const probe = new PaseoTimelineProbe(socket);
|
|
119
|
-
socket.send(JSON.stringify({
|
|
120
|
-
type: 'hello',
|
|
121
|
-
clientId: `square-wake-${process.pid}-${randomUUID()}`,
|
|
122
|
-
clientType: 'cli',
|
|
123
|
-
protocolVersion: 1,
|
|
124
|
-
}));
|
|
125
|
-
return probe;
|
|
126
|
-
}
|
|
127
|
-
snapshot(agentId, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
128
|
-
const requestId = randomUUID();
|
|
129
|
-
return new Promise((resolvePromise, reject) => {
|
|
130
|
-
const timer = setTimeout(() => {
|
|
131
|
-
this.pending.delete(requestId);
|
|
132
|
-
reject(new Error('Timed out reading Paseo timeline.'));
|
|
133
|
-
}, timeoutMs);
|
|
134
|
-
this.pending.set(requestId, { resolve: resolvePromise, reject, timer });
|
|
135
|
-
this.socket.send(JSON.stringify({
|
|
136
|
-
type: 'session',
|
|
137
|
-
message: {
|
|
138
|
-
type: 'fetch_agent_timeline_request',
|
|
139
|
-
agentId,
|
|
140
|
-
requestId,
|
|
141
|
-
direction: 'tail',
|
|
142
|
-
limit: 200,
|
|
143
|
-
projection: 'projected',
|
|
144
|
-
},
|
|
145
|
-
}));
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
close() {
|
|
149
|
-
this.socket.close();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
async function waitWithSnapshots(agentId, readSnapshot, options) {
|
|
153
|
-
const initial = await readSnapshot(agentId);
|
|
2
|
+
async function waitSnapshots(agentId, read, opts) {
|
|
3
|
+
const initial = await read(agentId);
|
|
154
4
|
if (initial.agentStatus === 'idle')
|
|
155
5
|
return true;
|
|
156
6
|
if (initial.agentStatus !== 'running')
|
|
157
7
|
return false;
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
8
|
+
const running = new Set(initial.toolCalls.filter((tool) => tool.status === 'running').map((tool) => tool.callId));
|
|
9
|
+
if (running.size === 0)
|
|
160
10
|
return true;
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const deadline =
|
|
164
|
-
while (
|
|
165
|
-
await delay(
|
|
166
|
-
const
|
|
167
|
-
if (
|
|
11
|
+
const delay = opts.delay ?? ((ms) => sleep(ms));
|
|
12
|
+
const interval = opts.pollIntervalMs ?? 100;
|
|
13
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 30_000);
|
|
14
|
+
while (Date.now() < deadline) {
|
|
15
|
+
await delay(interval);
|
|
16
|
+
const current = await read(agentId);
|
|
17
|
+
if (current.agentStatus === 'idle')
|
|
168
18
|
return true;
|
|
169
|
-
if (
|
|
19
|
+
if (current.agentStatus !== 'running')
|
|
170
20
|
return false;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
const status = latest.get(callId);
|
|
174
|
-
return status === 'completed' || status === 'failed';
|
|
175
|
-
});
|
|
176
|
-
if (allTerminal)
|
|
21
|
+
const states = new Map(current.toolCalls.map((tool) => [tool.callId, tool.status]));
|
|
22
|
+
if ([...running].every((id) => states.get(id) === 'completed' || states.get(id) === 'failed'))
|
|
177
23
|
return true;
|
|
178
24
|
}
|
|
179
25
|
return false;
|
|
180
26
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
27
|
+
function paseoUrl() {
|
|
28
|
+
const value = process.env.SQUARE_PASEO_WS_URL?.trim() || process.env.PASEO_LISTEN?.trim();
|
|
29
|
+
if (!value)
|
|
30
|
+
return 'ws://127.0.0.1:6767/ws';
|
|
31
|
+
if (/^wss?:\/\//i.test(value))
|
|
32
|
+
return value.replace(/\/$/, '') + (value.endsWith('/ws') ? '' : '/ws');
|
|
33
|
+
if (/^\d+$/.test(value))
|
|
34
|
+
return `ws://127.0.0.1:${value}/ws`;
|
|
35
|
+
return `ws://${value.replace(/\/$/, '')}/ws`;
|
|
36
|
+
}
|
|
37
|
+
async function remoteSnapshot(agentId) {
|
|
38
|
+
const socket = new WebSocket(paseoUrl());
|
|
39
|
+
await new Promise((resolve, reject) => {
|
|
40
|
+
const timer = setTimeout(() => { socket.close(); reject(new Error('Paseo timeline connection timed out.')); }, 3000);
|
|
41
|
+
socket.addEventListener('open', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
42
|
+
socket.addEventListener('error', () => { clearTimeout(timer); reject(new Error('Paseo timeline unavailable.')); }, { once: true });
|
|
43
|
+
});
|
|
44
|
+
return await new Promise((resolve, reject) => {
|
|
45
|
+
const timer = setTimeout(() => { socket.close(); reject(new Error('Paseo timeline request timed out.')); }, 3000);
|
|
46
|
+
const requestId = `${process.pid}-${Date.now()}`;
|
|
47
|
+
socket.addEventListener('message', (event) => {
|
|
48
|
+
try {
|
|
49
|
+
const outer = JSON.parse(String(event.data));
|
|
50
|
+
const payload = outer?.message?.payload;
|
|
51
|
+
if (outer?.message?.type !== 'fetch_agent_timeline_response' || payload?.requestId !== requestId)
|
|
52
|
+
return;
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
socket.close();
|
|
55
|
+
const tools = new Map();
|
|
56
|
+
for (const entry of payload.entries ?? []) {
|
|
57
|
+
const item = entry?.item;
|
|
58
|
+
if (item?.type === 'tool_call' && typeof item.callId === 'string' && ['running', 'completed', 'failed'].includes(item.status))
|
|
59
|
+
tools.set(item.callId, item.status);
|
|
60
|
+
}
|
|
61
|
+
resolve({ agentStatus: typeof payload.agent?.status === 'string' ? payload.agent.status : 'unknown', toolCalls: [...tools].map(([callId, status]) => ({ callId, status })) });
|
|
62
|
+
}
|
|
63
|
+
catch { /* ignore unrelated frames */ }
|
|
64
|
+
});
|
|
65
|
+
socket.send(JSON.stringify({ type: 'hello', clientId: `square-${process.pid}`, clientType: 'cli', protocolVersion: 1 }));
|
|
66
|
+
socket.send(JSON.stringify({ type: 'session', message: { type: 'fetch_agent_timeline_request', agentId, requestId, direction: 'tail', limit: 200, projection: 'projected' } }));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export async function waitForPaseoToolBoundary(agentId, opts = {}) {
|
|
196
70
|
try {
|
|
197
|
-
|
|
198
|
-
return await waitWithSnapshots(agentId, (id) => probe.snapshot(id), options);
|
|
71
|
+
return await waitSnapshots(agentId, opts.readSnapshot ?? remoteSnapshot, opts);
|
|
199
72
|
}
|
|
200
73
|
catch {
|
|
201
74
|
return false;
|
|
202
75
|
}
|
|
203
|
-
finally {
|
|
204
|
-
probe?.close();
|
|
205
|
-
}
|
|
206
76
|
}
|