@astrosheep/square 0.3.83 → 0.3.84

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 (36) hide show
  1. package/dist/claude-delivery-status.d.ts +7 -0
  2. package/dist/claude-delivery-status.js +24 -0
  3. package/dist/claude-delivery.js +9 -15
  4. package/dist/claude-mod.js +5 -0
  5. package/dist/cli/catch-presentation.d.ts +2 -6
  6. package/dist/cli/catch-presentation.js +2 -50
  7. package/dist/cli/express-presentation.d.ts +2 -2
  8. package/dist/cli/express-presentation.js +6 -29
  9. package/dist/cli/observation-commands.js +10 -142
  10. package/dist/cli/program.js +5 -28
  11. package/dist/cli/square-commands.js +14 -56
  12. package/dist/delivery-health.d.ts +2 -2
  13. package/dist/delivery-health.js +20 -2
  14. package/dist/host-ledger-file-adapter.d.ts +3 -0
  15. package/dist/host-ledger-file-adapter.js +12 -0
  16. package/dist/host-ledger.d.ts +12 -0
  17. package/dist/mcp.js +17 -6
  18. package/dist/operation-presentation.d.ts +45 -0
  19. package/dist/operation-presentation.js +370 -0
  20. package/dist/packages/agent-delivery/src/claude-native.js +3 -1
  21. package/dist/routes.js +7 -2
  22. package/dist/square-tools.d.ts +14 -2
  23. package/dist/square-tools.js +36 -17
  24. package/dist/wake-evidence.d.ts +2 -1
  25. package/dist/wake-evidence.js +2 -1
  26. package/guides/catch-cold-path-optimization.md +1 -1
  27. package/guides/claude-native.md +32 -0
  28. package/package.json +1 -1
  29. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  30. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  31. package/plugins/paseo/OVERVIEW.md +7 -4
  32. package/plugins/paseo/README.md +30 -6
  33. package/plugins/paseo/index.server.ts +10 -0
  34. package/plugins/paseo/package.json +3 -3
  35. package/plugins/paseo/paseo-plugin.json +1 -1
  36. package/plugins/paseo/server/record-delivery.ts +28 -0
@@ -3,6 +3,8 @@ import { formatActivityId } from './square-core.js';
3
3
  import { participantIdentity, truncateChars } from './presentation.js';
4
4
  import { formatDuration } from './time.js';
5
5
  import { projectWakeEvidence } from './wake-eligibility.js';
6
+ import { decodeWakeEvidence } from './wake-evidence.js';
7
+ import { hostLedgerForEnv } from './registry.js';
6
8
  import { openSquare } from './open-square.js';
7
9
  import { closeOpenSquare } from './open-square.js';
8
10
  import { readWakeReleaseDiagnostics } from './wake-attempts.js';
@@ -62,7 +64,7 @@ function displayField(value) {
62
64
  const truncated = truncateChars(value, DISPLAY_FIELD_LIMIT - 1);
63
65
  return truncated.remaining === 0 ? truncated.text : `${truncated.text}…`;
64
66
  }
65
- export function renderDeliveryHealth(items, releases = [], now = Date.now()) {
67
+ export function renderDeliveryHealth(items, releases = [], now = Date.now(), nativeAttempts = []) {
66
68
  const out = items.length === 0 ? ['✓ no pending delivery attention'] : [`· delivery attention · ${items.length} pending`];
67
69
  if (items.length > 0) {
68
70
  let displayed = 0;
@@ -81,6 +83,17 @@ export function renderDeliveryHealth(items, releases = [], now = Date.now()) {
81
83
  if (items.length > DETAIL_LIMIT)
82
84
  out.push(`${DETAIL_LIMIT} of ${items.length} pending details shown`);
83
85
  }
86
+ if (nativeAttempts.length > 0) {
87
+ out.push(`· Claude native custody · ${nativeAttempts.length}`);
88
+ out.push(...nativeAttempts.slice(0, DETAIL_LIMIT).map((attempt) => {
89
+ const observations = [...attempt.nativeObservations ?? []].sort((a, b) => a.observedAt - b.observedAt);
90
+ const last = observations.at(-1);
91
+ const states = observations.map((observation) => `${observation.state}${observation.reason ? ` (${observation.reason})` : ''}`).join(' → ');
92
+ return ` · ${formatActivityId(attempt.attention.actIndex)} → ${displayField(participantIdentity(attempt.attention.recipient))} · ${attempt.outcome} · native ${displayField(states)} · ${formatDuration(Math.max(0, now - last.observedAt))} · message ${displayField(last.messageId)}`;
93
+ }));
94
+ if (nativeAttempts.length > DETAIL_LIMIT)
95
+ out.push(`${DETAIL_LIMIT} of ${nativeAttempts.length} native custody details shown`);
96
+ }
84
97
  if (releases.length > 0) {
85
98
  out.push(`· recent wake releases · ${releases.length}`);
86
99
  out.push(...releases.slice(0, DETAIL_LIMIT).map((release) => formatRelease(release, now)));
@@ -100,5 +113,10 @@ function formatRelease(release, now) {
100
113
  export async function doctorDeliveryHealth(squarePath, graceMs, now = Date.now(), env = process.env) {
101
114
  const items = await classifyDeliveryHealth(squarePath, { graceMs, now, env });
102
115
  const releases = await readWakeReleaseDiagnostics({ location: squarePath, now, env });
103
- return renderDeliveryHealth(items, releases, now);
116
+ const rows = await hostLedgerForEnv(env).listEvidence({ location: squarePath, kind: 'wake', includeReleased: true, now });
117
+ const nativeAttempts = rows.flatMap((row) => {
118
+ const decoded = decodeWakeEvidence(row, now);
119
+ return decoded?.kind === 'attempt' && decoded.value.nativeObservations?.length ? [decoded.value] : [];
120
+ }).sort((a, b) => Math.max(...b.nativeObservations.map((row) => row.observedAt)) - Math.max(...a.nativeObservations.map((row) => row.observedAt)));
121
+ return renderDeliveryHealth(items, releases, now, nativeAttempts);
104
122
  }
@@ -32,6 +32,9 @@ export declare class FileHostLedgerPort implements HostLedgerPort {
32
32
  confirmWakeAdmission(i: EvidenceRecord & {
33
33
  readonly claimToken: string;
34
34
  }): Promise<boolean>;
35
+ observeNativeDelivery(i: EvidenceRecord & {
36
+ readonly claimToken: string;
37
+ }, observation: import('./host-ledger.js').NativeDeliveryObservation): Promise<boolean>;
35
38
  claimEvidence(i: EvidenceClaim): Promise<ClaimResult>;
36
39
  releaseEvidence(i: EvidenceRelease): Promise<void>;
37
40
  appendEvidence(i: EvidenceRecord & {
@@ -279,6 +279,18 @@ export class FileHostLedgerPort {
279
279
  return current.outcome === 'accepted' ? current : finishWake(current, 'accepted', { signature: 'native_queue_confirmed', message: undefined, diagnostic: undefined });
280
280
  });
281
281
  }
282
+ async observeNativeDelivery(i, observation) {
283
+ return this.updateWake(i, (current) => {
284
+ const native = current.nativeDelivery;
285
+ if (native?.harness !== 'claude' || native.messageId !== observation.messageId)
286
+ return undefined;
287
+ const observations = current.nativeObservations ?? [];
288
+ if (observations.some((prior) => prior.eventId === observation.eventId))
289
+ return current;
290
+ // Native custody is diagnostic evidence, never permission to retry or a queue receipt.
291
+ return { ...current, nativeObservations: [...observations, observation] };
292
+ });
293
+ }
282
294
  async claimEvidence(i) {
283
295
  if (i.kind === 'wake')
284
296
  return { status: 'degraded', error: new Error('wake attempts are claimed with claimWakeAttempt') };
@@ -42,11 +42,20 @@ export type NativeDeliveryEvidence = {
42
42
  } & ({
43
43
  readonly harness: 'claude';
44
44
  readonly endpoint: string;
45
+ readonly messageId?: string;
45
46
  } | {
46
47
  readonly harness: 'opencode';
47
48
  readonly inputId: string;
48
49
  });
50
+ export interface NativeDeliveryObservation {
51
+ readonly eventId: string;
52
+ readonly messageId: string;
53
+ readonly state: 'held' | 'released' | 'dropped';
54
+ readonly reason?: string;
55
+ readonly observedAt: number;
56
+ }
49
57
  export interface EvidenceRecord {
58
+ readonly nativeObservations?: readonly NativeDeliveryObservation[];
50
59
  readonly bindingEpoch?: number;
51
60
  readonly nativeDelivery?: NativeDeliveryEvidence;
52
61
  readonly location: string;
@@ -179,5 +188,8 @@ export interface HostLedgerPort {
179
188
  confirmWakeAdmission(input: EvidenceRecord & {
180
189
  readonly claimToken: string;
181
190
  }): Promise<boolean>;
191
+ observeNativeDelivery(input: EvidenceRecord & {
192
+ readonly claimToken: string;
193
+ }, observation: NativeDeliveryObservation): Promise<boolean>;
182
194
  claimWakeAttempt(input: WakeAttemptClaimInput): Promise<WakeAttemptClaim>;
183
195
  }
package/dist/mcp.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure } from './square-tools.js';
3
+ import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure, squareToolFailureText } from './square-tools.js';
4
4
  import { operationEnv } from './participant-identity.js';
5
5
  import { readClaudeMcpCall } from './claude-mcp-context.js';
6
6
  import { SQUARE_IDENTITY } from './identity.js';
@@ -16,6 +16,7 @@ export function createSquareMcpServer(options) {
16
16
  for (const tool of SQUARE_TOOLS) {
17
17
  server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args, extra) => {
18
18
  const call = (async () => {
19
+ let square;
19
20
  try {
20
21
  const { as: participant, location } = args;
21
22
  const threadId = extra._meta?.threadId;
@@ -33,25 +34,35 @@ export function createSquareMcpServer(options) {
33
34
  : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
34
35
  ? operationEnv('opencode', opencodeSessionId, baseEnv)
35
36
  : baseEnv;
36
- const square = await Square.at({
37
+ square = await Square.at({
37
38
  ...options, env, cwd: claude?.cwd ?? options.cwd,
38
39
  path: location ?? options.path,
39
40
  participant: participant ?? options.participant,
40
41
  // The server owner is bound to its configured square; overrides close after each call.
41
42
  resources: location === undefined && claude === undefined ? resources : undefined,
42
43
  }, { signal: extra.signal });
43
- let structuredContent;
44
+ let receipt;
45
+ // The transport shows people the square, so it asks for the rendering facts
46
+ // the CLI already gathers and keeps that request out of the tool schema.
44
47
  try {
45
- structuredContent = await tool.run(square, args, { signal: extra.signal });
48
+ receipt = await tool.run(square, args, { signal: extra.signal, observe: true });
46
49
  }
47
50
  finally {
48
51
  await square.close();
49
52
  }
50
- return { content: [{ type: 'text', text: JSON.stringify(structuredContent) }], structuredContent };
53
+ // A missing observation renders as nothing, never as raw JSON for a person to read.
54
+ const text = tool.render(receipt, args) ?? '';
55
+ // Observation facts are a rendering aid, not part of the receipt machines receive.
56
+ const { observation: _observation, ...structuredContent } = receipt;
57
+ return { content: [{ type: 'text', text }], structuredContent };
51
58
  }
52
59
  catch (error) {
53
60
  const failure = squareToolFailure(error, extra.signal);
54
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(failure) }], structuredContent: { error: failure } };
61
+ const participant = args.as;
62
+ return { isError: true, content: [{ type: 'text', text: squareToolFailureText(failure, {
63
+ location: square?.location ?? options.path,
64
+ participant: typeof participant === 'string' ? participant : options.participant,
65
+ }) }], structuredContent: { error: failure } };
55
66
  }
56
67
  })();
57
68
  inFlight.add(call);
@@ -0,0 +1,45 @@
1
+ import type { CatchOptions, CatchResult, ExpressResult, HistoryQuery, HistoryResult, JoinResult, LifecycleResult, ListenerChangeResult, ListeningResult, OperationFailureDetail, ParticipantsResult, StatusResult } from './square-facade.js';
2
+ export declare const PARTICIPANTS_DEFAULT_LIMIT = 20;
3
+ export declare const PARTICIPANTS_MAX_LIMIT = 100;
4
+ /** A join receipt whose participant is already reduced to its serializable name. */
5
+ export type JoinPresentation = Omit<JoinResult, 'participant'> & {
6
+ readonly participant: {
7
+ readonly name: string;
8
+ } | string;
9
+ };
10
+ export declare function renderJoinResult(data: JoinPresentation): string | undefined;
11
+ export declare function renderExpression(squarePath: string, data: ExpressResult): string | undefined;
12
+ /** Render settled catch facts; no selection, terminal decisions, receipts or wait machinery. */
13
+ export declare function renderCatchOutcome(squarePath: string, data: CatchResult, options?: CatchOptions): {
14
+ stdout: string;
15
+ exitCode: number;
16
+ };
17
+ export declare function renderHistoryResult(squarePath: string, data: HistoryResult, options?: {
18
+ query?: HistoryQuery;
19
+ noTruncate?: boolean;
20
+ continuationArgs?: readonly string[];
21
+ }): string | undefined;
22
+ export declare function renderParticipantsResult(squarePath: string, data: ParticipantsResult, limit?: number): string | undefined;
23
+ export declare function renderStatusResult(squarePath: string, data: StatusResult): string | undefined;
24
+ export declare function renderListeningResult(squarePath: string, data: ListeningResult): string | undefined;
25
+ export type ListenerVerb = 'listen' | 'ignore' | 'unlisten';
26
+ export declare function renderListenerChange(squarePath: string, verb: ListenerVerb, data: ListenerChangeResult): string | undefined;
27
+ export type LifecycleVerb = 'hold' | 'resume' | 'done';
28
+ export declare function renderLifecycleResult(squarePath: string, verb: LifecycleVerb, data: LifecycleResult): string | undefined;
29
+ /** The refusal facts a transport has after normalizing a failure, without the live error. */
30
+ export interface FailureReport {
31
+ readonly code: string;
32
+ readonly message: string;
33
+ readonly facts?: {
34
+ readonly reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'migration_required' | 'invalid_reply' | 'invalid_done_body';
35
+ readonly activityId?: string;
36
+ readonly ledgerRoot?: string;
37
+ readonly activityExists?: boolean;
38
+ };
39
+ readonly operation?: OperationFailureDetail;
40
+ }
41
+ /** Refusal lines with the same recovery commands the CLI prints. */
42
+ export declare function renderFailureLines(failure: FailureReport, options?: {
43
+ squarePath?: string;
44
+ name?: string;
45
+ }): string[];
@@ -0,0 +1,370 @@
1
+ // Human-facing text for one settled operation. The CLI and the MCP transport both
2
+ // render through these functions, so a person sees the same square either way.
3
+ import { sameName } from './model.js';
4
+ import { formatActivityId, parseActivityId } from './square-core.js';
5
+ import { commandPrefix, joinRecoveryCommand, participantCommandPrefix, participantIdentity, participantsRecoveryCommand, quoteShell, renderAmbientEvent, renderAmbientEventFacts, renderEventCli, renderGrepActivitiesView, renderPendingFeed, renderPresenceAnchor, renderRoomChangeText, renderWatchAlreadyActive, renderWatchReplaced, renderWatchStatus, takeoverRecoveryLines, withPathOutput, } from './presentation.js';
6
+ import { formatRelativeTime, formatTimestamp } from './time.js';
7
+ import { style } from './tty-style.js';
8
+ export const PARTICIPANTS_DEFAULT_LIMIT = 20;
9
+ export const PARTICIPANTS_MAX_LIMIT = 100;
10
+ const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
11
+ export function renderJoinResult(data) {
12
+ const after = data.observation;
13
+ if (after === undefined)
14
+ return undefined;
15
+ const entry = after.entry;
16
+ const name = typeof data.participant === 'string' ? data.participant : data.participant.name;
17
+ const activities = (entry?.recentActivities ?? []).map((event) => renderAmbientEvent(event, name, {
18
+ now: after.now, preview: 200,
19
+ actNumber: event.kind === 'say' ? entry.sayNumbers[event.index] : undefined,
20
+ perception: entry.perceptions[event.index],
21
+ })).filter(Boolean).join('\n\n');
22
+ const fallback = after.automaticDeliveryAvailable ? [] : [
23
+ '', `${participantCommandPrefix(after.location, name)} catch --idle 30m`,
24
+ ' the square has no way to call you — keep this catch open and stay within earshot',
25
+ ];
26
+ const entryLine = data.kind === 'joined' ? '● you stepped into the square'
27
+ : data.kind === 'reconnected' ? '● you are already in the square'
28
+ : `✓ you banished the original ${participantIdentity(name)} — the name is yours`;
29
+ const isRejoin = data.kind !== 'joined';
30
+ const entryScene = isRejoin || entry === undefined ? '' : entry.joinedBefore
31
+ ? '*the flagstones are where you left them. so is your shadow.*' : entry.scene;
32
+ return withPathOutput(after.location, [
33
+ entryLine, '',
34
+ "· carved into the fountain's edge: every word here lands on a real ear — speak when someone needs it.",
35
+ ...(entryScene === '' ? [] : ['', entryScene]),
36
+ ...(isRejoin || !entry?.joinContext ? [] : ['', style('dim', 'context'), entry.joinContext]),
37
+ ...(after.enrichmentUnavailable ? [' · joined; recent activity and context are unavailable right now'] : []),
38
+ ...(isRejoin || activities === '' ? [] : ['', style('dim', 'recent activity'), activities]),
39
+ ...fallback,
40
+ ].join('\n'), { participantCount: after.standingCount, held: after.held });
41
+ }
42
+ function isGestureOnly(body) {
43
+ return /\*[^*]*\*/.test(body) && body.replace(/\*[^*]*\*/g, '').trim() === '';
44
+ }
45
+ export function renderExpression(squarePath, data) {
46
+ const projection = data.observation;
47
+ if (projection === undefined)
48
+ return undefined;
49
+ const name = projection.participant;
50
+ const pendingSpeaker = projection.pendingPublic[0]?.actor;
51
+ const pendingLead = pendingSpeaker === undefined ? ''
52
+ : `▲ while you spoke, ${participantIdentity(pendingSpeaker)} said something behind you\n\n`;
53
+ const pending = projection.pendingPublic.length === 0 ? ''
54
+ : `\n\n${pendingLead}${renderPendingFeed(projection.pendingPublic, projection.sayNumbers, projection.perceptions, name)}`;
55
+ const ownCount = projection.ownActivityCount;
56
+ const capCount = projection.hardCap === null ? '' : ` · ${ownCount}/${projection.hardCap}`;
57
+ const activity = data.activity;
58
+ const confirmation = activity.reach === 'bell'
59
+ ? `● you ring the bell — everyone turns · ${activity.id}${style('dim', capCount)}`
60
+ : `● ${activity.mentions.length > 0
61
+ ? `you walk over to ${activity.mentions.map((target) => participantIdentity(target)).join(', ')}`
62
+ : isGestureOnly(activity.body ?? '') ? 'your gesture lands' : 'your words land'} — ${activity.id}${style('dim', capCount)}`;
63
+ const replyNotice = data.replyAuthorNotHere === undefined ? ''
64
+ : `\n○ ${participantIdentity(data.replyAuthorNotHere)} isn't in the square right now — the reply landed without reaching them`;
65
+ const unmatchedNotice = data.unmatchedMentions?.length
66
+ ? `\n · ${data.unmatchedMentions.map((token) => `@${token} isn't standing here`).join('; ')} — ${activity.mentions.length === 0 && activity.reply === undefined ? 'activity landed bare' : 'left as literal text'}` : '';
67
+ return withPathOutput(projection.location ?? squarePath, (projection.reentry === undefined ? '' : '● you stepped back into the square\n')
68
+ + confirmation + (data.hint ? `\n${style('dim', `· ${data.hint}`)}` : '') + replyNotice + unmatchedNotice + pending, { participantCount: projection.standingCount, held: projection.held });
69
+ }
70
+ function continuationCommand(squarePath, name, options) {
71
+ const args = ['--now'];
72
+ if (options.from?.length)
73
+ args.push('--from', options.from.join(','));
74
+ if (options.mention)
75
+ args.push('--mention');
76
+ if (options.limit !== undefined)
77
+ args.push('--limit', String(options.limit));
78
+ return `${participantCommandPrefix(squarePath, name)} catch ${args.join(' ')}`;
79
+ }
80
+ /** Render settled catch facts; no selection, terminal decisions, receipts or wait machinery. */
81
+ export function renderCatchOutcome(squarePath, data, options = {}) {
82
+ const projection = data.observation;
83
+ if (projection === undefined)
84
+ return { stdout: '', exitCode: 0 };
85
+ const location = projection.location ?? squarePath;
86
+ const name = projection.participant;
87
+ const stop = projection.stop ?? data.status;
88
+ const header = { participantCount: projection.standingCount, held: projection.held };
89
+ const statusOptions = {
90
+ squarePath: location, name, presence: projection.presence,
91
+ showCatchHint: !projection.automaticDeliveryAvailable,
92
+ ownActivityCount: projection.ownActivityCount, hardCap: projection.hardCap,
93
+ };
94
+ if (stop === 'already-active' || stop === 'replaced') {
95
+ return {
96
+ stdout: withPathOutput(location, stop === 'already-active'
97
+ ? renderWatchAlreadyActive(statusOptions) : renderWatchReplaced(statusOptions), header),
98
+ exitCode: stop === 'already-active' ? 1 : 0,
99
+ };
100
+ }
101
+ const rendered = data.activities.map((activity) => renderAmbientEvent(activity, name, {
102
+ now: projection.now, actNumber: projection.sayNumbers[parseActivityId(activity.id)],
103
+ perception: activity.perception, mention: options.mention ? name : undefined,
104
+ })).filter(Boolean).join('\n\n');
105
+ const remaining = data.remaining === 0 ? ''
106
+ : `○ ${data.remaining} matching ${data.remaining === 1 ? 'activity remains' : 'activities remain'}\n${continuationCommand(location, name, options)}`;
107
+ const terminal = stop === undefined ? '' : renderWatchStatus({
108
+ ...statusOptions, status: stop,
109
+ ...(stop === 'stale' && options.idle !== undefined ? { idleMs: options.idle } : {}),
110
+ });
111
+ const fallback = stop === undefined && !projection.automaticDeliveryAvailable && data.remaining === 0
112
+ ? `${participantCommandPrefix(location, name)} catch --idle 30m\n stay available for new activity` : '';
113
+ const hint = data.hint === undefined ? '' : style('dim', `· ${data.hint}`);
114
+ return {
115
+ stdout: withPathOutput(location, [terminal, rendered, hint, remaining, fallback].filter(Boolean).join('\n\n'), header),
116
+ exitCode: stop === 'capped' ? 1 : 0,
117
+ };
118
+ }
119
+ function renderHistoryProjection(projection, visible, noTruncate, squarePath) {
120
+ const shown = visible;
121
+ const preview = noTruncate || shown.length <= 1 ? undefined : 200;
122
+ const chunks = [];
123
+ // Presence anchors collect into one closing line instead of interrupting the page.
124
+ const anchored = [];
125
+ for (const activity of shown) {
126
+ const options = {
127
+ preview,
128
+ actNumber: activity.kind === 'say' ? projection.sayNumbers[activity.index] : undefined,
129
+ };
130
+ const rendered = renderEventCli(activity, options);
131
+ if (rendered !== '')
132
+ chunks.push(rendered);
133
+ for (const participant of projection.presenceAnchors[activity.index] ?? []) {
134
+ if (!anchored.some((existing) => sameName(existing, participant)))
135
+ anchored.push(participant);
136
+ }
137
+ }
138
+ if (chunks.length === 0)
139
+ return 'latest\n ○ no public activity in this view';
140
+ if (preview !== undefined && shown.some((activity) => activity.kind === 'say' && activity.body.length > preview)) {
141
+ chunks.push(`${commandPrefix(squarePath)} history --no-truncate`);
142
+ }
143
+ if (anchored.length > 0)
144
+ chunks.push(renderPresenceAnchor(anchored));
145
+ return chunks.join('\n\n');
146
+ }
147
+ function historyContinuationCommand(squarePath, args) {
148
+ return `${commandPrefix(squarePath)} history ${args.map((arg) => arg.startsWith('-') || /^act\/\d+$/.test(arg) || /^\d+$/.test(arg) ? arg : quoteShell(arg)).join(' ')}`;
149
+ }
150
+ /** The same query spelled as CLI arguments, for a continuation someone can actually run. */
151
+ function historyQueryArgs(query) {
152
+ const args = [];
153
+ if (query.from?.length)
154
+ args.push('--from', query.from.join(','));
155
+ if (query.since !== undefined)
156
+ args.push('--since', formatTimestamp(query.since));
157
+ if (query.at?.length)
158
+ args.push('--at', query.at.join(','));
159
+ if (query.contextBefore !== undefined)
160
+ args.push('-B', String(query.contextBefore));
161
+ if (query.contextAfter !== undefined)
162
+ args.push('-A', String(query.contextAfter));
163
+ if (query.mention !== undefined)
164
+ args.push('--mention', query.mention);
165
+ if (query.grep !== undefined)
166
+ args.push('--grep', query.grep);
167
+ if (query.fixed !== undefined)
168
+ args.push('--fixed', query.fixed);
169
+ if (query.order !== undefined)
170
+ args.push('--order', query.order);
171
+ return args;
172
+ }
173
+ export function renderHistoryResult(squarePath, data, options = {}) {
174
+ const projection = data.observation;
175
+ if (projection === undefined)
176
+ return undefined;
177
+ const query = options.query ?? {};
178
+ const noTruncate = options.noTruncate === true;
179
+ const pattern = query.grep ?? query.fixed;
180
+ const output = pattern === undefined || pattern === ''
181
+ ? renderHistoryProjection(projection, projection.visibleActivities, noTruncate, squarePath)
182
+ : renderGrepActivitiesView(projection.visibleActivities, projection.totalMatches, noTruncate, squarePath, pattern, query.fixed !== undefined, () => 'full');
183
+ const next = projection.continuation;
184
+ const base = options.continuationArgs ?? historyQueryArgs(query);
185
+ const continuation = next === undefined ? '' : `\n\n${historyContinuationCommand(squarePath, [...base, `--${next.direction}`, next.cursor, '--limit', String(next.limit)])}`;
186
+ return withPathOutput(squarePath, output + continuation, { participantCount: projection.standingCount });
187
+ }
188
+ function participantsLimitCommand(squarePath, limit) {
189
+ return `${commandPrefix(squarePath)} participants --limit ${limit}`;
190
+ }
191
+ export function renderParticipantsResult(squarePath, data, limit = PARTICIPANTS_DEFAULT_LIMIT) {
192
+ const projection = data.observation;
193
+ if (projection === undefined)
194
+ return undefined;
195
+ const now = projection.now;
196
+ const participants = data.participants;
197
+ const lines = participants.slice(0, limit).map((participant) => {
198
+ const glyph = participant.state === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 && projection.classificationByName[participant.name] === 'here' ? '●' : '○';
199
+ const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
200
+ const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
201
+ return ` ${glyph} ${participant.name}${style('dim', ` · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`)}`;
202
+ });
203
+ const tail = participants.length <= limit
204
+ ? []
205
+ : [
206
+ style('dim', ` ○ ${lines.length} of ${participants.length} participants shown`),
207
+ ...(participants.length <= PARTICIPANTS_MAX_LIMIT ? [
208
+ `${participantsLimitCommand(squarePath, participants.length)}`,
209
+ ] : []),
210
+ ];
211
+ return withPathOutput(squarePath, ['participants', ...lines, ...tail].join('\n'), {
212
+ participantCount: projection.standingCount,
213
+ });
214
+ }
215
+ export function renderStatusResult(squarePath, data) {
216
+ const presentation = data.observation;
217
+ if (presentation === undefined)
218
+ return undefined;
219
+ const participantsByName = new Map(data.participants.map((participant) => [participant.name, participant]));
220
+ const orderedHere = presentation.orderedHereNames.map((name) => participantsByName.get(name));
221
+ const lingering = presentation.lingeringCount;
222
+ const people = [];
223
+ if (data.activeCount === 0) {
224
+ people.push(' ○ nobody in the square');
225
+ }
226
+ else {
227
+ if (orderedHere.length === 0)
228
+ people.push(' ○ nobody here right now');
229
+ people.push(...orderedHere.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map((participant) => {
230
+ const glyph = participant.presence === 'watching'
231
+ ? '◎'
232
+ : participant.activityCount > 0 ? '●' : '○';
233
+ const summary = participant.presence === 'watching'
234
+ ? 'is nearby · catching'
235
+ : participant.activityCount > 0
236
+ ? `is nearby · ${formatRelativeTime(participant.lastActiveAt ?? data.now, data.now)}`
237
+ : `is nearby · quiet · ${participant.lastActiveAt === undefined ? 'just now' : formatRelativeTime(participant.lastActiveAt, data.now)}`;
238
+ const showAttention = presentation.attentionNames.includes(participant.name);
239
+ const attention = !showAttention
240
+ ? ''
241
+ : participant.pendingMentionCount > 0
242
+ ? `${participant.pendingMentionCount} ${participant.pendingMentionCount === 1 ? 'mention' : 'mentions'} waiting`
243
+ : participant.unreadActivityCount > 0
244
+ ? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
245
+ : 'caught up';
246
+ return ` ${glyph} ${participantIdentity(participant.name)}${style('dim', ` ${summary}${attention === '' ? '' : ` · ${attention}`}`)}`;
247
+ }));
248
+ const unshown = Math.max(0, orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT) + lingering;
249
+ if (orderedHere.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
250
+ people.push(` ○ … ${orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more here`);
251
+ }
252
+ if (lingering > 0)
253
+ people.push(` ○ … ${lingering} lingering`);
254
+ if (unshown > 0) {
255
+ people.push(`${presentation.knownParticipantCount <= PARTICIPANTS_MAX_LIMIT
256
+ ? participantsLimitCommand(squarePath, presentation.knownParticipantCount)
257
+ : `${commandPrefix(squarePath)} participants`}`);
258
+ }
259
+ }
260
+ const cap = data.hardCap === null ? 'unlimited' : `${data.hardCap} each`;
261
+ const holdTime = data.hold?.at === undefined ? 'just now' : formatRelativeTime(data.hold.at, data.now);
262
+ const hold = data.hold !== null
263
+ ? `· ${data.hold.by === undefined ? 'someone' : participantIdentity(data.hold.by)} raised a hand${data.hold.reason ? ` — ${data.hold.reason}` : ''}${style('dim', ` · ${holdTime}`)}`
264
+ : undefined;
265
+ const visible = data.latestActivity === undefined
266
+ ? { text: '', bodyClipped: false }
267
+ : renderAmbientEventFacts(data.latestActivity, presentation.viewer, {
268
+ now: data.now,
269
+ preview: 200,
270
+ actNumber: presentation.latestActNumber,
271
+ perception: presentation.latestPerception,
272
+ });
273
+ const latest = visible.text === ''
274
+ ? [data.latestActivity === undefined
275
+ ? ' ○ no public activity yet'
276
+ : ' · the latest words were meant for other ears']
277
+ : [` ${visible.text.replace(/\n/g, '\n ')}`];
278
+ // Presence events after the latest public act: otherwise the roster can say someone is
279
+ // here while the latest line shows them leaving, with nothing to reconcile the two.
280
+ for (const { activity, rejoined } of presentation.joins) {
281
+ latest.push(style('dim', ` → ${participantIdentity(activity.actor)} ${rejoined ? 'stepped back in' : 'stepped in'} · ${formatRelativeTime(activity.at, data.now)}`));
282
+ }
283
+ if (presentation.joinCount > 3)
284
+ latest.push(style('dim', ` → … ${presentation.joinCount - 3} more stepped in`));
285
+ if (visible.bodyClipped && data.latestActivity !== undefined) {
286
+ latest.push(`${commandPrefix(squarePath)} history --at ${data.latestActivity.id} -C 2 --no-truncate`);
287
+ }
288
+ const counts = [
289
+ `${orderedHere.length} here`,
290
+ ...(lingering > 0 ? [`${lingering} lingering`] : []),
291
+ `${data.doneCount} done`,
292
+ `cap ${cap}`,
293
+ `throttle ${data.throttlePerMinute === undefined ? 'none' : `${data.throttlePerMinute}/min`}`,
294
+ ].join(' · ');
295
+ const output = [
296
+ style('dim', counts),
297
+ ...(hold === undefined ? [] : ['', hold]), '', style('dim', 'around the square'), ...people, '', style('dim', 'latest'), ...latest,
298
+ ].join('\n');
299
+ return withPathOutput(squarePath, output, { participantCount: presentation.standingCount, held: presentation.held });
300
+ }
301
+ export function renderListeningResult(squarePath, data) {
302
+ const projection = data.observation;
303
+ if (projection?.participant === undefined)
304
+ return undefined;
305
+ const body = data.listening.length === 0
306
+ ? `○ ${participantIdentity(projection.participant)} is not turned toward anyone`
307
+ : ['listening', ...data.listening.map((target) => ` · ${participantIdentity(target)}`)].join('\n');
308
+ return withPathOutput(squarePath, body, { participantCount: projection.standingCount, held: projection.held });
309
+ }
310
+ export function renderListenerChange(squarePath, verb, data) {
311
+ const projection = data.observation;
312
+ if (projection?.participant === undefined)
313
+ return undefined;
314
+ const actor = participantIdentity(projection.participant);
315
+ const target = participantIdentity(projection.target);
316
+ const action = data.activity !== null
317
+ ? verb === 'listen' ? `${actor} turns an ear toward ${target}`
318
+ : verb === 'ignore' ? `${actor} turns away from ${target}`
319
+ : `${actor} stops listening to ${target}`
320
+ : verb === 'listen' ? `${actor} already turns an ear toward ${target}`
321
+ : verb === 'ignore' ? `${actor} is not turned toward ${target}`
322
+ : `${actor} is not listening to ${target}`;
323
+ return withPathOutput(squarePath, `${data.activity === null ? '○' : '·'} ${action}`, {
324
+ participantCount: projection.standingCount, held: projection.held,
325
+ });
326
+ }
327
+ export function renderLifecycleResult(squarePath, verb, data) {
328
+ const projection = data.observation;
329
+ if (projection === undefined)
330
+ return undefined;
331
+ const header = { participantCount: projection.standingCount, held: projection.held };
332
+ if (verb === 'done') {
333
+ return withPathOutput(projection.location ?? squarePath, `○ you stepped out — your footsteps fade · ${data.activity.id}`, header);
334
+ }
335
+ const line = `${renderRoomChangeText(projection.activity)} · ${formatActivityId(projection.activity.index)} · just now`;
336
+ return withPathOutput(projection.location ?? squarePath, verb === 'hold' ? `· ${line}` : `✓ ${line}`, header);
337
+ }
338
+ /** Refusal lines with the same recovery commands the CLI prints. */
339
+ export function renderFailureLines(failure, options = {}) {
340
+ const { squarePath, name } = options;
341
+ const facts = failure.facts;
342
+ const reason = facts?.reason;
343
+ const marked = reason === 'never_joined' || reason === 'not_standing';
344
+ const lines = marked ? failure.message.split('\n').map((line) => `✕ ${line}`) : [failure.message];
345
+ if (reason === 'migration_required') {
346
+ if (facts?.ledgerRoot !== undefined)
347
+ lines.push('square ledger migrate', 'square ledger verify');
348
+ else if (squarePath !== undefined)
349
+ lines.push(`${commandPrefix(squarePath)} migrate`);
350
+ }
351
+ else if (squarePath !== undefined && name !== undefined && failure.code === 'already_joined') {
352
+ lines.splice(0, 1, `✕ ${participantIdentity(name)} shoos you out of the square`, ` · ${participantIdentity(name)} already stands here — another session holds the name`);
353
+ lines.push(...takeoverRecoveryLines(squarePath, name));
354
+ }
355
+ else if (squarePath !== undefined && reason === 'never_joined') {
356
+ lines.push(failure.operation?.subject !== 'filter' && name !== undefined
357
+ ? joinRecoveryCommand(squarePath, name) : participantsRecoveryCommand(squarePath));
358
+ }
359
+ else if (squarePath !== undefined && reason === 'not_standing') {
360
+ lines.push(participantsRecoveryCommand(squarePath));
361
+ }
362
+ else if (squarePath !== undefined && reason === 'not_addressed' && facts?.activityId !== undefined) {
363
+ lines.push(facts.activityExists === true
364
+ ? `${commandPrefix(squarePath)} history --at ${facts.activityId} -C 2`
365
+ : `${commandPrefix(squarePath)} history --limit 10`);
366
+ }
367
+ if (reason === 'invalid_done_body')
368
+ lines.push('square done --help');
369
+ return lines;
370
+ }
@@ -32,9 +32,11 @@ export async function writeClaudeNative(target, text, control) {
32
32
  try {
33
33
  scope.check();
34
34
  const token = await scope.wait(resolveClaudeToken({ endpoint, token: target.token, claudeHome: target.claudeHome, env: target.env, platform: target.platform }, scope.signal));
35
+ const messageId = target.messageId ?? randomUUID();
35
36
  const frames = [
36
37
  ...(token === undefined ? [] : [{ type: 'auth', token }]),
37
- { msgV: 1, msg_id: target.messageId ?? randomUUID(), type: 'user', session_id: target.sessionId, message: { role: 'user', content: text }, priority: 'next' },
38
+ // Claude's SDK custody events echo uuid, not the peer-protocol msg_id.
39
+ { msgV: 1, msg_id: messageId, uuid: messageId, type: 'user', session_id: target.sessionId, message: { role: 'user', content: text }, priority: 'next' },
38
40
  ];
39
41
  scope.check();
40
42
  if (Date.now() >= control.deadline)
package/dist/routes.js CHANGED
@@ -19,10 +19,10 @@ export async function defaultWakeRouteCapabilities(hostLedger) {
19
19
  export function resolvePrimaryWakeRoute(boundary, env, capabilities) {
20
20
  const paseoAgentId = env.PASEO_AGENT_ID?.trim();
21
21
  const candidates = [];
22
- if (paseoAgentId)
23
- candidates.push({ kind: 'paseo', address: { agentId: paseoAgentId } });
24
22
  if (boundary.provider === 'codex')
25
23
  candidates.push({ kind: 'codex-queue', address: { threadId: boundary.sessionId } });
24
+ if (paseoAgentId)
25
+ candidates.push({ kind: 'paseo', address: { agentId: paseoAgentId } });
26
26
  const chosen = candidates.find((candidate) => Object.values(candidate.address).every((value) => value.trim() !== '') && capabilities.canUse(candidate.kind, candidate.address));
27
27
  return chosen === undefined ? undefined : { location: boundary.location, participant: boundary.participant, sessionId: boundary.sessionId, channel: boundary.provider === 'paseo' ? 'paseo' : boundary.provider === 'claude' ? 'claude-code' : boundary.provider, ...chosen };
28
28
  }
@@ -80,6 +80,11 @@ export async function publishWakeRoute(artifact, route, opts = {}) {
80
80
  return;
81
81
  const identity = routeIdentityKey(route, location);
82
82
  const matching = routes.filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) === identity);
83
+ // A receiver's fresh native registration outranks a generic Paseo proposal.
84
+ // Arbitrate in the write transaction so tool publication and mod reconciliation cannot flip it back.
85
+ if (route.kind === 'paseo' && matching.some((item) => item.kind !== 'paseo' && item.channel === route.channel
86
+ && item.epoch === route.epoch && item.updatedAt <= at && item.updatedAt > at - ROUTE_FRESH_MS))
87
+ return;
83
88
  const existing = matching[0];
84
89
  if (matching.length === 1 && existing.updatedAt > at - ROUTE_FRESH_MS && existing.kind === route.kind
85
90
  && existing.channel === route.channel && sameRouteAddress(existing.address, route.address) && existing.epoch === route.epoch)