@astrosheep/square 0.3.4 → 0.3.6
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/dist/activity-feed.js +26 -18
- package/dist/activity.js +23 -22
- package/dist/artifact.js +126 -202
- package/dist/claude-hook.js +45 -21
- package/dist/cli/context.js +143 -0
- package/dist/cli/harness-command.js +50 -0
- package/dist/cli/maintenance-commands.js +76 -0
- package/dist/cli/meta-commands.js +28 -0
- package/dist/cli/observation-commands.js +453 -0
- package/dist/cli/program.js +48 -0
- package/dist/cli/registry.js +40 -0
- package/dist/cli/square-commands.js +219 -0
- package/dist/cmd/notify-once.js +23 -21
- package/dist/compact.js +6 -19
- package/dist/decisions.js +53 -86
- package/dist/delivery-health.js +104 -210
- package/dist/delivery.js +68 -18
- package/dist/doctor.js +9 -8
- package/dist/harness-claude.js +68 -0
- package/dist/harness-codex.js +119 -0
- package/dist/harness-links.js +123 -0
- package/dist/harness-stage.js +36 -0
- package/dist/harness.js +94 -576
- package/dist/help.js +44 -35
- package/dist/inbox.js +12 -11
- package/dist/index.js +30 -129
- 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-timeline.js +58 -188
- package/dist/presentation.js +55 -63
- package/dist/presented.js +9 -8
- package/dist/registry.js +55 -45
- package/dist/runtime.js +26 -137
- package/dist/square-application.js +264 -0
- package/dist/square-core.js +3 -11
- package/dist/square.js +5 -1362
- package/dist/stream.js +27 -126
- package/dist/wake-sink.js +134 -188
- package/dist/watch.js +79 -138
- package/extensions/square-opencode.js +1 -1
- package/extensions/square-pi.js +8 -130
- 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-feedback/SKILL.md +4 -4
- package/dist/terminal.js +0 -125
package/dist/stream.js
CHANGED
|
@@ -1,149 +1,50 @@
|
|
|
1
|
-
// stream.ts — live activity feed
|
|
2
1
|
import fs from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
4
|
import { loadSquare } from './artifact.js';
|
|
6
|
-
import { planActNotifications } from './
|
|
5
|
+
import { planActNotifications } from './delivery.js';
|
|
7
6
|
import { sameName } from './model.js';
|
|
8
|
-
import { indexedDelta } from './activity-feed.js';
|
|
9
|
-
import { SLEEP_MS, actStableIndex, inSquareCount, latestIndexedActIndex, nowMs, rosterNames, sayNumberFor } from './runtime.js';
|
|
10
7
|
import { quoteShell } from './presentation.js';
|
|
11
|
-
import {
|
|
12
|
-
const INITIAL_DUMP = 20;
|
|
8
|
+
import { SLEEP_MS } from './runtime.js';
|
|
13
9
|
export function streamNotificationFor(doc, item, recipient) {
|
|
14
10
|
return planActNotifications(doc, item).find((notification) => sameName(notification.recipient, recipient));
|
|
15
11
|
}
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
function streamRows(squarePath, doc, cursor, recipient) {
|
|
13
|
+
return doc.acts.filter((act) => act.index > cursor).flatMap((act) => {
|
|
14
|
+
const notification = recipient === undefined ? undefined : streamNotificationFor(doc, act, recipient);
|
|
15
|
+
if (recipient !== undefined && notification === undefined)
|
|
16
|
+
return [];
|
|
17
|
+
return [JSON.stringify({
|
|
18
|
+
seq: act.index,
|
|
19
|
+
square: squarePath,
|
|
20
|
+
...act,
|
|
21
|
+
...(notification === undefined ? {} : { route: notification.route }),
|
|
22
|
+
})];
|
|
23
|
+
});
|
|
27
24
|
}
|
|
28
|
-
|
|
25
|
+
/** Machine-readable tailing stays available; interactive terminal rendering was retired. */
|
|
26
|
+
export async function cmdStreamNdjson(squarePath, recipient) {
|
|
29
27
|
if (!fs.existsSync(squarePath)) {
|
|
30
28
|
process.stderr.write(`square not found: ${squarePath}\n`);
|
|
31
|
-
process.
|
|
29
|
+
process.exitCode = 2;
|
|
30
|
+
return;
|
|
32
31
|
}
|
|
33
|
-
let doc = loadSquare(squarePath);
|
|
34
32
|
let cursor = -1;
|
|
35
|
-
const emit = (events) => {
|
|
36
|
-
for (const { act, index } of events) {
|
|
37
|
-
const item = { act, index };
|
|
38
|
-
const notification = forName ? streamNotificationFor(doc, item, forName) : undefined;
|
|
39
|
-
if (forName && !notification)
|
|
40
|
-
continue;
|
|
41
|
-
process.stdout.write(`${JSON.stringify({
|
|
42
|
-
seq: index,
|
|
43
|
-
square: squarePath,
|
|
44
|
-
...act,
|
|
45
|
-
...(notification ? { via: notification.via } : {}),
|
|
46
|
-
})}\n`);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
const backlog = indexedDelta(doc.acts, cursor);
|
|
50
|
-
emit(backlog);
|
|
51
|
-
cursor = latestIndexedActIndex(doc.acts.map((act) => ({ act, index: actStableIndex(act) })));
|
|
52
33
|
while (true) {
|
|
53
|
-
await sleep(SLEEP_MS);
|
|
54
34
|
try {
|
|
55
|
-
doc = loadSquare(squarePath);
|
|
35
|
+
const doc = loadSquare(squarePath);
|
|
36
|
+
for (const row of streamRows(squarePath, doc, cursor, recipient))
|
|
37
|
+
process.stdout.write(`${row}\n`);
|
|
38
|
+
cursor = Math.max(cursor, ...doc.acts.map((act) => act.index));
|
|
56
39
|
}
|
|
57
40
|
catch {
|
|
58
|
-
//
|
|
59
|
-
continue;
|
|
41
|
+
// A concurrent artifact replacement is retried on the next poll.
|
|
60
42
|
}
|
|
61
|
-
|
|
62
|
-
if (delta.length === 0)
|
|
63
|
-
continue;
|
|
64
|
-
emit(delta);
|
|
65
|
-
cursor = latestIndexedActIndex(doc.acts.map((act) => ({ act, index: actStableIndex(act) })));
|
|
43
|
+
await sleep(SLEEP_MS);
|
|
66
44
|
}
|
|
67
45
|
}
|
|
68
|
-
async function readKey() {
|
|
69
|
-
return new Promise((resolve) => {
|
|
70
|
-
const finish = (value) => {
|
|
71
|
-
clearTimeout(timer);
|
|
72
|
-
process.stdin.removeListener('data', onData);
|
|
73
|
-
resolve(value);
|
|
74
|
-
};
|
|
75
|
-
const onData = (chunk) => finish(chunk.toString());
|
|
76
|
-
const timer = setTimeout(() => finish(null), 100);
|
|
77
|
-
timer.unref();
|
|
78
|
-
process.stdin.once('data', onData);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
46
|
export async function cmdStream(squarePath) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
87
|
-
process.stderr.write('✕ interactive stream requires a TTY\n');
|
|
88
|
-
process.stderr.write(`» square --square-path ${quoteShell(path.resolve(squarePath))} stream --ndjson\n`);
|
|
89
|
-
process.exit(2);
|
|
90
|
-
}
|
|
91
|
-
let doc = loadSquare(squarePath);
|
|
92
|
-
let cursor = latestIndexedActIndex(doc.acts.map((act) => ({ act, index: actStableIndex(act) })));
|
|
93
|
-
let stoppedBy;
|
|
94
|
-
const stop = (signal) => {
|
|
95
|
-
stoppedBy = signal;
|
|
96
|
-
};
|
|
97
|
-
const onSigint = () => stop('SIGINT');
|
|
98
|
-
const onSigterm = () => stop('SIGTERM');
|
|
99
|
-
process.once('SIGINT', onSigint);
|
|
100
|
-
process.once('SIGTERM', onSigterm);
|
|
101
|
-
enterAlternateScreen();
|
|
102
|
-
enableRawMode();
|
|
103
|
-
hideCursor();
|
|
104
|
-
clearScreen();
|
|
105
|
-
try {
|
|
106
|
-
const allIndexed = doc.acts.map((act) => ({ act, index: actStableIndex(act) }));
|
|
107
|
-
const initial = allIndexed.filter((item) => item.act.kind !== 'read').slice(-INITIAL_DUMP);
|
|
108
|
-
cursor = Math.max(cursor, latestIndexedActIndex(initial));
|
|
109
|
-
process.stdout.write(renderDump(squarePath, doc, initial, nowMs()));
|
|
110
|
-
process.stdout.write(renderWaiting());
|
|
111
|
-
while (stoppedBy === undefined) {
|
|
112
|
-
const key = await readKey();
|
|
113
|
-
if (key === 'q' || key === '\x1b' || key === '\x03')
|
|
114
|
-
break;
|
|
115
|
-
try {
|
|
116
|
-
doc = loadSquare(squarePath);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
await sleep(SLEEP_MS);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const delta = indexedDelta(doc.acts, cursor);
|
|
123
|
-
if (delta.length === 0) {
|
|
124
|
-
await sleep(SLEEP_MS);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
cursor = latestIndexedActIndex(doc.acts.map((act) => ({ act, index: actStableIndex(act) })));
|
|
128
|
-
cursorUp(1);
|
|
129
|
-
clearLine();
|
|
130
|
-
const fresh = nowMs();
|
|
131
|
-
for (const item of delta) {
|
|
132
|
-
if (item.act.kind !== 'read') {
|
|
133
|
-
process.stdout.write(renderStreamEvent(item.act, fresh, item.act.kind === 'say' ? sayNumberFor(doc.acts, item.act) : undefined));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
process.stdout.write(renderWaiting());
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
finally {
|
|
140
|
-
process.off('SIGINT', onSigint);
|
|
141
|
-
process.off('SIGTERM', onSigterm);
|
|
142
|
-
disableRawMode();
|
|
143
|
-
showCursor();
|
|
144
|
-
leaveAlternateScreen();
|
|
145
|
-
showCursor();
|
|
146
|
-
}
|
|
147
|
-
if (stoppedBy !== undefined)
|
|
148
|
-
process.exitCode = stoppedBy === 'SIGINT' ? 130 : 143;
|
|
47
|
+
process.stderr.write('✕ interactive stream was removed\n');
|
|
48
|
+
process.stderr.write(`» square --square-path ${quoteShell(path.resolve(squarePath))} stream --ndjson\n`);
|
|
49
|
+
process.exitCode = 2;
|
|
149
50
|
}
|
package/dist/wake-sink.js
CHANGED
|
@@ -1,219 +1,165 @@
|
|
|
1
|
-
|
|
2
|
-
* wake-sink.ts — shared paseo wake dispatch.
|
|
3
|
-
*
|
|
4
|
-
* This is the single canonical implementation of "wake a paseo agent
|
|
5
|
-
* when they receive an undelivered @mention or --bell in a square."
|
|
6
|
-
*
|
|
7
|
-
* The detached one-shot notification worker uses this module after the
|
|
8
|
-
* natural-delivery grace period. The WakeSink is stateless; the worker owns
|
|
9
|
-
* receipt checks, timing, and failure isolation.
|
|
10
|
-
*/
|
|
11
|
-
import { execSync, spawnSync } from 'node:child_process';
|
|
12
|
-
import { setTimeout as sleep } from 'node:timers/promises';
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
13
2
|
import { homedir } from 'node:os';
|
|
3
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
14
4
|
import { loadSquare } from './artifact.js';
|
|
15
|
-
import {
|
|
16
|
-
import { leaseOwnsNotification } from './delivery.js';
|
|
5
|
+
import { leaseOwnsNotification, isDeliveryDelivered } from './delivery.js';
|
|
17
6
|
import { sessionInbox } from './inbox.js';
|
|
18
|
-
import {
|
|
19
|
-
import { hasPresentedAttention, presentOnce } from './presented.js';
|
|
7
|
+
import { hasPresentedForOwner, presentOnce } from './presented.js';
|
|
20
8
|
import { lookupParticipant } from './registry.js';
|
|
21
9
|
import { quoteShell } from './presentation.js';
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const lease = lookupParticipant(squarePath, known).find((binding) => binding.ownerId === ownerId);
|
|
31
|
-
const activeLease = lease === undefined
|
|
32
|
-
? undefined
|
|
33
|
-
: sessionInbox(lease.sessionId).find((membership) => membership.name === known)?.catchLease;
|
|
34
|
-
if (activeLease === undefined || activeLease.expiresAt <= Date.now())
|
|
35
|
-
return false;
|
|
36
|
-
await sleep(Math.min(250, Math.max(1, activeLease.expiresAt - Date.now())));
|
|
10
|
+
import { isCurrentlyJoined, resolveRosterName } from './runtime.js';
|
|
11
|
+
import { waitForPaseoToolBoundary } from './paseo-timeline.js';
|
|
12
|
+
export class PaseoWakeError extends Error {
|
|
13
|
+
diagnostic;
|
|
14
|
+
constructor(message, diagnostic) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.diagnostic = diagnostic;
|
|
17
|
+
this.name = 'PaseoWakeError';
|
|
37
18
|
}
|
|
38
|
-
return false;
|
|
39
19
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const raw = process.env['SQUARE_PASEO_WAKE_TIMEOUT_MS'];
|
|
43
|
-
if (raw === undefined)
|
|
44
|
-
return DEFAULT_PASEO_WAKE_TIMEOUT_MS;
|
|
45
|
-
const value = Number.parseInt(raw, 10);
|
|
46
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
47
|
-
throw new SquareError('invalid_args', 'Invalid SQUARE_PASEO_WAKE_TIMEOUT_MS: expected a positive integer.');
|
|
48
|
-
}
|
|
49
|
-
return value;
|
|
20
|
+
function endpoint() {
|
|
21
|
+
return process.env.SQUARE_PASEO_WS_URL ?? process.env.PASEO_LISTEN ?? '127.0.0.1:6767';
|
|
50
22
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
};
|
|
54
33
|
}
|
|
55
|
-
function
|
|
56
|
-
return
|
|
34
|
+
function native(binding) {
|
|
35
|
+
return ['claude-code', 'codex', 'opencode', 'pi'].includes(binding.channel);
|
|
57
36
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
37
|
+
export function paseoOwnershipSnapshot(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()];
|
|
66
51
|
}
|
|
67
|
-
function
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
const displayPath = squarePath.startsWith(home) ? `~${squarePath.slice(home.length)}` : squarePath;
|
|
71
|
-
return [
|
|
72
|
-
'<system-reminder source="square">',
|
|
73
|
-
`Mentioned by @${item.act.actor} in \`${displayPath}\``,
|
|
74
|
-
'',
|
|
75
|
-
`> ${previewBody(item.act.body).replace(/\n/g, '\n> ')}`,
|
|
76
|
-
'',
|
|
77
|
-
'To catch up and keep presence current:',
|
|
78
|
-
`\`${wakeNowCommand(squarePath, recipient)}\``,
|
|
79
|
-
'',
|
|
80
|
-
'*(Async notification from Square.)*',
|
|
81
|
-
'</system-reminder>',
|
|
82
|
-
].join('\n');
|
|
52
|
+
export function selectPaseoWakeAgents(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'));
|
|
83
55
|
}
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
56
|
+
export function discoverPaseoAgents(timeoutMs = 5000) {
|
|
57
|
+
try {
|
|
58
|
+
const raw = execFileSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['ls', '--json'], { encoding: 'utf8', timeout: timeoutMs, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
const agents = Array.isArray(parsed) ? parsed : parsed?.agents;
|
|
61
|
+
if (!Array.isArray(agents))
|
|
62
|
+
return { agents: [], error: 'Paseo returned malformed agent inventory.' };
|
|
63
|
+
return { agents: agents.filter((item) => item !== null && typeof item === 'object' && typeof item.id === 'string' && typeof item.status === 'string') };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { agents: [], error: error instanceof Error ? error.message : String(error) };
|
|
67
|
+
}
|
|
90
68
|
}
|
|
91
|
-
function
|
|
92
|
-
const result = spawnSync('paseo', ['send', agentId, '--prompt', prompt, '--no-wait'], {
|
|
93
|
-
stdio: 'ignore',
|
|
94
|
-
timeout: 5000,
|
|
95
|
-
});
|
|
69
|
+
function send(agentId, prompt) {
|
|
70
|
+
const result = spawnSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['send', agentId, '--prompt', prompt, '--no-wait'], { stdio: 'ignore', timeout: 5000, env: process.env });
|
|
96
71
|
if (result.error)
|
|
97
72
|
throw result.error;
|
|
98
73
|
if (result.status !== 0)
|
|
99
74
|
throw new Error(`paseo send exited with ${result.status ?? 'no status'}`);
|
|
100
75
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
export function listPaseoAgents() {
|
|
104
|
-
try {
|
|
105
|
-
return JSON.parse(execSync('paseo ls --json', {
|
|
106
|
-
encoding: 'utf8',
|
|
107
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
108
|
-
}));
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
76
|
+
function catchCommand(squarePath, recipient) {
|
|
77
|
+
return `square --as ${quoteShell(recipient)} --square-path ${quoteShell(squarePath)} catch --now`;
|
|
113
78
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
79
|
+
function prompt(notification, squarePath, nativeWake) {
|
|
80
|
+
const display = squarePath.startsWith(homedir()) ? `~${squarePath.slice(homedir().length)}` : squarePath;
|
|
81
|
+
const body = notification.item.body.length > 200 ? `${notification.item.body.slice(0, 197)}...` : notification.item.body;
|
|
82
|
+
return [
|
|
83
|
+
'<system-reminder source="square">',
|
|
84
|
+
`${notification.route === 'bell' ? 'Bell' : notification.route === 'beside' ? 'Beside' : 'Mention'} from @${notification.item.actor} in \`${display}\``,
|
|
85
|
+
nativeWake ? 'The native adapter will present it at the next boundary.' : `> ${body.replace(/\n/g, '\n> ')}`,
|
|
86
|
+
`\`${catchCommand(squarePath, notification.recipient)}\``,
|
|
87
|
+
'</system-reminder>',
|
|
88
|
+
].join('\n');
|
|
123
89
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return agents.filter((agent) => ids.has(agent.id) && (agent.status === 'running' || agent.status === 'idle'));
|
|
90
|
+
async function waitForCatch(squarePath, recipient, notification, ownerId) {
|
|
91
|
+
const deadline = Date.now() + 180_000;
|
|
92
|
+
while (Date.now() < deadline) {
|
|
93
|
+
const doc = loadSquare(squarePath);
|
|
94
|
+
if (isDeliveryDelivered(doc, recipient, notification.item.index))
|
|
95
|
+
return true;
|
|
96
|
+
const binding = lookupParticipant(squarePath, recipient).find((item) => item.ownerId === ownerId);
|
|
97
|
+
const lease = binding && sessionInbox(binding.sessionId).find((item) => item.name === recipient)?.catchLease;
|
|
98
|
+
if (!lease || lease.expiresAt <= Date.now())
|
|
99
|
+
return false;
|
|
100
|
+
await sleep(Math.min(250, lease.expiresAt - Date.now()));
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
138
103
|
}
|
|
139
|
-
export async function
|
|
140
|
-
const
|
|
104
|
+
export async function dispatchPaseoNotification(notification, ctx) {
|
|
105
|
+
const initial = loadSquare(ctx.squarePath);
|
|
106
|
+
const recipient = resolveRosterName(initial, notification.recipient);
|
|
107
|
+
if (!recipient || !isCurrentlyJoined(initial.acts, recipient))
|
|
108
|
+
return;
|
|
141
109
|
const bindings = lookupParticipant(ctx.squarePath, recipient);
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
110
|
+
const ownership = paseoOwnershipSnapshot(bindings);
|
|
111
|
+
if (ownership.length === 0)
|
|
144
112
|
return;
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
113
|
+
const discovery = discoverPaseoAgents();
|
|
114
|
+
if (discovery.error && discovery.agents.length === 0) {
|
|
115
|
+
throw new PaseoWakeError(`Paseo unavailable: ${discovery.error}`, diagnostic('discovery', ownership, 'unavailable'));
|
|
116
|
+
}
|
|
117
|
+
const active = selectPaseoWakeAgents(ownership, discovery.agents);
|
|
118
|
+
if (active.length === 0) {
|
|
119
|
+
throw new PaseoWakeError('No registered Paseo agent is idle or running.', diagnostic('selection', ownership, 'not_active'));
|
|
120
|
+
}
|
|
121
|
+
let boundaryTimedOut = false;
|
|
122
|
+
for (const agent of active) {
|
|
123
|
+
const owner = ownership.find((item) => item.agentId === agent.id);
|
|
124
|
+
if (!owner || hasPresentedForOwner(owner.ownerId, ctx.squarePath, recipient, notification.item.index))
|
|
125
|
+
continue;
|
|
126
|
+
if (agent.status === 'running' && !(await waitForPaseoToolBoundary(agent.id))) {
|
|
127
|
+
boundaryTimedOut = true;
|
|
128
|
+
continue;
|
|
151
129
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
130
|
+
const latest = loadSquare(ctx.squarePath);
|
|
131
|
+
if (!isCurrentlyJoined(latest.acts, recipient) || isDeliveryDelivered(latest, recipient, notification.item.index))
|
|
132
|
+
return;
|
|
133
|
+
const current = lookupParticipant(ctx.squarePath, recipient).find((item) => item.ownerId === owner.ownerId && item.paseoAgentId === owner.agentId);
|
|
134
|
+
if (!current)
|
|
135
|
+
continue;
|
|
136
|
+
const activeCatch = sessionInbox(current.sessionId).find((item) => item.name === recipient)?.catchLease;
|
|
137
|
+
if (activeCatch && leaseOwnsNotification(activeCatch, { actor: notification.item.actor, body: notification.item.body, route: notification.route, recipient })) {
|
|
138
|
+
if (await waitForCatch(ctx.squarePath, recipient, notification, owner.ownerId))
|
|
159
139
|
return;
|
|
160
140
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const binding = bindings.find((candidate) => candidate.paseoAgentId === active.id);
|
|
165
|
-
if (binding === undefined || wokenOwners.has(binding.ownerId))
|
|
166
|
-
continue;
|
|
167
|
-
try {
|
|
168
|
-
const catchMembership = sessionInbox(binding.sessionId).find((membership) => membership.name === (resolveRosterName(loadSquare(ctx.squarePath), recipient) ?? recipient) &&
|
|
169
|
-
membership.catchLease !== undefined &&
|
|
170
|
-
leaseOwnsNotification(membership.catchLease, {
|
|
171
|
-
actor: item.act.actor,
|
|
172
|
-
body: item.act.body,
|
|
173
|
-
via: notification.via === 'bell' ? 'bell' : 'mention',
|
|
174
|
-
}));
|
|
175
|
-
if (catchMembership?.catchLease !== undefined) {
|
|
176
|
-
if (await waitForCatchDelivery(ctx.squarePath, recipient, notification, binding.ownerId))
|
|
177
|
-
return;
|
|
178
|
-
const current = lookupParticipant(ctx.squarePath, recipient).find((candidate) => candidate.paseoAgentId === active.id);
|
|
179
|
-
if (current?.ownerId !== binding.ownerId)
|
|
180
|
-
continue;
|
|
181
|
-
const doc = loadSquare(ctx.squarePath);
|
|
182
|
-
const known = resolveRosterName(doc, recipient) ?? recipient;
|
|
183
|
-
if (isDeliveryDelivered(doc, known, item.index) ||
|
|
184
|
-
hasPresentedAttention(ctx.squarePath, known, item.index)) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
141
|
+
if (owner.nativeGuarantee) {
|
|
142
|
+
try {
|
|
143
|
+
send(agent.id, prompt({ ...notification, recipient }, ctx.squarePath, true));
|
|
187
144
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
wokenOwners.add(binding.ownerId);
|
|
191
|
-
continue;
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw new PaseoWakeError(error instanceof Error ? error.message : String(error), diagnostic('send', ownership, 'failed'));
|
|
192
147
|
}
|
|
193
|
-
|
|
194
|
-
.map((membership) => ({
|
|
195
|
-
...membership,
|
|
196
|
-
notifications: membership.notifications.filter((candidate) => candidate.actIndex === item.index),
|
|
197
|
-
}))
|
|
198
|
-
.filter((membership) => membership.notifications.length > 0), () => sendPaseoPrompt(active.id, fullWakePrompt(notification, ctx.squarePath)));
|
|
199
|
-
wokenOwners.add(binding.ownerId);
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
// Paseo unavailable: notification remains unpresented for a later retry.
|
|
148
|
+
return;
|
|
203
149
|
}
|
|
150
|
+
presentOnce(current.sessionId, (id) => sessionInbox(id).map((item) => ({ ...item, notifications: item.notifications.filter((note) => note.actIndex === notification.item.index) })).filter((item) => item.notifications.length > 0), () => {
|
|
151
|
+
try {
|
|
152
|
+
send(agent.id, prompt({ ...notification, recipient }, ctx.squarePath, false));
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
throw new PaseoWakeError(error instanceof Error ? error.message : String(error), diagnostic('send', ownership, 'failed'));
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
204
160
|
}
|
|
161
|
+
if (boundaryTimedOut)
|
|
162
|
+
throw new PaseoWakeError('Paseo did not reach the current tool boundary before the wake timeout.', diagnostic('boundary', ownership, 'timeout'));
|
|
205
163
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
export function paseoWakeSink() {
|
|
209
|
-
return {
|
|
210
|
-
name: 'paseo',
|
|
211
|
-
dispatch: dispatchPaseoWake,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
/** Default sink list, respecting SQUARE_DISABLE_PASEO_WAKE. */
|
|
215
|
-
export function defaultWakeSinks() {
|
|
216
|
-
if (process.env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
217
|
-
return [];
|
|
218
|
-
return [paseoWakeSink()];
|
|
219
|
-
}
|
|
164
|
+
export function paseoWakeSink() { return { name: 'paseo', dispatch: dispatchPaseoNotification }; }
|
|
165
|
+
export function defaultWakeSinks() { return process.env.SQUARE_DISABLE_PASEO_WAKE === '1' ? [] : [paseoWakeSink()]; }
|