@astrosheep/square 0.3.9 → 0.3.11
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 +1 -1
- package/dist/activity.js +4 -0
- package/dist/artifact.js +33 -2
- package/dist/cli/context.js +1 -1
- package/dist/cli/maintenance-commands.js +12 -1
- package/dist/cli/observation-commands.js +3 -16
- package/dist/cli/program.js +0 -4
- package/dist/cli/square-commands.js +23 -3
- package/dist/cmd/notify-once.js +5 -15
- package/dist/decisions.js +10 -2
- package/dist/delivery-health.js +55 -136
- package/dist/doctor.js +1 -0
- package/dist/file-lock.js +112 -0
- package/dist/harness-codex.js +35 -29
- package/dist/harness-links.js +0 -3
- package/dist/harness-pi.js +57 -0
- package/dist/harness.js +10 -15
- package/dist/help.js +8 -8
- package/dist/index.js +5 -1
- package/dist/model.js +4 -0
- package/dist/notifications.js +205 -28
- package/dist/paseo-connection.js +135 -0
- package/dist/paseo-delivery.js +73 -144
- package/dist/paseo-state.js +1 -1
- package/dist/paseo-timeline.js +32 -42
- package/dist/presentation.js +2 -2
- package/dist/presented.js +10 -72
- package/dist/registry.js +23 -24
- package/dist/routes.js +153 -0
- package/dist/square-application.js +47 -49
- package/dist/stream.js +1 -1
- package/dist/wake-attempts.js +171 -0
- package/dist/wake-evidence.js +35 -0
- package/dist/wake-port.js +22 -0
- package/dist/wake-sink.js +45 -6
- package/dist/watch.js +1 -2
- package/guides/participant.md +1 -1
- package/package.json +6 -1
- package/skills/brainstorm/SKILL.md +24 -24
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +4 -3
- package/skills/square-feedback/SKILL.md +2 -2
- package/dist/notification-failures.js +0 -54
package/dist/notifications.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
2
4
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { loadSquare } from './artifact.js';
|
|
5
|
-
import { isDeliveryDelivered, isPendingNotification, planActNotifications, } from './delivery.js';
|
|
6
|
-
import {
|
|
7
|
+
import { deriveDeliveryModel, isDeliveryDelivered, isPendingNotification, leaseOwnsNotification, planActNotifications, } from './delivery.js';
|
|
8
|
+
import { sessionInbox } from './inbox.js';
|
|
7
9
|
import { hasPresentedAttention } from './presented.js';
|
|
8
|
-
import { SquareError } from './model.js';
|
|
10
|
+
import { nameKey, SquareError } from './model.js';
|
|
9
11
|
import { SLEEP_MS, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
|
|
10
|
-
import {
|
|
12
|
+
import { PaseoAdapter } from './paseo-delivery.js';
|
|
13
|
+
import { quoteShell } from './presentation.js';
|
|
14
|
+
import { lookupParticipant } from './registry.js';
|
|
15
|
+
import { isCurrentlyJoined } from './runtime.js';
|
|
16
|
+
import { execute } from './square-application.js';
|
|
17
|
+
import { nextWakeAttemptNumber, recordRecoveredUnknown, recordWakeAttempt, } from './wake-attempts.js';
|
|
18
|
+
import { joinedRecipients, wakeEvidence, wakeIsEligible } from './wake-evidence.js';
|
|
19
|
+
import { WakePort } from './wake-port.js';
|
|
20
|
+
const NOTIFY_LEASE_MS = 5 * 60 * 1000;
|
|
11
21
|
export { planActNotifications, matchesMentionTarget };
|
|
12
22
|
function known(doc, name) {
|
|
13
23
|
const value = resolveRosterName(doc, name);
|
|
@@ -16,12 +26,55 @@ function known(doc, name) {
|
|
|
16
26
|
return value;
|
|
17
27
|
}
|
|
18
28
|
export { notificationMessageId } from './delivery.js';
|
|
19
|
-
export function
|
|
20
|
-
const value = Number.parseInt(
|
|
21
|
-
if (!Number.isFinite(value) || value <= 0)
|
|
29
|
+
export function wakeGraceMs(env = process.env) {
|
|
30
|
+
const value = Number.parseInt(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? '5000', 10);
|
|
31
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
22
32
|
throw new SquareError('invalid_args', 'Invalid SQUARE_NOTIFY_DELIVERY_WAIT_MS: expected a positive integer.');
|
|
33
|
+
}
|
|
23
34
|
return value;
|
|
24
35
|
}
|
|
36
|
+
function catchCommand(squarePath, recipient) {
|
|
37
|
+
return `square --as ${quoteShell(recipient)} --location ${quoteShell(squarePath)} catch --now`;
|
|
38
|
+
}
|
|
39
|
+
function renderWakePayload(request) {
|
|
40
|
+
const display = request.squarePath.startsWith(homedir())
|
|
41
|
+
? `~${request.squarePath.slice(homedir().length)}`
|
|
42
|
+
: request.squarePath;
|
|
43
|
+
return [
|
|
44
|
+
'<system-reminder source="square">',
|
|
45
|
+
`${request.route === 'bell' ? 'Bell' : request.route === 'beside' ? 'Beside' : 'Mention'} from @${request.actor} in \`${display}\``,
|
|
46
|
+
'The native adapter will present it at the next boundary. If no native wake is available, pull from the square yourself.',
|
|
47
|
+
`\`${catchCommand(request.squarePath, request.recipient)}\``,
|
|
48
|
+
'</system-reminder>',
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
async function waitForCatch(route, request, body) {
|
|
52
|
+
const binding = lookupParticipant(request.squarePath, request.recipient)
|
|
53
|
+
.find((item) => item.ownerId === route.ownerId);
|
|
54
|
+
const activeCatch = binding && sessionInbox(binding.sessionId)
|
|
55
|
+
.find((item) => item.name === request.recipient)?.catchLease;
|
|
56
|
+
if (!activeCatch || !leaseOwnsNotification(activeCatch, {
|
|
57
|
+
actor: request.actor,
|
|
58
|
+
body,
|
|
59
|
+
route: request.route,
|
|
60
|
+
recipient: request.recipient,
|
|
61
|
+
}))
|
|
62
|
+
return false;
|
|
63
|
+
const deadline = Date.now() + 180_000;
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
const doc = loadSquare(request.squarePath);
|
|
66
|
+
if (isDeliveryDelivered(doc, request.recipient, request.actIndex))
|
|
67
|
+
return true;
|
|
68
|
+
const currentBinding = lookupParticipant(request.squarePath, request.recipient)
|
|
69
|
+
.find((item) => item.ownerId === route.ownerId);
|
|
70
|
+
const lease = currentBinding && sessionInbox(currentBinding.sessionId)
|
|
71
|
+
.find((item) => item.name === request.recipient)?.catchLease;
|
|
72
|
+
if (!lease || lease.expiresAt <= Date.now())
|
|
73
|
+
return false;
|
|
74
|
+
await sleep(Math.min(250, lease.expiresAt - Date.now()));
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
25
78
|
export function hasDeliveredNotification(squarePath, name, ref) {
|
|
26
79
|
const doc = loadSquare(squarePath);
|
|
27
80
|
return isDeliveryDelivered(doc, known(doc, name), typeof ref === 'number' ? ref : Number(ref.slice(4)));
|
|
@@ -41,31 +94,128 @@ export async function waitForDeliveredNotification(squarePath, name, ref, opts =
|
|
|
41
94
|
}
|
|
42
95
|
return false;
|
|
43
96
|
}
|
|
97
|
+
function notifyLeaseKey(recipient, actIndex) {
|
|
98
|
+
return JSON.stringify([`act_${actIndex}`, nameKey(recipient)]);
|
|
99
|
+
}
|
|
100
|
+
async function claimNotifyLease(squarePath, recipient, actIndex) {
|
|
101
|
+
const at = Date.now();
|
|
102
|
+
const committed = await execute(squarePath, {
|
|
103
|
+
type: 'claim-notify',
|
|
104
|
+
key: notifyLeaseKey(recipient, actIndex),
|
|
105
|
+
leaseId: randomUUID(),
|
|
106
|
+
at,
|
|
107
|
+
expiresAt: at + NOTIFY_LEASE_MS,
|
|
108
|
+
});
|
|
109
|
+
return committed.result;
|
|
110
|
+
}
|
|
111
|
+
async function transitionNotifyLease(squarePath, recipient, actIndex, leaseId, phase, routeKind, attemptN) {
|
|
112
|
+
const at = Date.now();
|
|
113
|
+
const committed = await execute(squarePath, {
|
|
114
|
+
type: 'transition-notify',
|
|
115
|
+
key: notifyLeaseKey(recipient, actIndex),
|
|
116
|
+
leaseId,
|
|
117
|
+
expiresAt: at + NOTIFY_LEASE_MS,
|
|
118
|
+
phase,
|
|
119
|
+
...(routeKind === undefined ? {} : { routeKind }),
|
|
120
|
+
...(attemptN === undefined ? {} : { attemptN }),
|
|
121
|
+
});
|
|
122
|
+
return committed.result.updated;
|
|
123
|
+
}
|
|
124
|
+
function releaseNotifyLease(squarePath, recipient, actIndex, leaseId) {
|
|
125
|
+
return execute(squarePath, {
|
|
126
|
+
type: 'release-notify',
|
|
127
|
+
key: notifyLeaseKey(recipient, actIndex),
|
|
128
|
+
leaseId,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async function processNotification(squarePath, notification, opts) {
|
|
132
|
+
const env = opts.env ?? process.env;
|
|
133
|
+
const now = opts.now ?? Date.now;
|
|
134
|
+
const attention = {
|
|
135
|
+
squarePath,
|
|
136
|
+
actIndex: notification.item.index,
|
|
137
|
+
recipient: notification.recipient,
|
|
138
|
+
};
|
|
139
|
+
const initialAt = now();
|
|
140
|
+
if (!wakeIsEligible(wakeEvidence(squarePath, notification.recipient, notification.item.index, initialAt, env)))
|
|
141
|
+
return;
|
|
142
|
+
const claim = await claimNotifyLease(squarePath, notification.recipient, notification.item.index);
|
|
143
|
+
if (claim.type === 'busy')
|
|
144
|
+
return;
|
|
145
|
+
if (claim.type === 'ambiguous') {
|
|
146
|
+
const recovered = recordRecoveredUnknown(attention, claim.lease, env);
|
|
147
|
+
if (recovered !== undefined) {
|
|
148
|
+
await releaseNotifyLease(squarePath, notification.recipient, notification.item.index, claim.lease.leaseId);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const { leaseId } = claim;
|
|
153
|
+
let releaseLease = true;
|
|
154
|
+
try {
|
|
155
|
+
const dispatchAt = now();
|
|
156
|
+
const evidence = wakeEvidence(squarePath, notification.recipient, notification.item.index, dispatchAt, env);
|
|
157
|
+
if (!wakeIsEligible(evidence))
|
|
158
|
+
return;
|
|
159
|
+
const port = new WakePort(opts.adapters ?? [new PaseoAdapter()]);
|
|
160
|
+
const request = {
|
|
161
|
+
squarePath,
|
|
162
|
+
actIndex: notification.item.index,
|
|
163
|
+
recipient: notification.recipient,
|
|
164
|
+
actor: notification.item.actor,
|
|
165
|
+
route: notification.route,
|
|
166
|
+
};
|
|
167
|
+
await port.dispatch(evidence.attemptableRoutes, renderWakePayload(request), {
|
|
168
|
+
nextAttemptN: () => nextWakeAttemptNumber(attention, { env, now: now() }),
|
|
169
|
+
beforeSend: async (route, attemptN) => {
|
|
170
|
+
if (await waitForCatch(route, request, notification.item.body))
|
|
171
|
+
return false;
|
|
172
|
+
const currentAt = now();
|
|
173
|
+
const latest = loadSquare(squarePath);
|
|
174
|
+
if (!isCurrentlyJoined(latest.acts, notification.recipient))
|
|
175
|
+
return false;
|
|
176
|
+
const current = wakeEvidence(squarePath, notification.recipient, notification.item.index, currentAt, env);
|
|
177
|
+
if (!wakeIsEligible(current))
|
|
178
|
+
return false;
|
|
179
|
+
if (!current.attemptableRoutes.some((candidate) => candidate.ownerId === route.ownerId && candidate.kind === route.kind && candidate.sessionId === route.sessionId))
|
|
180
|
+
return false;
|
|
181
|
+
const dispatching = await transitionNotifyLease(squarePath, notification.recipient, notification.item.index, leaseId, 'dispatching', route.kind, attemptN);
|
|
182
|
+
if (dispatching)
|
|
183
|
+
releaseLease = false;
|
|
184
|
+
return dispatching;
|
|
185
|
+
},
|
|
186
|
+
record: async (route, attemptN, outcome) => {
|
|
187
|
+
if (outcome.outcome === 'failed') {
|
|
188
|
+
await transitionNotifyLease(squarePath, notification.recipient, notification.item.index, leaseId, 'claimed');
|
|
189
|
+
releaseLease = true;
|
|
190
|
+
}
|
|
191
|
+
recordWakeAttempt({
|
|
192
|
+
attention,
|
|
193
|
+
routeKind: route.kind,
|
|
194
|
+
outcome: outcome.outcome,
|
|
195
|
+
attemptN,
|
|
196
|
+
at: now(),
|
|
197
|
+
...('signature' in outcome ? { signature: outcome.signature } : {}),
|
|
198
|
+
...('message' in outcome ? { message: outcome.message } : {}),
|
|
199
|
+
...('diagnostic' in outcome && outcome.diagnostic !== undefined ? { diagnostic: outcome.diagnostic } : {}),
|
|
200
|
+
}, env);
|
|
201
|
+
if (outcome.outcome !== 'failed')
|
|
202
|
+
releaseLease = true;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
if (releaseLease) {
|
|
208
|
+
await releaseNotifyLease(squarePath, notification.recipient, notification.item.index, leaseId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
44
212
|
export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
|
|
45
213
|
const doc = loadSquare(squarePath);
|
|
46
214
|
const item = doc.acts.find((candidate) => candidate.index === actIndex);
|
|
47
215
|
if (item === undefined)
|
|
48
216
|
return;
|
|
49
217
|
const notifications = planActNotifications(doc, item).filter(isPendingNotification);
|
|
50
|
-
|
|
51
|
-
if (hasAttentionNotification(squarePath, notification.recipient, notification.item.index))
|
|
52
|
-
continue;
|
|
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
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
218
|
+
await Promise.all(notifications.map((notification) => processNotification(squarePath, notification, opts)));
|
|
69
219
|
}
|
|
70
220
|
function launchWorker(workerPath, args) {
|
|
71
221
|
const child = spawn(process.execPath, [workerPath, ...args], { detached: true, stdio: 'ignore', env: process.env });
|
|
@@ -73,10 +223,37 @@ function launchWorker(workerPath, args) {
|
|
|
73
223
|
}
|
|
74
224
|
/** Start one detached worker only when this act contains directed attention. */
|
|
75
225
|
export async function dispatchActNotifications(squarePath, item, opts = {}) {
|
|
76
|
-
|
|
226
|
+
const env = opts.env ?? process.env;
|
|
227
|
+
if (env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
77
228
|
return;
|
|
78
229
|
const doc = loadSquare(squarePath);
|
|
79
230
|
if (!planActNotifications(doc, item).some(isPendingNotification))
|
|
80
231
|
return;
|
|
81
|
-
(opts.launchWorker ?? launchWorker)(fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url)), ['--
|
|
232
|
+
(opts.launchWorker ?? launchWorker)(fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url)), ['--location', squarePath, '--act-index', String(item.index)]);
|
|
233
|
+
}
|
|
234
|
+
/** Reconsider old pending attention at a bounded action boundary using the existing worker. */
|
|
235
|
+
export function sweepPendingNotifications(squarePath, opts = {}) {
|
|
236
|
+
const env = opts.env ?? process.env;
|
|
237
|
+
if (env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
238
|
+
return [];
|
|
239
|
+
const now = opts.now ?? Date.now();
|
|
240
|
+
const limit = opts.limit ?? 8;
|
|
241
|
+
const doc = loadSquare(squarePath);
|
|
242
|
+
const model = deriveDeliveryModel(doc);
|
|
243
|
+
const indexes = new Set();
|
|
244
|
+
for (const recipient of joinedRecipients(doc)) {
|
|
245
|
+
for (const note of model.pendingFor(recipient)) {
|
|
246
|
+
if (now - note.item.at <= wakeGraceMs(env))
|
|
247
|
+
continue;
|
|
248
|
+
if (!wakeIsEligible(wakeEvidence(squarePath, recipient, note.item.index, now, env)))
|
|
249
|
+
continue;
|
|
250
|
+
indexes.add(note.item.index);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const selected = [...indexes].sort((a, b) => a - b).slice(0, Math.max(0, limit));
|
|
254
|
+
const workerPath = fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url));
|
|
255
|
+
for (const actIndex of selected) {
|
|
256
|
+
(opts.launchWorker ?? launchWorker)(workerPath, ['--location', squarePath, '--act-index', String(actIndex)]);
|
|
257
|
+
}
|
|
258
|
+
return selected;
|
|
82
259
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DaemonClient } from '@getpaseo/client/internal/daemon-client';
|
|
5
|
+
import WebSocket from 'ws';
|
|
6
|
+
const DEFAULT_HOST = 'localhost:6767';
|
|
7
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 3_000;
|
|
8
|
+
function paseoHome(env) {
|
|
9
|
+
return env.PASEO_HOME?.trim() || path.join(homedir(), '.paseo');
|
|
10
|
+
}
|
|
11
|
+
function expandHome(value) {
|
|
12
|
+
return value === '~' ? homedir() : value.startsWith('~/') ? path.join(homedir(), value.slice(2)) : value;
|
|
13
|
+
}
|
|
14
|
+
function normalizeHost(raw) {
|
|
15
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
16
|
+
return undefined;
|
|
17
|
+
const value = raw.trim();
|
|
18
|
+
if (value.startsWith('unix://') || value.startsWith('pipe://') || value.startsWith('tcp://'))
|
|
19
|
+
return value;
|
|
20
|
+
if (value.startsWith('\\\\.\\pipe\\'))
|
|
21
|
+
return `pipe://${value}`;
|
|
22
|
+
if (value.startsWith('/') || value.startsWith('~/'))
|
|
23
|
+
return `unix://${expandHome(value)}`;
|
|
24
|
+
if (/^\d+$/.test(value))
|
|
25
|
+
return `127.0.0.1:${value}`;
|
|
26
|
+
return value.includes(':') ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
function configuredHost(env) {
|
|
29
|
+
try {
|
|
30
|
+
const config = JSON.parse(fs.readFileSync(path.join(paseoHome(env), 'config.json'), 'utf8'));
|
|
31
|
+
return normalizeHost(config.daemon?.listen ?? config.listen);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function pidHost(env) {
|
|
38
|
+
try {
|
|
39
|
+
const pid = JSON.parse(fs.readFileSync(path.join(paseoHome(env), 'paseo.pid'), 'utf8'));
|
|
40
|
+
return normalizeHost(pid.listen ?? pid.sockPath);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isIpc(host) {
|
|
47
|
+
return host !== undefined && (host.startsWith('unix://') || host.startsWith('pipe://'));
|
|
48
|
+
}
|
|
49
|
+
/** Match the host precedence used by the Paseo CLI for local daemon connections. */
|
|
50
|
+
export function paseoDaemonHosts(env = process.env) {
|
|
51
|
+
const explicit = normalizeHost(env.PASEO_HOST);
|
|
52
|
+
if (explicit !== undefined)
|
|
53
|
+
return [explicit];
|
|
54
|
+
const candidates = [];
|
|
55
|
+
const listen = normalizeHost(env.PASEO_LISTEN);
|
|
56
|
+
const pid = pidHost(env);
|
|
57
|
+
const configured = configuredHost(env);
|
|
58
|
+
if (isIpc(listen))
|
|
59
|
+
candidates.push(listen);
|
|
60
|
+
if (isIpc(pid))
|
|
61
|
+
candidates.push(pid);
|
|
62
|
+
if (isIpc(configured))
|
|
63
|
+
candidates.push(configured);
|
|
64
|
+
if (configured !== undefined && !isIpc(configured) && configured !== '127.0.0.1:6767')
|
|
65
|
+
candidates.push(configured);
|
|
66
|
+
candidates.push(DEFAULT_HOST);
|
|
67
|
+
return [...new Set(candidates)];
|
|
68
|
+
}
|
|
69
|
+
function uriPassword(uri) {
|
|
70
|
+
const value = uri.searchParams.get('password');
|
|
71
|
+
return value === null || value === '' ? undefined : value;
|
|
72
|
+
}
|
|
73
|
+
export function resolvePaseoDaemonTarget(host, env = process.env) {
|
|
74
|
+
const passwordFromEnv = env.PASEO_PASSWORD?.trim() || undefined;
|
|
75
|
+
if (host.startsWith('unix://') || host.startsWith('pipe://')) {
|
|
76
|
+
const prefix = host.startsWith('unix://') ? 'unix://' : 'pipe://';
|
|
77
|
+
const socketPath = expandHome(host.slice(prefix.length).trim());
|
|
78
|
+
if (socketPath === '')
|
|
79
|
+
throw new Error('Invalid Paseo IPC target: missing socket path.');
|
|
80
|
+
return {
|
|
81
|
+
type: 'ipc',
|
|
82
|
+
url: host.startsWith('unix://') ? `ws+unix://${socketPath}:/ws` : 'ws://localhost/ws',
|
|
83
|
+
socketPath,
|
|
84
|
+
...(passwordFromEnv === undefined ? {} : { password: passwordFromEnv }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (host.startsWith('tcp://')) {
|
|
88
|
+
const uri = new URL(host);
|
|
89
|
+
const hostname = uri.hostname.replace(/^\[|\]$/g, '');
|
|
90
|
+
const endpoint = `${hostname.includes(':') ? `[${hostname}]` : hostname}:${uri.port || '6767'}`;
|
|
91
|
+
const secure = uri.searchParams.get('ssl') === 'true';
|
|
92
|
+
const password = uriPassword(uri) ?? passwordFromEnv;
|
|
93
|
+
return {
|
|
94
|
+
type: 'tcp',
|
|
95
|
+
url: `${secure ? 'wss' : 'ws'}://${endpoint}/ws`,
|
|
96
|
+
...(password === undefined ? {} : { password }),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
type: 'tcp',
|
|
101
|
+
url: `ws://${host.replace(/\/$/, '')}/ws`,
|
|
102
|
+
...(passwordFromEnv === undefined ? {} : { password: passwordFromEnv }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function webSocketFactory(target) {
|
|
106
|
+
return (url, options) => new WebSocket(url, options?.protocols, {
|
|
107
|
+
headers: options?.headers,
|
|
108
|
+
...(target.type === 'ipc' ? { socketPath: target.socketPath } : {}),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
export async function connectPaseoDaemon(env = process.env, connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS) {
|
|
112
|
+
let lastError;
|
|
113
|
+
for (const host of paseoDaemonHosts(env)) {
|
|
114
|
+
const target = resolvePaseoDaemonTarget(host, env);
|
|
115
|
+
const client = new DaemonClient({
|
|
116
|
+
url: target.url,
|
|
117
|
+
clientId: `square-${process.pid}-${Date.now()}`,
|
|
118
|
+
clientType: 'cli',
|
|
119
|
+
appVersion: 'square',
|
|
120
|
+
password: target.password,
|
|
121
|
+
connectTimeoutMs,
|
|
122
|
+
webSocketFactory: webSocketFactory(target),
|
|
123
|
+
reconnect: { enabled: false },
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
await client.connect();
|
|
127
|
+
return client;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
lastError = error;
|
|
131
|
+
await client.close().catch(() => { });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw lastError instanceof Error ? lastError : new Error('Unable to connect to the Paseo daemon.');
|
|
135
|
+
}
|
package/dist/paseo-delivery.js
CHANGED
|
@@ -1,160 +1,89 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
}
|
|
1
|
+
import { paseoDaemonHosts, resolvePaseoDaemonTarget } from './paseo-connection.js';
|
|
2
|
+
import { discoverPaseoAgents, waitForPaseoWakeBoundary } from './paseo-state.js';
|
|
3
|
+
import { PaseoWakeSendError, sendPaseoWake } from './wake-sink.js';
|
|
20
4
|
function endpoint() {
|
|
21
|
-
|
|
5
|
+
try {
|
|
6
|
+
return resolvePaseoDaemonTarget(paseoDaemonHosts()[0]).url;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return 'unresolved';
|
|
10
|
+
}
|
|
22
11
|
}
|
|
23
|
-
function diagnostic(phase,
|
|
12
|
+
function diagnostic(phase, address, code) {
|
|
24
13
|
return {
|
|
25
14
|
phase,
|
|
26
15
|
code,
|
|
27
|
-
command: phase === 'discovery' ? 'paseo ls --json' : 'paseo send <agent-id> --prompt <prompt> --no-wait',
|
|
16
|
+
command: phase === 'discovery' ? 'paseo ls --global --json' : 'paseo send <agent-id> --prompt <prompt> --no-wait --json',
|
|
28
17
|
endpoint: endpoint(),
|
|
29
|
-
paseoAgentIds:
|
|
30
|
-
ownerIds: [...new Set(ownership.map((item) => item.ownerId))],
|
|
18
|
+
paseoAgentIds: [address.agentId].filter(Boolean),
|
|
31
19
|
passwordPresent: Boolean(process.env.PASEO_PASSWORD),
|
|
32
20
|
};
|
|
33
21
|
}
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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`;
|
|
22
|
+
function discoveryRetryable(message) {
|
|
23
|
+
if (/password|auth|unauthori[sz]ed/i.test(message))
|
|
24
|
+
return false;
|
|
25
|
+
return /DAEMON_NOT_RUNNING|ECONNREFUSED|ENOENT|not found.*executable|ETIMEDOUT|timed out|timeout/i.test(message);
|
|
58
26
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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()));
|
|
27
|
+
export class PaseoAdapter {
|
|
28
|
+
opts;
|
|
29
|
+
kind = 'paseo';
|
|
30
|
+
constructor(opts = {}) {
|
|
31
|
+
this.opts = opts;
|
|
81
32
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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;
|
|
33
|
+
async dispatch(address, payload, beforeSend) {
|
|
34
|
+
const agentId = address.agentId?.trim();
|
|
35
|
+
if (!agentId) {
|
|
36
|
+
return {
|
|
37
|
+
outcome: 'failed',
|
|
38
|
+
signature: 'invalid_address',
|
|
39
|
+
message: 'Paseo route has no agent id.',
|
|
40
|
+
diagnostic: diagnostic('selection', address, 'invalid_address'),
|
|
41
|
+
};
|
|
116
42
|
}
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
return
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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;
|
|
43
|
+
const discovery = (this.opts.discover ?? discoverPaseoAgents)();
|
|
44
|
+
if (discovery.error && discovery.agents.length === 0) {
|
|
45
|
+
return {
|
|
46
|
+
outcome: 'failed',
|
|
47
|
+
signature: discoveryRetryable(discovery.error) ? 'discovery_transient' : 'discovery_rejected',
|
|
48
|
+
message: `Paseo unavailable: ${discovery.error}`,
|
|
49
|
+
diagnostic: diagnostic('discovery', address, 'unavailable'),
|
|
50
|
+
};
|
|
133
51
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
52
|
+
const agent = discovery.agents.find((candidate) => candidate.id === agentId);
|
|
53
|
+
if (agent === undefined || (agent.status !== 'idle' && agent.status !== 'running')) {
|
|
54
|
+
return {
|
|
55
|
+
outcome: 'failed',
|
|
56
|
+
signature: agent === undefined ? 'address_not_found' : 'agent_not_active',
|
|
57
|
+
message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle or running.',
|
|
58
|
+
diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : 'not_active'),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (!(await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent))) {
|
|
62
|
+
return {
|
|
63
|
+
outcome: 'failed',
|
|
64
|
+
signature: 'boundary_unavailable',
|
|
65
|
+
message: 'Paseo did not reach the current tool boundary before the wake timeout.',
|
|
66
|
+
diagnostic: diagnostic('boundary', address, 'unavailable'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (!(await beforeSend()))
|
|
70
|
+
return { outcome: 'cancelled' };
|
|
71
|
+
try {
|
|
72
|
+
(this.opts.sendWake ?? sendPaseoWake)({ agentId, prompt: payload });
|
|
73
|
+
return { outcome: 'accepted' };
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
const kind = error instanceof PaseoWakeSendError ? error.kind : 'unknown';
|
|
78
|
+
const details = { ...diagnostic('send', address, 'failed'), outcome: kind };
|
|
79
|
+
if (kind === 'unknown')
|
|
80
|
+
return { outcome: 'unknown', signature: 'send_unknown', message, diagnostic: details };
|
|
81
|
+
return {
|
|
82
|
+
outcome: 'failed',
|
|
83
|
+
signature: kind === 'transient' ? 'send_pre_accept_transient' : 'send_pre_accept_rejected',
|
|
84
|
+
message,
|
|
85
|
+
diagnostic: details,
|
|
86
|
+
};
|
|
142
87
|
}
|
|
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
88
|
}
|
|
154
89
|
}
|
|
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
|
-
}
|
package/dist/paseo-state.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { waitForPaseoToolBoundary } from './paseo-timeline.js';
|
|
3
3
|
export function discoverPaseoAgents(timeoutMs = 5000) {
|
|
4
4
|
try {
|
|
5
|
-
const raw = execFileSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['ls', '--json'], {
|
|
5
|
+
const raw = execFileSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['ls', '--global', '--json'], {
|
|
6
6
|
encoding: 'utf8',
|
|
7
7
|
timeout: timeoutMs,
|
|
8
8
|
stdio: ['ignore', 'pipe', 'pipe'],
|