@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.
Files changed (52) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/dist/activity-feed.js +26 -18
  3. package/dist/activity.js +23 -22
  4. package/dist/artifact.js +126 -202
  5. package/dist/claude-hook.js +45 -21
  6. package/dist/cli/context.js +143 -0
  7. package/dist/cli/harness-command.js +50 -0
  8. package/dist/cli/maintenance-commands.js +76 -0
  9. package/dist/cli/meta-commands.js +28 -0
  10. package/dist/cli/observation-commands.js +453 -0
  11. package/dist/cli/program.js +48 -0
  12. package/dist/cli/registry.js +40 -0
  13. package/dist/cli/square-commands.js +219 -0
  14. package/dist/cmd/notify-once.js +23 -21
  15. package/dist/compact.js +6 -19
  16. package/dist/decisions.js +53 -86
  17. package/dist/delivery-health.js +104 -210
  18. package/dist/delivery.js +68 -18
  19. package/dist/doctor.js +9 -8
  20. package/dist/harness-claude.js +68 -0
  21. package/dist/harness-codex.js +119 -0
  22. package/dist/harness-links.js +123 -0
  23. package/dist/harness-stage.js +36 -0
  24. package/dist/harness.js +94 -576
  25. package/dist/help.js +44 -35
  26. package/dist/inbox.js +12 -11
  27. package/dist/index.js +30 -129
  28. package/dist/list.js +1 -1
  29. package/dist/model.js +0 -6
  30. package/dist/notification-failures.js +54 -0
  31. package/dist/notifications.js +47 -62
  32. package/dist/paseo-timeline.js +58 -188
  33. package/dist/presentation.js +55 -63
  34. package/dist/presented.js +9 -8
  35. package/dist/registry.js +55 -45
  36. package/dist/runtime.js +26 -137
  37. package/dist/square-application.js +264 -0
  38. package/dist/square-core.js +3 -11
  39. package/dist/square.js +5 -1362
  40. package/dist/stream.js +27 -126
  41. package/dist/wake-sink.js +134 -188
  42. package/dist/watch.js +79 -138
  43. package/extensions/square-opencode.js +1 -1
  44. package/extensions/square-pi.js +8 -130
  45. package/guides/architect.md +3 -3
  46. package/guides/participant.md +25 -16
  47. package/package.json +2 -2
  48. package/skills/brainstorm/SKILL.md +25 -32
  49. package/skills/square/.claude-plugin/plugin.json +1 -1
  50. package/skills/square/SKILL.md +39 -107
  51. package/skills/square-feedback/SKILL.md +4 -4
  52. 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 './notifications.js';
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 { enableRawMode, disableRawMode, enterAlternateScreen, leaveAlternateScreen, clearScreen, hideCursor, showCursor, renderStreamHeader, renderStreamEvent, renderWaiting, cursorUp, clearLine, } from './terminal.js';
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
- export function matchesStreamRecipient(doc, item, recipient) {
17
- return streamNotificationFor(doc, item, recipient) !== undefined;
18
- }
19
- function renderDump(squarePath, doc, events, now) {
20
- const relevant = events.filter((item) => item.act.kind !== 'read');
21
- const active = inSquareCount(doc);
22
- if (relevant.length === 0)
23
- return `${renderStreamHeader(squarePath, rosterNames(doc).length, active)}\n\n (no activity yet)\n`;
24
- const header = renderStreamHeader(squarePath, rosterNames(doc).length, active);
25
- const body = relevant.map((item) => renderStreamEvent(item.act, now, item.act.kind === 'say' ? sayNumberFor(doc.acts, item.act) : undefined)).join('');
26
- return `${header}\n${body}`;
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
- export async function cmdStreamNdjson(squarePath, forName) {
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.exit(2);
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
- // Transient read failure (e.g. concurrent writer mid-rename) retry next poll.
59
- continue;
41
+ // A concurrent artifact replacement is retried on the next poll.
60
42
  }
61
- const delta = indexedDelta(doc.acts, cursor);
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
- if (!fs.existsSync(squarePath)) {
83
- process.stderr.write(`square not found: ${squarePath}\n`);
84
- process.exit(2);
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 { SquareError } from './model.js';
16
- import { leaseOwnsNotification } from './delivery.js';
5
+ import { leaseOwnsNotification, isDeliveryDelivered } from './delivery.js';
17
6
  import { sessionInbox } from './inbox.js';
18
- import { waitForPaseoToolBoundary } from './paseo-timeline.js';
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 { isDeliveryDelivered, resolveRosterName } from './runtime.js';
23
- async function waitForCatchDelivery(squarePath, recipient, notification, ownerId) {
24
- const deadline = Date.now() + 180_000;
25
- while (Date.now() < deadline) {
26
- const doc = loadSquare(squarePath);
27
- const known = resolveRosterName(doc, recipient) ?? recipient;
28
- if (isDeliveryDelivered(doc, known, notification.item.index))
29
- return true;
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
- const DEFAULT_PASEO_WAKE_TIMEOUT_MS = 90_000;
41
- function paseoWakeTimeoutMs() {
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
- // ── helpers ──────────────────────────────────────────────────────
52
- function previewBody(body) {
53
- return body.length > 200 ? body.slice(0, 197) + '...' : body;
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 wakeNowCommand(squarePath, recipient) {
56
- return `square --as ${quoteShell(recipient)} --square-path ${quoteShell(squarePath)} catch --now`;
34
+ function native(binding) {
35
+ return ['claude-code', 'codex', 'opencode', 'pi'].includes(binding.channel);
57
36
  }
58
- function nativeWakePrompt(squarePath, recipient) {
59
- return [
60
- '<system-reminder source="square">',
61
- `Square activity is waiting for @${recipient} in ${squarePath}.`,
62
- 'The native adapter will present it at the next boundary. If it does not:',
63
- `\`${wakeNowCommand(squarePath, recipient)}\``,
64
- '</system-reminder>',
65
- ].join('\n');
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 fullWakePrompt(notification, squarePath) {
68
- const { item, recipient } = notification;
69
- const home = homedir();
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 hasNativeGuarantee(bindings, ownerId) {
85
- return bindings.some((binding) => binding.ownerId === ownerId &&
86
- (binding.channel === 'claude-code' ||
87
- binding.channel === 'codex' ||
88
- binding.channel === 'opencode' ||
89
- binding.channel === 'pi'));
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 sendPaseoPrompt(agentId, prompt) {
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
- // ── paseo agent discovery ────────────────────────────────────────
102
- /** List all paseo agents. Returns empty array if paseo is unavailable. */
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
- * Wait for the tool calls that are running now to finish. Calls that start
116
- * afterward are deliberately ignored: the transitional wake policy allows
117
- * `paseo send` to replace the next call.
118
- */
119
- export async function waitForToolEnd(agentId, timeoutMs) {
120
- return waitForPaseoToolBoundary(agentId, {
121
- timeoutMs: timeoutMs ?? paseoWakeTimeoutMs(),
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
- // ── paseo wake dispatch ──────────────────────────────────────────
125
- /**
126
- * Dispatch a paseo send wake prompt for a single notification.
127
- *
128
- * Resolves exact Paseo agent ids from the machine-local participant registry.
129
- * Idle agents receive the wake immediately. Running agents wait for the
130
- * current tool call boundary; the following `paseo send` may replace the next
131
- * call by design.
132
- */
133
- export function selectPaseoWakeAgents(squarePath, recipient, agents = listPaseoAgents()) {
134
- const ids = new Set(lookupParticipant(squarePath, recipient)
135
- .map((binding) => binding.paseoAgentId)
136
- .filter((id) => id !== undefined));
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 dispatchPaseoWake(notification, ctx) {
140
- const { item, recipient } = notification;
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 activeAgents = selectPaseoWakeAgents(ctx.squarePath, recipient);
143
- if (activeAgents.length === 0)
110
+ const ownership = paseoOwnershipSnapshot(bindings);
111
+ if (ownership.length === 0)
144
112
  return;
145
- const wokenOwners = new Set();
146
- for (const active of activeAgents) {
147
- if (active.status === 'running') {
148
- const reachedBoundary = await waitForToolEnd(active.id);
149
- if (!reachedBoundary)
150
- continue;
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
- // Re-read after the tool wait: harness inject may have presented during the wait.
153
- try {
154
- const doc = loadSquare(ctx.squarePath);
155
- const known = resolveRosterName(doc, recipient) ?? recipient;
156
- if (isDeliveryDelivered(doc, known, item.index))
157
- return;
158
- if (hasPresentedAttention(ctx.squarePath, known, item.index))
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
- catch {
162
- // Receipt re-read failure fails open toward wake.
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
- if (hasNativeGuarantee(bindings, binding.ownerId)) {
189
- sendPaseoPrompt(active.id, nativeWakePrompt(ctx.squarePath, recipient));
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
- presentOnce(binding.sessionId, (sessionId) => sessionInbox(sessionId)
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
- // ── WakeSink factory ─────────────────────────────────────────────
207
- /** Create the default paseo WakeSink used by both sync and async paths. */
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()]; }