@astrosheep/square 0.3.63 → 0.3.65
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/claude-plugin/.claude-plugin/plugin.json +3 -2
- package/claude-plugin/hooks/hooks.json +1 -22
- package/claude-plugin/hooks/register.js +119 -0
- package/claude-plugin/types.d.ts +7 -0
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity-feed.d.ts +1 -10
- package/dist/activity-feed.js +1 -27
- package/dist/activity.js +57 -25
- package/dist/artifact.d.ts +1 -1
- package/dist/artifact.js +24 -25
- package/dist/attention-presentation.js +1 -0
- package/dist/automatic-session.d.ts +3 -1
- package/dist/automatic-session.js +21 -34
- package/dist/boundary-presentation.js +4 -13
- package/dist/canonical-path.d.ts +11 -0
- package/dist/canonical-path.js +47 -0
- package/dist/catch-decisions.js +14 -10
- package/dist/catch-selection.d.ts +6 -0
- package/dist/catch-selection.js +14 -0
- package/dist/claude-delivery.d.ts +11 -0
- package/dist/claude-delivery.js +145 -0
- package/dist/claude-mod.d.ts +8 -0
- package/dist/claude-mod.js +164 -0
- package/dist/cli/context.d.ts +0 -1
- package/dist/cli/context.js +9 -21
- package/dist/cli/harness-command.d.ts +0 -1
- package/dist/cli/harness-command.js +0 -6
- package/dist/cli/observation-commands.d.ts +1 -1
- package/dist/cli/observation-commands.js +31 -58
- package/dist/cli/program.js +20 -18
- package/dist/cli/registry.js +2 -2
- package/dist/cli/square-commands.js +10 -6
- package/dist/codex-boundary-state.d.ts +2 -2
- package/dist/codex-boundary-state.js +4 -4
- package/dist/codex-hook.js +12 -15
- package/dist/codex-queue.d.ts +2 -2
- package/dist/codex-queue.js +7 -5
- package/dist/decisions.d.ts +2 -6
- package/dist/decisions.js +16 -17
- package/dist/delivery-health.d.ts +1 -2
- package/dist/delivery-health.js +4 -5
- package/dist/delivery-operations.d.ts +2 -5
- package/dist/delivery-operations.js +77 -214
- package/dist/delivery.d.ts +25 -47
- package/dist/delivery.js +14 -46
- package/dist/diagnostic-redaction.js +7 -9
- package/dist/file-changes.d.ts +0 -1
- package/dist/file-changes.js +2 -19
- package/dist/harness-claude.js +8 -6
- package/dist/harness-codex.d.ts +0 -2
- package/dist/harness-codex.js +0 -2
- package/dist/harness-links.js +13 -17
- package/dist/harness.js +3 -7
- package/dist/help.js +4 -3
- package/dist/host-ledger-file-adapter.d.ts +17 -8
- package/dist/host-ledger-file-adapter.js +164 -80
- package/dist/host-ledger.d.ts +55 -50
- package/dist/inbox.d.ts +0 -3
- package/dist/inbox.js +88 -50
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/list.js +1 -4
- package/dist/mcp.js +24 -41
- package/dist/mention-parse.js +3 -9
- package/dist/model.d.ts +11 -2
- package/dist/model.js +1 -1
- package/dist/notifications.d.ts +3 -16
- package/dist/notifications.js +41 -122
- package/dist/opencode-delivery.d.ts +14 -0
- package/dist/opencode-delivery.js +171 -0
- package/dist/opencode.d.ts +39 -18
- package/dist/opencode.js +145 -40
- package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
- package/dist/packages/agent-delivery/src/claude-auth.js +75 -0
- package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
- package/dist/packages/agent-delivery/src/claude-native.js +68 -0
- package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
- package/dist/packages/agent-delivery/src/claude.js +66 -0
- package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
- package/dist/packages/agent-delivery/src/deadline.js +41 -0
- package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
- package/dist/packages/agent-delivery/src/errors.js +41 -0
- package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
- package/dist/packages/agent-delivery/src/guards.js +6 -0
- package/dist/packages/agent-delivery/src/index.d.ts +83 -0
- package/dist/packages/agent-delivery/src/index.js +110 -0
- package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
- package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
- package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
- package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
- package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
- package/dist/packages/agent-delivery/src/opencode.js +127 -0
- package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
- package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
- package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
- package/dist/packages/agent-delivery/src/paseo.js +106 -0
- package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
- package/dist/packages/agent-delivery/src/pi-client.js +106 -0
- package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
- package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
- package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
- package/dist/packages/agent-delivery/src/pi-send.js +4 -0
- package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
- package/dist/packages/agent-delivery/src/pi.js +209 -0
- package/dist/packages/agent-delivery/src/types.d.ts +120 -0
- package/dist/participant-host.js +12 -13
- package/dist/participant-identity.d.ts +21 -0
- package/dist/participant-identity.js +32 -11
- package/dist/paseo-delivery.d.ts +2 -2
- package/dist/paseo-delivery.js +35 -27
- package/dist/paseo-timeline.js +25 -21
- package/dist/paseo.d.ts +2 -2
- package/dist/paseo.js +1 -2
- package/dist/ports.d.ts +7 -20
- package/dist/presentation-operations.js +16 -4
- package/dist/presentation.d.ts +19 -10
- package/dist/presentation.js +99 -177
- package/dist/registry.d.ts +15 -66
- package/dist/registry.js +45 -123
- package/dist/routes.d.ts +1 -28
- package/dist/routes.js +22 -104
- package/dist/runtime.d.ts +3 -9
- package/dist/runtime.js +14 -67
- package/dist/search.d.ts +0 -2
- package/dist/search.js +3 -10
- package/dist/square-actions.js +48 -72
- package/dist/square-application.js +32 -30
- package/dist/square-core.d.ts +3 -0
- package/dist/square-core.js +22 -5
- package/dist/square-facade.d.ts +2 -0
- package/dist/square-file-adapter.d.ts +2 -1
- package/dist/square-file-adapter.js +6 -9
- package/dist/square-projections.d.ts +40 -45
- package/dist/square-projections.js +60 -74
- package/dist/square-state.js +1 -4
- package/dist/square-storage.d.ts +5 -8
- package/dist/square-storage.js +13 -36
- package/dist/square-wiring.d.ts +2 -7
- package/dist/square-wiring.js +6 -24
- package/dist/stream.js +4 -6
- package/dist/time.js +2 -7
- package/dist/views.d.ts +6 -18
- package/dist/views.js +14 -21
- package/dist/wake-attempts.d.ts +2 -56
- package/dist/wake-attempts.js +15 -110
- package/dist/wake-eligibility.d.ts +43 -0
- package/dist/wake-eligibility.js +106 -0
- package/dist/wake-evidence.d.ts +32 -9
- package/dist/wake-evidence.js +29 -24
- package/dist/wake-sink.d.ts +13 -9
- package/dist/wake-sink.js +35 -54
- package/dist/watch.js +13 -21
- package/extensions/square-pi.js +27 -13
- package/guides/change-waits.md +1 -1
- package/guides/claude-native.md +101 -0
- package/guides/host-ledger.md +3 -1
- package/guides/participant.md +14 -7
- package/guides/pi-native-delivery.md +49 -0
- package/package.json +4 -3
- package/dist/claude-hook.d.ts +0 -10
- package/dist/claude-hook.js +0 -60
- package/dist/paseo-connection.d.ts +0 -15
- package/dist/paseo-connection.js +0 -141
- package/dist/presence.d.ts +0 -2
- package/dist/presence.js +0 -24
- package/dist/presented.d.ts +0 -2
- package/dist/presented.js +0 -10
- package/dist/state-cell.d.ts +0 -14
- package/dist/wake-port.d.ts +0 -23
- package/dist/wake-port.js +0 -27
- /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { chmod, link, lstat, unlink } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { sendPiMessage } from './pi-send.js';
|
|
6
|
+
import { CUSTOM_TYPE, FRAME_WAIT_MS, MAX_CONNECTIONS, MAX_TEXT_BYTES, VERSION, endpointValid, frame, identity, readFrame, record, waitValid } from './pi-protocol.js';
|
|
7
|
+
export { sendPiMessage } from './pi-send.js';
|
|
8
|
+
/**
|
|
9
|
+
* Resources start only on session_start. Missing endpoint leaves the extension inert.
|
|
10
|
+
* SDK hosts that dispose without session_shutdown must explicitly close this receiver.
|
|
11
|
+
*/
|
|
12
|
+
export function createPiReceiver(pi, options = {}) {
|
|
13
|
+
let live;
|
|
14
|
+
let lifecycle = Promise.resolve();
|
|
15
|
+
const serialize = (work) => {
|
|
16
|
+
const result = lifecycle.then(work);
|
|
17
|
+
lifecycle = result.catch(() => { });
|
|
18
|
+
return result;
|
|
19
|
+
};
|
|
20
|
+
const retire = () => {
|
|
21
|
+
const old = live;
|
|
22
|
+
live = undefined;
|
|
23
|
+
if (old !== undefined) {
|
|
24
|
+
old.retired = true;
|
|
25
|
+
for (const waiter of [...old.waiters.values()])
|
|
26
|
+
waiter.finish('unknown', 'session_replaced');
|
|
27
|
+
for (const socket of old.sockets)
|
|
28
|
+
socket.destroy();
|
|
29
|
+
}
|
|
30
|
+
return old;
|
|
31
|
+
};
|
|
32
|
+
const stop = async (old) => {
|
|
33
|
+
if (old === undefined)
|
|
34
|
+
return;
|
|
35
|
+
if (old.published !== undefined) {
|
|
36
|
+
const own = old.published;
|
|
37
|
+
old.published = undefined;
|
|
38
|
+
const current = await lstat(old.endpoint).catch(() => undefined);
|
|
39
|
+
if (current?.isSocket() && current.dev === own.dev && current.ino === own.ino)
|
|
40
|
+
await unlink(old.endpoint);
|
|
41
|
+
}
|
|
42
|
+
// Bind under an owned temporary name: Node blindly unlinks its bind path on close.
|
|
43
|
+
// Publishing a hard link lets us protect a replacement at the caller's explicit endpoint.
|
|
44
|
+
if (old.server?.listening)
|
|
45
|
+
await new Promise((resolve) => old.server.close(() => resolve()));
|
|
46
|
+
};
|
|
47
|
+
const close = () => {
|
|
48
|
+
const old = retire();
|
|
49
|
+
return serialize(() => stop(old));
|
|
50
|
+
};
|
|
51
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
52
|
+
// Getter is deliberately read here, after Pi applies extension flag values.
|
|
53
|
+
const endpoint = options.endpoint;
|
|
54
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
55
|
+
if (live !== undefined && !live.retired && live.sessionId === sessionId &&
|
|
56
|
+
live.endpoint === endpoint && live.manager === ctx.sessionManager)
|
|
57
|
+
return lifecycle;
|
|
58
|
+
const old = retire();
|
|
59
|
+
const state = endpoint === undefined ? undefined : {
|
|
60
|
+
sessionId, endpoint, manager: ctx.sessionManager, sockets: new Set(), waiters: new Map(), retired: false,
|
|
61
|
+
};
|
|
62
|
+
live = state;
|
|
63
|
+
return serialize(async () => {
|
|
64
|
+
await stop(old);
|
|
65
|
+
if (state === undefined || state.retired)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
if (process.platform !== 'darwin')
|
|
69
|
+
throw new Error('Pi delivery requires macOS.');
|
|
70
|
+
if (!endpointValid(state.endpoint) || !identity(state.sessionId))
|
|
71
|
+
throw new Error('Invalid Pi receiver coordinate.');
|
|
72
|
+
const parent = await lstat(dirname(state.endpoint));
|
|
73
|
+
if (!parent.isDirectory() || parent.uid !== process.getuid?.() || (parent.mode & 0o077) !== 0) {
|
|
74
|
+
throw new Error('Pi delivery requires a private caller-owned parent directory.');
|
|
75
|
+
}
|
|
76
|
+
// Fail closed even for stale sockets/files. No reclaim or broad chmod.
|
|
77
|
+
try {
|
|
78
|
+
await lstat(state.endpoint);
|
|
79
|
+
throw new Error('Pi delivery endpoint is occupied.');
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code !== 'ENOENT')
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
if (state.retired)
|
|
86
|
+
return;
|
|
87
|
+
const bindPath = join(dirname(state.endpoint), `.pi-${randomBytes(6).toString('hex')}`);
|
|
88
|
+
if (!endpointValid(bindPath))
|
|
89
|
+
throw new Error('Invalid Pi receiver coordinate.');
|
|
90
|
+
const server = createServer((socket) => accept(state, socket));
|
|
91
|
+
state.server = server;
|
|
92
|
+
// All diagnostics are fixed text: no path, request, or native error cause.
|
|
93
|
+
server.on('error', () => { if (live === state)
|
|
94
|
+
void close(); });
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
server.once('error', () => reject(new Error('Pi delivery listener unavailable.')));
|
|
97
|
+
server.listen(bindPath, resolve);
|
|
98
|
+
});
|
|
99
|
+
if (state.retired) {
|
|
100
|
+
await stop(state);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await chmod(bindPath, 0o600);
|
|
104
|
+
const owned = await lstat(bindPath);
|
|
105
|
+
await link(bindPath, state.endpoint); // Atomic no-replace publish; even a race fails closed.
|
|
106
|
+
state.published = { dev: owned.dev, ino: owned.ino };
|
|
107
|
+
if (state.retired)
|
|
108
|
+
await stop(state);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
if (live === state)
|
|
112
|
+
retire();
|
|
113
|
+
await stop(state);
|
|
114
|
+
throw new Error('Pi delivery listener unavailable.');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
function accept(state, socket) {
|
|
119
|
+
if (state.retired || state.sockets.size >= MAX_CONNECTIONS) {
|
|
120
|
+
socket.destroy();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
state.sockets.add(socket);
|
|
124
|
+
socket.on('error', () => { });
|
|
125
|
+
let waiter;
|
|
126
|
+
let timer = setTimeout(() => socket.destroy(), FRAME_WAIT_MS);
|
|
127
|
+
const reply = (value) => {
|
|
128
|
+
if (!socket.destroyed)
|
|
129
|
+
socket.end(frame(value));
|
|
130
|
+
// A non-reading peer must not retain the connection after an attempted reply.
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
timer = setTimeout(() => socket.destroy(), FRAME_WAIT_MS);
|
|
133
|
+
};
|
|
134
|
+
const reject = (code) => reply({ protocol: VERSION, state: 'rejected', code });
|
|
135
|
+
const detach = readFrame(socket, (request) => {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
if (!record(request) || request.protocol !== VERSION || request.harness !== 'pi') {
|
|
138
|
+
reject('invalid_request');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (state.retired || live !== state || request.sessionId !== state.sessionId) {
|
|
142
|
+
reject('wrong_session');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (request.op === 'hello') {
|
|
146
|
+
reply({ protocol: VERSION, harness: 'pi', sessionId: state.sessionId });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (request.op !== 'send' || typeof request.text !== 'string' || request.text.length === 0 ||
|
|
150
|
+
Buffer.byteLength(request.text) > MAX_TEXT_BYTES || !identity(request.inputId) ||
|
|
151
|
+
(request.delivery !== 'steer' && request.delivery !== 'queue') || !waitValid(request.timeoutMs) ||
|
|
152
|
+
Object.keys(request).some((key) => !['protocol', 'harness', 'op', 'sessionId', 'text', 'inputId', 'delivery', 'timeoutMs'].includes(key))) {
|
|
153
|
+
reject('invalid_request');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const { inputId, text, delivery, timeoutMs } = request;
|
|
157
|
+
if (state.waiters.has(inputId)) {
|
|
158
|
+
reject('duplicate_inflight_id');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let settled = false;
|
|
162
|
+
waiter = {
|
|
163
|
+
text,
|
|
164
|
+
finish(result, code) {
|
|
165
|
+
if (settled)
|
|
166
|
+
return;
|
|
167
|
+
settled = true;
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
state.waiters.delete(inputId);
|
|
170
|
+
reply({ protocol: VERSION, harness: 'pi', sessionId: state.sessionId, inputId, delivery,
|
|
171
|
+
state: result, ...(result === 'observed' ? { evidence: 'message_end' } : { code }) });
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
// Register first: native events may be synchronous in an embedding.
|
|
175
|
+
state.waiters.set(inputId, waiter);
|
|
176
|
+
timer = setTimeout(() => waiter?.finish('unknown', 'timeout'), timeoutMs);
|
|
177
|
+
try {
|
|
178
|
+
sendPiMessage(pi, { customType: CUSTOM_TYPE, content: text, display: true,
|
|
179
|
+
details: { agentDelivery: { deliveryId: inputId } } }, { deliverAs: delivery === 'queue' ? 'followUp' : 'steer', triggerTurn: true });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
waiter.finish('unknown', 'native_call');
|
|
183
|
+
}
|
|
184
|
+
}, () => socket.destroy());
|
|
185
|
+
socket.once('close', () => {
|
|
186
|
+
detach();
|
|
187
|
+
waiter?.finish('unknown', 'transport');
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
state.sockets.delete(socket);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
pi.on('message_end', (event, ctx) => {
|
|
193
|
+
const state = live;
|
|
194
|
+
const message = event.message;
|
|
195
|
+
if (state === undefined || state.retired || ctx.sessionManager !== state.manager ||
|
|
196
|
+
ctx.sessionManager.getSessionId() !== state.sessionId || !record(message) ||
|
|
197
|
+
message.role !== 'custom' || message.customType !== CUSTOM_TYPE || !record(message.details) ||
|
|
198
|
+
!record(message.details.agentDelivery))
|
|
199
|
+
return;
|
|
200
|
+
const id = message.details.agentDelivery.deliveryId;
|
|
201
|
+
if (typeof id !== 'string')
|
|
202
|
+
return;
|
|
203
|
+
const waiter = state.waiters.get(id);
|
|
204
|
+
if (waiter !== undefined && message.content === waiter.text)
|
|
205
|
+
waiter.finish('observed');
|
|
206
|
+
});
|
|
207
|
+
pi.on('session_shutdown', close);
|
|
208
|
+
return { close };
|
|
209
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
declare const targetBrand: unique symbol;
|
|
2
|
+
export interface OpenCodeTarget {
|
|
3
|
+
readonly harness: 'opencode';
|
|
4
|
+
readonly sessionId: string;
|
|
5
|
+
readonly [targetBrand]: true;
|
|
6
|
+
}
|
|
7
|
+
export interface ClaudeTarget {
|
|
8
|
+
readonly harness: 'claude';
|
|
9
|
+
readonly sessionId: string;
|
|
10
|
+
readonly [targetBrand]: true;
|
|
11
|
+
}
|
|
12
|
+
export interface PiTarget {
|
|
13
|
+
readonly harness: 'pi';
|
|
14
|
+
readonly sessionId: string;
|
|
15
|
+
readonly [targetBrand]: true;
|
|
16
|
+
}
|
|
17
|
+
export interface PaseoTarget {
|
|
18
|
+
readonly harness: 'paseo';
|
|
19
|
+
readonly agentId: string;
|
|
20
|
+
readonly [targetBrand]: true;
|
|
21
|
+
}
|
|
22
|
+
export type Target = OpenCodeTarget | ClaudeTarget | PiTarget | PaseoTarget;
|
|
23
|
+
export type Delivery = 'steer' | 'queue';
|
|
24
|
+
export interface OpenCodeSendTextOptions {
|
|
25
|
+
readonly delivery?: Delivery;
|
|
26
|
+
readonly inputId?: string;
|
|
27
|
+
readonly timeoutMs?: number;
|
|
28
|
+
readonly signal?: AbortSignal;
|
|
29
|
+
}
|
|
30
|
+
export interface ClaudeSendTextOptions {
|
|
31
|
+
readonly delivery?: 'steer';
|
|
32
|
+
readonly inputId?: never;
|
|
33
|
+
readonly timeoutMs?: number;
|
|
34
|
+
readonly signal?: AbortSignal;
|
|
35
|
+
}
|
|
36
|
+
export interface PiSendTextOptions {
|
|
37
|
+
readonly delivery?: Delivery;
|
|
38
|
+
readonly inputId?: never;
|
|
39
|
+
readonly timeoutMs?: number;
|
|
40
|
+
readonly signal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
export interface PaseoSendTextOptions {
|
|
43
|
+
readonly delivery?: 'steer';
|
|
44
|
+
readonly inputId?: string;
|
|
45
|
+
readonly timeoutMs?: number;
|
|
46
|
+
readonly signal?: AbortSignal;
|
|
47
|
+
}
|
|
48
|
+
export type SendTextOptions = OpenCodeSendTextOptions | ClaudeSendTextOptions | PiSendTextOptions | PaseoSendTextOptions;
|
|
49
|
+
interface OpenCodeAttempt {
|
|
50
|
+
readonly harness: 'opencode';
|
|
51
|
+
readonly sessionId: string;
|
|
52
|
+
readonly inputId: string;
|
|
53
|
+
}
|
|
54
|
+
export type OpenCodeDeliveryResult = (OpenCodeAttempt & {
|
|
55
|
+
readonly state: 'accepted';
|
|
56
|
+
readonly inboxId: string;
|
|
57
|
+
readonly delivery: Delivery;
|
|
58
|
+
}) | (OpenCodeAttempt & {
|
|
59
|
+
readonly state: 'unknown';
|
|
60
|
+
readonly code: 'timeout' | 'aborted' | 'transport' | 'invalid_response';
|
|
61
|
+
}) | (OpenCodeAttempt & {
|
|
62
|
+
readonly state: 'rejected';
|
|
63
|
+
readonly code: 'http_rejection';
|
|
64
|
+
readonly status: number;
|
|
65
|
+
}) | (OpenCodeAttempt & {
|
|
66
|
+
readonly state: 'unavailable';
|
|
67
|
+
readonly code: 'aborted' | 'timeout';
|
|
68
|
+
});
|
|
69
|
+
interface ClaudeAttempt {
|
|
70
|
+
readonly harness: 'claude';
|
|
71
|
+
readonly sessionId: string;
|
|
72
|
+
}
|
|
73
|
+
export type ClaudeDeliveryResult = (ClaudeAttempt & {
|
|
74
|
+
readonly state: 'written';
|
|
75
|
+
readonly messageId: string;
|
|
76
|
+
}) | (ClaudeAttempt & {
|
|
77
|
+
readonly state: 'unknown';
|
|
78
|
+
readonly code: 'timeout' | 'aborted' | 'transport';
|
|
79
|
+
}) | (ClaudeAttempt & {
|
|
80
|
+
readonly state: 'unavailable';
|
|
81
|
+
readonly code: 'aborted' | 'timeout' | 'endpoint_unavailable' | 'authentication_failed';
|
|
82
|
+
});
|
|
83
|
+
interface PiAttempt {
|
|
84
|
+
readonly harness: 'pi';
|
|
85
|
+
readonly sessionId: string;
|
|
86
|
+
readonly inputId: string;
|
|
87
|
+
}
|
|
88
|
+
export type PiDeliveryResult = (PiAttempt & {
|
|
89
|
+
readonly state: 'observed';
|
|
90
|
+
readonly evidence: 'message_end';
|
|
91
|
+
readonly delivery: Delivery;
|
|
92
|
+
}) | (PiAttempt & {
|
|
93
|
+
readonly state: 'unknown';
|
|
94
|
+
readonly code: 'timeout' | 'aborted' | 'transport' | 'invalid_response' | 'session_replaced' | 'native_call';
|
|
95
|
+
}) | (PiAttempt & {
|
|
96
|
+
readonly state: 'rejected';
|
|
97
|
+
readonly code: 'wrong_session' | 'invalid_request' | 'duplicate_inflight_id';
|
|
98
|
+
}) | (PiAttempt & {
|
|
99
|
+
readonly state: 'unavailable';
|
|
100
|
+
readonly code: 'timeout' | 'aborted' | 'transport' | 'invalid_response';
|
|
101
|
+
});
|
|
102
|
+
interface PaseoAttempt {
|
|
103
|
+
readonly harness: 'paseo';
|
|
104
|
+
readonly agentId: string;
|
|
105
|
+
readonly inputId: string;
|
|
106
|
+
}
|
|
107
|
+
export type PaseoDeliveryResult = (PaseoAttempt & {
|
|
108
|
+
readonly state: 'admitted';
|
|
109
|
+
}) | (PaseoAttempt & {
|
|
110
|
+
readonly state: 'rejected';
|
|
111
|
+
readonly code: 'key_conflict' | 'not_found' | 'refused';
|
|
112
|
+
}) | (PaseoAttempt & {
|
|
113
|
+
readonly state: 'unavailable';
|
|
114
|
+
readonly code: 'aborted' | 'timeout' | 'authentication_failed' | 'transport';
|
|
115
|
+
}) | (PaseoAttempt & {
|
|
116
|
+
readonly state: 'unknown';
|
|
117
|
+
readonly code: 'timeout' | 'aborted' | 'transport';
|
|
118
|
+
});
|
|
119
|
+
export type DeliveryResult = OpenCodeDeliveryResult | ClaudeDeliveryResult | PiDeliveryResult | PaseoDeliveryResult;
|
|
120
|
+
export {};
|
package/dist/participant-host.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { readParticipantOwner, sessionOwnsParticipant } from './registry.js';
|
|
1
|
+
import { presenceEpoch, readParticipantOwner, sessionOwnsParticipant } from './registry.js';
|
|
2
|
+
import { firstHarnessSession } from './participant-identity.js';
|
|
2
3
|
import { defaultWakeRouteCapabilities, publishWakeRoute, retireWakeRouteFromArtifact, resolvePrimaryWakeRoute, ROUTE_FRESH_MS } from './routes.js';
|
|
3
4
|
export function processIdentity(env) {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
[env.OPENCODE_SESSION_ID, 'opencode'], [env.PI_SESSION_ID, 'pi'], [env.PASEO_AGENT_ID, 'paseo'],
|
|
7
|
-
];
|
|
8
|
-
const found = choices.find(([session]) => session?.trim());
|
|
9
|
-
return found === undefined ? { session: `process:${process.pid}`, channel: 'unknown' } : { session: found[0].trim(), channel: found[1] };
|
|
5
|
+
const found = firstHarnessSession(env);
|
|
6
|
+
return found === undefined ? { session: `process:${process.pid}`, channel: 'unknown' } : { session: found.sessionId, channel: found.channel };
|
|
10
7
|
}
|
|
11
8
|
export async function identityRouteDraft(context, participant) {
|
|
12
9
|
if (context.location === undefined || context.location === 'memory')
|
|
@@ -17,10 +14,11 @@ export async function identityRouteDraft(context, participant) {
|
|
|
17
14
|
return resolvePrimaryWakeRoute({ location: context.location, participant, sessionId: identity.session, provider }, context.env ?? process.env, capabilities);
|
|
18
15
|
}
|
|
19
16
|
async function currentOwnerEpoch(context, participant) {
|
|
20
|
-
|
|
17
|
+
const hostLedger = context.hostLedger;
|
|
18
|
+
if (hostLedger === undefined || context.location === undefined || context.location === 'memory')
|
|
21
19
|
return undefined;
|
|
22
|
-
const owner = await readParticipantOwner(context.location, participant,
|
|
23
|
-
return owner
|
|
20
|
+
const owner = await readParticipantOwner(context.location, participant, hostLedger);
|
|
21
|
+
return owner === undefined ? undefined : presenceEpoch(owner);
|
|
24
22
|
}
|
|
25
23
|
export async function publishIdentityRoute(context, participant, epoch) {
|
|
26
24
|
const route = await identityRouteDraft(context, participant);
|
|
@@ -36,13 +34,14 @@ export async function retireIdentityRoute(context, participant, expectedEpoch) {
|
|
|
36
34
|
await retireWakeRouteFromArtifact(context.artifact, { location: context.location, participant, sessionId: identity.session }, expectedEpoch === undefined ? {} : { expectedEpoch }).catch(() => undefined);
|
|
37
35
|
}
|
|
38
36
|
export async function assertLiveOwner(context, participant, expectedEpoch) {
|
|
39
|
-
|
|
37
|
+
const hostLedger = context.hostLedger;
|
|
38
|
+
if (hostLedger === undefined || context.location === undefined || context.location === 'memory')
|
|
40
39
|
return true;
|
|
41
40
|
const identity = processIdentity(context.env ?? process.env);
|
|
42
41
|
// Library callers without a harness session are not ownership-fenced unless an epoch was supplied.
|
|
43
42
|
if (identity.channel === 'unknown' && expectedEpoch === undefined)
|
|
44
43
|
return true;
|
|
45
|
-
return sessionOwnsParticipant(context.location, participant, identity.session,
|
|
44
|
+
return sessionOwnsParticipant(context.location, participant, identity.session, hostLedger, expectedEpoch);
|
|
46
45
|
}
|
|
47
46
|
/** Presence is best effort and runs only after the artifact mutation commits. */
|
|
48
47
|
export async function ensureLocalPresence(context, participant, epoch) {
|
|
@@ -56,7 +55,7 @@ export async function ensureLocalPresence(context, participant, epoch) {
|
|
|
56
55
|
const existing = await context.hostLedger.listPresence({ location: context.location, participant, session: identity.session, now });
|
|
57
56
|
if (existing.some((row) => row.channel === identity.channel
|
|
58
57
|
&& now - (row.updatedAt ?? 0) < ROUTE_FRESH_MS
|
|
59
|
-
&& (ownerEpoch === undefined || row
|
|
58
|
+
&& (ownerEpoch === undefined || presenceEpoch(row) === ownerEpoch)))
|
|
60
59
|
return;
|
|
61
60
|
}
|
|
62
61
|
catch { /* fall through to the best-effort ensure below */ }
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
export declare function participantIdentity(name: string): string;
|
|
2
2
|
export type AutomaticProvider = 'codex' | 'claude' | 'opencode' | 'pi';
|
|
3
|
+
/** Channels a harness coordinate can map to. `unknown` stays a process-level fallback, not a coordinate. */
|
|
4
|
+
export type HarnessSessionChannel = 'claude-code' | 'codex' | 'opencode' | 'pi' | 'paseo';
|
|
5
|
+
export type HarnessSessionVariable = 'CLAUDE_CODE_SESSION_ID' | 'CODEX_THREAD_ID' | 'OPENCODE_SESSION_ID' | 'PI_SESSION_ID' | 'PASEO_AGENT_ID';
|
|
6
|
+
/**
|
|
7
|
+
* Ordered harness environment coordinates. This one table is the mapping owner every consumer
|
|
8
|
+
* derives from; `provider` is absent for a coordinate without an automatic participant provider.
|
|
9
|
+
*/
|
|
10
|
+
export interface HarnessSessionSource {
|
|
11
|
+
readonly variable: HarnessSessionVariable;
|
|
12
|
+
readonly channel: HarnessSessionChannel;
|
|
13
|
+
readonly provider?: AutomaticProvider;
|
|
14
|
+
readonly childVariable?: 'CLAUDE_CODE_CHILD_SESSION';
|
|
15
|
+
}
|
|
16
|
+
export declare const harnessSessionSources: readonly HarnessSessionSource[];
|
|
17
|
+
/** Raw nonempty harness session ids in table order. Duplicates are preserved, never collapsed. */
|
|
18
|
+
export declare function sessionIdsFromEnvironment(env?: NodeJS.ProcessEnv): readonly string[];
|
|
19
|
+
/** The first nonempty harness coordinate in table order, or undefined when none is set. */
|
|
20
|
+
export declare function firstHarnessSession(env?: NodeJS.ProcessEnv): {
|
|
21
|
+
readonly sessionId: string;
|
|
22
|
+
readonly channel: HarnessSessionChannel;
|
|
23
|
+
} | undefined;
|
|
3
24
|
export declare function automaticParticipant(provider: AutomaticProvider, sessionId: string, env: NodeJS.ProcessEnv): string;
|
|
4
25
|
/** Compute the current harness participant without reading a Square or registry artifact. */
|
|
5
26
|
export declare function squareAssignedParticipantName(env?: NodeJS.ProcessEnv): string | undefined;
|
|
@@ -3,12 +3,28 @@ import { validateName } from './model.js';
|
|
|
3
3
|
export function participantIdentity(name) {
|
|
4
4
|
return `@${name}`;
|
|
5
5
|
}
|
|
6
|
-
const
|
|
7
|
-
{ variable: 'CLAUDE_CODE_SESSION_ID', provider: 'claude' },
|
|
8
|
-
{ variable: 'CODEX_THREAD_ID', provider: 'codex' },
|
|
9
|
-
{ variable: 'OPENCODE_SESSION_ID', provider: 'opencode' },
|
|
10
|
-
{ variable: 'PI_SESSION_ID', provider: 'pi' },
|
|
6
|
+
export const harnessSessionSources = [
|
|
7
|
+
{ variable: 'CLAUDE_CODE_SESSION_ID', channel: 'claude-code', provider: 'claude', childVariable: 'CLAUDE_CODE_CHILD_SESSION' },
|
|
8
|
+
{ variable: 'CODEX_THREAD_ID', channel: 'codex', provider: 'codex' },
|
|
9
|
+
{ variable: 'OPENCODE_SESSION_ID', channel: 'opencode', provider: 'opencode' },
|
|
10
|
+
{ variable: 'PI_SESSION_ID', channel: 'pi', provider: 'pi' },
|
|
11
|
+
{ variable: 'PASEO_AGENT_ID', channel: 'paseo' },
|
|
11
12
|
];
|
|
13
|
+
/** Raw nonempty harness session ids in table order. Duplicates are preserved, never collapsed. */
|
|
14
|
+
export function sessionIdsFromEnvironment(env = process.env) {
|
|
15
|
+
return harnessSessionSources
|
|
16
|
+
.map((source) => env[source.variable]?.trim())
|
|
17
|
+
.filter((value) => Boolean(value));
|
|
18
|
+
}
|
|
19
|
+
/** The first nonempty harness coordinate in table order, or undefined when none is set. */
|
|
20
|
+
export function firstHarnessSession(env = process.env) {
|
|
21
|
+
for (const source of harnessSessionSources) {
|
|
22
|
+
const sessionId = env[source.variable]?.trim();
|
|
23
|
+
if (sessionId)
|
|
24
|
+
return { sessionId, channel: source.channel };
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
12
28
|
export function automaticParticipant(provider, sessionId, env) {
|
|
13
29
|
const configured = env.SQUARE_PARTICIPANT_NAME?.trim();
|
|
14
30
|
if (configured) {
|
|
@@ -25,11 +41,16 @@ export function squareAssignedParticipantName(env = process.env) {
|
|
|
25
41
|
validateName(configured);
|
|
26
42
|
return configured;
|
|
27
43
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
// Every native provider contributes its own name without deduping session ids first: equal ids
|
|
45
|
+
// in different providers are different coordinates and must stay ambiguous.
|
|
46
|
+
const names = new Set();
|
|
47
|
+
for (const source of harnessSessionSources) {
|
|
48
|
+
if (source.provider === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
const sessionId = env[source.variable]?.trim();
|
|
51
|
+
if (!sessionId)
|
|
52
|
+
continue;
|
|
53
|
+
names.add(automaticParticipant(source.provider, sessionId, env));
|
|
54
|
+
}
|
|
34
55
|
return names.size === 1 ? [...names][0] : undefined;
|
|
35
56
|
}
|
package/dist/paseo-delivery.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type WakeAdapter, type
|
|
1
|
+
import { type WakeAdapter, type WakeAdapterResult, type WakeDispatchContext } from './delivery.js';
|
|
2
2
|
import { discoverPaseoAgents, waitForPaseoWakeBoundary } from './paseo-state.js';
|
|
3
3
|
import { sendPaseoWake } from './wake-sink.js';
|
|
4
4
|
export interface PaseoAdapterOptions {
|
|
@@ -10,5 +10,5 @@ export declare class PaseoAdapter implements WakeAdapter {
|
|
|
10
10
|
private readonly opts;
|
|
11
11
|
readonly kind: "paseo";
|
|
12
12
|
constructor(opts?: PaseoAdapterOptions);
|
|
13
|
-
dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number): Promise<
|
|
13
|
+
dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number, context?: WakeDispatchContext): Promise<WakeAdapterResult>;
|
|
14
14
|
}
|
package/dist/paseo-delivery.js
CHANGED
|
@@ -1,40 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DeliveryError } from './packages/agent-delivery/src/index.js';
|
|
2
|
+
import { resolvePaseoDaemon } from './packages/agent-delivery/src/paseo-native.js';
|
|
2
3
|
import { discoverPaseoAgents, waitForPaseoWakeBoundary } from './paseo-state.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
try {
|
|
6
|
-
return resolvePaseoDaemonTarget(paseoDaemonHosts()[0]).url;
|
|
7
|
-
}
|
|
8
|
-
catch {
|
|
9
|
-
return 'unresolved';
|
|
10
|
-
}
|
|
11
|
-
}
|
|
4
|
+
import { sendPaseoWake } from './wake-sink.js';
|
|
5
|
+
/** Every send failure is a DeliveryError; only its code and certainty decide retry. */
|
|
12
6
|
function diagnostic(phase, address, code) {
|
|
13
7
|
return {
|
|
14
8
|
phase,
|
|
15
9
|
code,
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
// Only discovery still runs a Paseo CLI; a wake is a capability call on the shared entry.
|
|
11
|
+
...(phase === 'discovery' ? { command: 'paseo ls --global --json' } : {}),
|
|
12
|
+
endpoint: resolvePaseoDaemon(undefined)?.url ?? 'unresolved',
|
|
18
13
|
paseoAgentIds: [address.agentId].filter(Boolean),
|
|
19
14
|
passwordPresent: Boolean(process.env.PASEO_PASSWORD),
|
|
20
15
|
};
|
|
21
16
|
}
|
|
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);
|
|
26
|
-
}
|
|
27
17
|
export class PaseoAdapter {
|
|
28
18
|
opts;
|
|
29
19
|
kind = 'paseo';
|
|
30
20
|
constructor(opts = {}) {
|
|
31
21
|
this.opts = opts;
|
|
32
22
|
}
|
|
33
|
-
async dispatch(address, payload, beforeSend, timeoutMs = 5000) {
|
|
23
|
+
async dispatch(address, payload, beforeSend, timeoutMs = 5000, context) {
|
|
34
24
|
const deadline = Date.now() + timeoutMs;
|
|
35
25
|
const remainingMs = () => Math.max(0, deadline - Date.now());
|
|
36
26
|
const budgetUnavailable = () => ({
|
|
37
|
-
outcome: '
|
|
27
|
+
outcome: 'failed',
|
|
28
|
+
unavailable: true,
|
|
38
29
|
signature: 'dispatch_budget_exhausted',
|
|
39
30
|
message: 'The wake dispatch budget elapsed before Paseo accepted the wake.',
|
|
40
31
|
retainRoute: true,
|
|
@@ -42,7 +33,8 @@ export class PaseoAdapter {
|
|
|
42
33
|
const agentId = address.agentId?.trim();
|
|
43
34
|
if (!agentId) {
|
|
44
35
|
return {
|
|
45
|
-
outcome: '
|
|
36
|
+
outcome: 'failed',
|
|
37
|
+
unavailable: true,
|
|
46
38
|
signature: 'invalid_address',
|
|
47
39
|
message: 'Paseo route has no agent id.',
|
|
48
40
|
routeStale: true,
|
|
@@ -55,8 +47,12 @@ export class PaseoAdapter {
|
|
|
55
47
|
const discovery = (this.opts.discover ?? discoverPaseoAgents)(remaining);
|
|
56
48
|
if (discovery.error && discovery.agents.length === 0) {
|
|
57
49
|
return {
|
|
58
|
-
outcome: '
|
|
59
|
-
|
|
50
|
+
outcome: 'failed',
|
|
51
|
+
unavailable: true,
|
|
52
|
+
// A credential or authorization refusal is proven; anything else may be transient.
|
|
53
|
+
signature: /password|auth|unauthori[sz]ed/i.test(discovery.error) ? 'discovery_rejected'
|
|
54
|
+
: /DAEMON_NOT_RUNNING|ECONNREFUSED|ENOENT|not found.*executable|ETIMEDOUT|timed out|timeout/i.test(discovery.error)
|
|
55
|
+
? 'discovery_transient' : 'discovery_rejected',
|
|
60
56
|
message: `Paseo unavailable: ${discovery.error}`,
|
|
61
57
|
diagnostic: diagnostic('discovery', address, 'unavailable'),
|
|
62
58
|
retainRoute: true,
|
|
@@ -65,7 +61,8 @@ export class PaseoAdapter {
|
|
|
65
61
|
const agent = discovery.agents.find((candidate) => candidate.id === agentId);
|
|
66
62
|
if (agent === undefined || agent.status !== 'idle') {
|
|
67
63
|
return {
|
|
68
|
-
outcome: '
|
|
64
|
+
outcome: 'failed',
|
|
65
|
+
unavailable: true,
|
|
69
66
|
signature: agent === undefined ? 'address_not_found' : 'agent_not_idle',
|
|
70
67
|
message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle.',
|
|
71
68
|
diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : 'not_idle'),
|
|
@@ -78,7 +75,8 @@ export class PaseoAdapter {
|
|
|
78
75
|
return budgetUnavailable();
|
|
79
76
|
if (!(await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent, remaining))) {
|
|
80
77
|
return {
|
|
81
|
-
outcome: '
|
|
78
|
+
outcome: 'failed',
|
|
79
|
+
unavailable: true,
|
|
82
80
|
signature: 'boundary_unavailable',
|
|
83
81
|
message: 'Paseo did not reach the current tool boundary before the wake timeout.',
|
|
84
82
|
diagnostic: diagnostic('boundary', address, 'unavailable'),
|
|
@@ -86,17 +84,27 @@ export class PaseoAdapter {
|
|
|
86
84
|
};
|
|
87
85
|
}
|
|
88
86
|
if (!(await beforeSend()))
|
|
89
|
-
return { outcome: '
|
|
87
|
+
return { outcome: 'gate-rejected' };
|
|
90
88
|
remaining = remainingMs();
|
|
91
89
|
if (remaining === 0)
|
|
92
90
|
return budgetUnavailable();
|
|
93
91
|
try {
|
|
94
|
-
(this.opts.sendWake ?? sendPaseoWake)({
|
|
92
|
+
await (this.opts.sendWake ?? sendPaseoWake)({
|
|
93
|
+
agentId,
|
|
94
|
+
prompt: payload,
|
|
95
|
+
location: context?.location ?? '',
|
|
96
|
+
participant: context?.participant ?? '',
|
|
97
|
+
activity: context?.activity ?? '',
|
|
98
|
+
...(context?.attemptN === undefined ? {} : { attemptN: context.attemptN }),
|
|
99
|
+
}, { timeoutMs: remaining });
|
|
95
100
|
return { outcome: 'accepted' };
|
|
96
101
|
}
|
|
97
102
|
catch (error) {
|
|
98
103
|
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
-
const kind = error instanceof
|
|
104
|
+
const kind = !(error instanceof DeliveryError) || error.maybeDelivered ? 'unknown'
|
|
105
|
+
: error.code === 'unavailable' ? 'transient'
|
|
106
|
+
: ['authentication_failed', 'session_not_found', 'rejected', 'invalid_arguments'].includes(error.code) ? 'rejected'
|
|
107
|
+
: 'unknown';
|
|
100
108
|
const details = { ...diagnostic('send', address, 'failed'), outcome: kind };
|
|
101
109
|
if (kind === 'unknown')
|
|
102
110
|
return { outcome: 'unknown', signature: 'send_unknown', message, diagnostic: details };
|