@astrosheep/square 0.4.4 → 0.4.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 (100) hide show
  1. package/dist/artifact-store.d.ts +1 -1
  2. package/dist/artifact-store.js +32 -15
  3. package/dist/artifact.d.ts +1 -4
  4. package/dist/artifact.js +0 -28
  5. package/dist/attention-presentation.js +1 -1
  6. package/dist/boundary-presentation.js +2 -2
  7. package/dist/claude-boundary-state.d.ts +6 -6
  8. package/dist/claude-boundary-state.js +13 -13
  9. package/dist/claude-hook.js +25 -24
  10. package/dist/claude-mcp-context.d.ts +2 -2
  11. package/dist/claude-mcp-context.js +6 -6
  12. package/dist/cli/context.js +3 -6
  13. package/dist/cli/json.js +1 -1
  14. package/dist/cli/mcp-server-command.js +3 -3
  15. package/dist/cli/observation-commands.d.ts +2 -2
  16. package/dist/cli/observation-commands.js +23 -51
  17. package/dist/cli/perceive-presentation.d.ts +3 -0
  18. package/dist/cli/{catch-presentation.js → perceive-presentation.js} +3 -3
  19. package/dist/cli/registry.js +2 -2
  20. package/dist/codex-address.d.ts +3 -0
  21. package/dist/codex-address.js +7 -0
  22. package/dist/codex-hook.d.ts +1 -1
  23. package/dist/codex-hook.js +11 -8
  24. package/dist/codex-queue.d.ts +3 -2
  25. package/dist/codex-queue.js +8 -11
  26. package/dist/codex-watch.d.ts +6 -1
  27. package/dist/codex-watch.js +27 -29
  28. package/dist/delivery-ledger-sqlite.d.ts +25 -20
  29. package/dist/delivery-ledger-sqlite.js +76 -61
  30. package/dist/delivery-ledger.d.ts +24 -19
  31. package/dist/delivery-ledger.js +150 -81
  32. package/dist/delivery-lifetime.d.ts +2 -2
  33. package/dist/delivery-lifetime.js +6 -18
  34. package/dist/help.js +12 -12
  35. package/dist/hints.d.ts +2 -2
  36. package/dist/hints.js +7 -7
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/list.d.ts +2 -2
  40. package/dist/list.js +4 -3
  41. package/dist/mcp.d.ts +2 -2
  42. package/dist/mcp.js +8 -32
  43. package/dist/neutral.d.ts +5 -5
  44. package/dist/notifications.js +12 -6
  45. package/dist/operation-presentation.d.ts +8 -3
  46. package/dist/operation-presentation.js +50 -12
  47. package/dist/packages/agent-delivery/src/file-lock.js +4 -1
  48. package/dist/perceive-decisions.d.ts +28 -0
  49. package/dist/{catch-decisions.js → perceive-decisions.js} +24 -24
  50. package/dist/{catch-selection.d.ts → perceive-selection.d.ts} +2 -2
  51. package/dist/{catch-selection.js → perceive-selection.js} +3 -3
  52. package/dist/plugins/opencode/index.js +4 -4
  53. package/dist/plugins/opencode-shared/delivery.d.ts +6 -6
  54. package/dist/plugins/opencode-shared/delivery.js +27 -21
  55. package/dist/plugins/opencode-v1/runtime.js +3 -3
  56. package/dist/ports.d.ts +0 -4
  57. package/dist/presentation.d.ts +1 -1
  58. package/dist/presentation.js +13 -13
  59. package/dist/session-lifecycle.d.ts +2 -2
  60. package/dist/session-lifecycle.js +11 -11
  61. package/dist/sqlite-square.d.ts +2 -4
  62. package/dist/sqlite-square.js +47 -54
  63. package/dist/sqlite-transaction.d.ts +2 -0
  64. package/dist/sqlite-transaction.js +25 -3
  65. package/dist/square-api.d.ts +1 -1
  66. package/dist/square-api.js +1 -1
  67. package/dist/square-domain.js +1 -1
  68. package/dist/square-facade.d.ts +15 -13
  69. package/dist/square-location.js +1 -1
  70. package/dist/square-posting.d.ts +5 -5
  71. package/dist/square-posting.js +39 -38
  72. package/dist/square-tools.d.ts +15 -4
  73. package/dist/square-tools.js +35 -17
  74. package/dist/views.d.ts +7 -3
  75. package/dist/views.js +6 -3
  76. package/guides/architect.md +2 -2
  77. package/guides/change-waits.md +1 -1
  78. package/guides/codex-delivery.md +40 -35
  79. package/guides/delivery-primitives.md +22 -7
  80. package/guides/mcp.md +4 -2
  81. package/guides/operation-cancellation.md +11 -3
  82. package/guides/pi-native-delivery.md +1 -1
  83. package/guides/pi-tools.md +11 -6
  84. package/guides/receive-primitive.md +3 -3
  85. package/guides/square-api.md +15 -8
  86. package/package.json +2 -2
  87. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  88. package/plugins/claude/skills/square/SKILL.md +10 -10
  89. package/plugins/codex/.codex-plugin/plugin.json +2 -2
  90. package/plugins/codex/hooks/hooks.json +3 -0
  91. package/plugins/opencode/index.ts +4 -4
  92. package/plugins/opencode-shared/delivery.ts +27 -21
  93. package/plugins/opencode-v1/runtime.ts +3 -3
  94. package/plugins/paseo/client/markdown.tsx +9 -26
  95. package/plugins/paseo/client/square-message.tsx +10 -14
  96. package/plugins/pi/square-pi.js +100 -20
  97. package/skills/square/SKILL.md +10 -10
  98. package/dist/catch-decisions.d.ts +0 -28
  99. package/dist/cli/catch-presentation.d.ts +0 -3
  100. package/guides/mcp-connection-lifetime.md +0 -22
package/dist/mcp.js CHANGED
@@ -5,23 +5,20 @@ import { operationEnv } from './participant-identity.js';
5
5
  import { codexWakeAddress } from './codex-address.js';
6
6
  import { readClaudeMcpCall } from './claude-mcp-context.js';
7
7
  import { SQUARE_IDENTITY } from './identity.js';
8
- import { Square, createSquareResourceOwner } from './square-api.js';
8
+ import { Square, discover, squareClock } from './square-api.js';
9
9
  import { harnessOwner } from './square-api.js';
10
10
  export function createSquareMcpServer(options) {
11
11
  const { owner: configuredOwner, wakeAddress, ...resourceOptions } = options;
12
12
  const baseEnv = { ...(resourceOptions.env ?? process.env) };
13
13
  const toolsEnabled = squareToolsEnabled(baseEnv);
14
- const resources = createSquareResourceOwner();
15
14
  const invoke = createSquareToolRunner();
16
- // Calls to the configured square borrow one server-owned connection.
17
- const inFlight = new Set();
18
15
  const server = new McpServer({ name: 'square', version: SQUARE_IDENTITY.packageVersion }, {
19
16
  ...(toolsEnabled ? { instructions: SQUARE_TOOL_INSTRUCTIONS } : {}),
20
17
  });
21
18
  if (toolsEnabled)
22
19
  for (const tool of SQUARE_TOOLS) {
23
20
  server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args, extra) => {
24
- const call = invoke(tool, args, { signal: extra.signal }, async () => {
21
+ return invoke(tool, args, { signal: extra.signal }, async () => {
25
22
  const threadId = extra._meta?.threadId;
26
23
  const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
27
24
  // Claude's bundled plugin requires a per-call native hook association; startup identity is not a fallback.
@@ -51,38 +48,17 @@ export function createSquareMcpServer(options) {
51
48
  return {
52
49
  identity: { owner, wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
53
50
  ? { ...codexWakeAddress(threadId, env), kind: 'codex-queue' } : callWake },
54
- open: (location, control) => Square.at({
55
- ...resourceOptions, env, cwd: claude?.cwd ?? resourceOptions.cwd,
56
- path: location ?? options.path,
57
- // Overrides and Claude calls own their connections.
58
- resources: location === undefined && claude === undefined ? resources : undefined,
59
- }, control),
51
+ open: (location, control) => Square.at({ ...resourceOptions, env, cwd: claude?.cwd ?? resourceOptions.cwd, path: location }, control),
52
+ discover: async (discovery, control) => ({
53
+ result: await discover({ ...resourceOptions, env, cwd: claude?.cwd ?? resourceOptions.cwd }, discovery, control),
54
+ now: squareClock(env, resourceOptions.clock)(),
55
+ }),
60
56
  };
61
- }, options.path).then(({ text, isError }) => ({
57
+ }).then(({ text, isError }) => ({
62
58
  ...(isError ? { isError } : {}), content: [{ type: 'text', text }],
63
59
  }));
64
- inFlight.add(call);
65
- return call.finally(() => { inFlight.delete(call); });
66
60
  });
67
61
  }
68
- const close = server.close.bind(server);
69
- // One idempotent shutdown for both close paths: an explicit server.close() and a
70
- // transport-initiated disconnect, where the SDK protocol fires onclose by itself.
71
- let draining;
72
- const drain = () => draining ??= (async () => {
73
- // The SDK aborts in-flight request signals before onclose/close returns; wait for
74
- // those handlers to release the shared connection (their cleanup writes included).
75
- await Promise.allSettled([...inFlight]);
76
- await resources.close();
77
- })();
78
- server.close = async () => { await close(); await drain(); };
79
- const priorOnclose = server.server.onclose;
80
- server.server.onclose = () => {
81
- // onclose is synchronous, so the disconnect path starts the drain without awaiting it;
82
- // the handler's own catch keeps an unexpected close failure from crashing the process.
83
- void drain().catch(() => undefined);
84
- priorOnclose?.();
85
- };
86
62
  return server;
87
63
  }
88
64
  export async function runSquareMcpStdio(options) {
package/dist/neutral.d.ts CHANGED
@@ -9,7 +9,7 @@ export interface SquareErrorFacts {
9
9
  activityUnsent?: true;
10
10
  /** Refusal reason, including absent participants, conflicting reach, and inaccessible activities. */
11
11
  reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'invalid_reply' | 'invalid_done_body' | 'needs_seat';
12
- /** The activity id a `catch --id` refusal named. */
12
+ /** The activity id a `perceive --id` refusal named. */
13
13
  activityId?: string;
14
14
  /** Requested recipient spellings, without parsing presentation copy. */
15
15
  targets?: readonly string[];
@@ -18,7 +18,7 @@ export interface SquareErrorFacts {
18
18
  pending?: number;
19
19
  retryAfterMs?: number;
20
20
  }
21
- export interface CatchWaitStart {
21
+ export interface PerceiveWaitStart {
22
22
  readonly idleMs: number;
23
23
  readonly replacementRequested: boolean;
24
24
  readonly replaced: boolean;
@@ -28,15 +28,15 @@ export type OperationProgress = {
28
28
  readonly reason: 'throttled';
29
29
  readonly delayMs?: number;
30
30
  } | ({
31
- readonly kind: 'catch-started';
31
+ readonly kind: 'perceive-started';
32
32
  readonly participant: string;
33
- } & CatchWaitStart);
33
+ } & PerceiveWaitStart);
34
34
  export interface OperationControl {
35
35
  readonly signal?: AbortSignal;
36
36
  readonly onProgress?: (progress: OperationProgress) => void;
37
37
  }
38
38
  export interface OperationFailureDetail {
39
- readonly operation: 'express' | 'join' | 'catch' | 'history' | 'other';
39
+ readonly operation: 'express' | 'join' | 'perceive' | 'history' | 'other';
40
40
  readonly subject?: 'caller' | 'filter' | 'recipient';
41
41
  readonly participant?: string;
42
42
  readonly expressEffect?: 'unsent' | 'landed' | 'unknown';
@@ -1,17 +1,23 @@
1
1
  import { createDefaultDeliveryTransports } from './wake-transports.js';
2
2
  export { createDefaultDeliveryTransports } from './wake-transports.js';
3
+ function deliveryWaitMs(env = process.env) {
4
+ const configured = Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000);
5
+ return Number.isSafeInteger(configured) && configured > 0 ? configured : 5000;
6
+ }
3
7
  /** Best-effort dispatch for currently eligible cards, including cards restored by release. */
4
8
  export async function dispatchEligibleCards(ledger, owner, options = {}) {
5
- const session = await ledger.session(owner);
9
+ options = { ...options, signal: options.signal ?? AbortSignal.timeout(deliveryWaitMs(options.env)) };
10
+ const session = await ledger.session(owner, options.signal);
6
11
  if (!session || session.state !== 'attached' || options.signal?.aborted)
7
12
  return;
8
- const pending = await ledger.pending(owner, { generation: session.generation });
13
+ const pending = await ledger.pending(owner, { generation: session.generation }, options.signal);
9
14
  for (const card of pending)
10
15
  await dispatchPostedCard(ledger, owner, card.key, options.track ?? (() => undefined), options.env, options.signal, options.transports);
11
16
  }
12
17
  /** Post, then best-effort dispatch without consulting producer storage. */
13
18
  export async function postAndDispatch(ledger, input, options = {}) {
14
- const result = await ledger.post(input);
19
+ options = { ...options, signal: options.signal ?? AbortSignal.timeout(deliveryWaitMs(options.env)) };
20
+ const result = await ledger.post(input, options.signal);
15
21
  if (result.status === 'posted' || result.status === 'duplicate') {
16
22
  try {
17
23
  await dispatchEligibleCards(ledger, input.owner, options);
@@ -24,12 +30,12 @@ export async function postAndDispatch(ledger, input, options = {}) {
24
30
  export async function dispatchPostedCard(ledger, owner, key, track, env = process.env, signal, suppliedTransports) {
25
31
  if (signal?.aborted)
26
32
  return;
27
- const session = await ledger.session(owner);
33
+ const timeoutMs = deliveryWaitMs(env);
34
+ signal ??= AbortSignal.timeout(timeoutMs);
35
+ const session = await ledger.session(owner, signal);
28
36
  if (!session || session.state !== 'attached')
29
37
  return;
30
38
  const transports = suppliedTransports ?? await createDefaultDeliveryTransports(env);
31
- const configured = Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000);
32
- const timeoutMs = Number.isSafeInteger(configured) && configured > 0 ? configured : 5000;
33
39
  for (const route of session.routes) {
34
40
  // Claude and Codex custody belongs to their native receivers, including for
35
41
  // callers that supply transports explicitly to the producer API.
@@ -1,4 +1,9 @@
1
- import type { CatchOptions, CatchResult, ExpressResult, HistoryQuery, HistoryResult, JoinResult, LifecycleResult, ListenerChangeResult, ListeningResult, OperationFailureDetail, ParticipantsResult, StatusResult } from './square-facade.js';
1
+ import type { PerceiveOptions, PerceiveResult, ExpressResult, HistoryQuery, HistoryResult, JoinResult, LifecycleResult, ListenerChangeResult, ListeningResult, OperationFailureDetail, ParticipantsResult, StatusResult, DiscoveryResult } from './square-facade.js';
2
+ export declare function renderSquareList(data: DiscoveryResult, projection: {
3
+ now: number;
4
+ depth: number;
5
+ limit: number;
6
+ }): string;
2
7
  export declare const PARTICIPANTS_DEFAULT_LIMIT = 20;
3
8
  export declare const PARTICIPANTS_MAX_LIMIT = 100;
4
9
  /** A join receipt whose participant is already reduced to its serializable name. */
@@ -9,8 +14,8 @@ export type JoinPresentation = Omit<JoinResult, 'participant'> & {
9
14
  };
10
15
  export declare function renderJoinResult(data: JoinPresentation, owner?: string): string | undefined;
11
16
  export declare function renderExpression(squarePath: string, data: ExpressResult, owner?: string): 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, owner?: string): {
17
+ /** Render settled perceive facts; no selection, terminal decisions, receipts or wait machinery. */
18
+ export declare function renderPerceiveOutcome(squarePath: string, data: PerceiveResult, options?: PerceiveOptions, owner?: string): {
14
19
  stdout: string;
15
20
  exitCode: number;
16
21
  };
@@ -2,9 +2,47 @@
2
2
  // render through these functions, so a person sees the same square either way.
3
3
  import { sameName } from './model.js';
4
4
  import { formatActivityId } from './square-core.js';
5
- import { commandPrefix, joinRecoveryCommand, participantCommandPrefix, participantIdentity, participantsRecoveryCommand, quoteShell, renderAmbientEvent, renderAmbientEventFacts, renderEventCli, renderGrepActivitiesView, renderPresenceAnchor, renderWatchAlreadyActive, renderWatchReplaced, renderWatchStatus, takeoverRecoveryLines, withPathOutput, } from './presentation.js';
5
+ import { commandPrefix, joinRecoveryCommand, participantCommandPrefix, participantIdentity, participantsRecoveryCommand, quoteShell, renderAmbientEvent, renderAmbientEventFacts, renderEventCli, renderGrepActivitiesView, renderPresenceAnchor, renderWatchAlreadyActive, renderWatchReplaced, renderWatchStatus, takeoverRecoveryLines, withPathOutput, truncateChars, } from './presentation.js';
6
6
  import { formatRelativeTime, formatTimestamp } from './time.js';
7
7
  import { style } from './tty-style.js';
8
+ function listDisplay(value) {
9
+ const truncated = truncateChars(value, 159);
10
+ return truncated.remaining === 0 ? truncated.text : `${truncated.text}…`;
11
+ }
12
+ export function renderSquareList(data, projection) {
13
+ const { items, stopped, remaining, next } = data;
14
+ if (data.total === 0) {
15
+ return ['(no squares found)', ...(stopped ? [style('dim', '○ the walk stopped early — deeper squares may still be hiding')] : [])].join('\n') + '\n';
16
+ }
17
+ const lines = ['squares'];
18
+ for (const item of items) {
19
+ const glyph = item.activities > 0 ? '●' : style('dim', '○');
20
+ lines.push(`${glyph} ${listDisplay(item.path)}${style('dim', ` · ${formatRelativeTime(item.lastActiveAt, projection.now)} · ${item.participants.length} in the square · ${item.activities} ${item.activities === 1 ? 'activity' : 'activities'}`)}`);
21
+ const context = item.context.slice(0, 2);
22
+ if (context.length === 0)
23
+ lines.push(` ${style('dim', 'context')} · (none)`);
24
+ else {
25
+ context.forEach((line, index) => lines.push(` ${index === 0 ? style('dim', 'context') : ' '} · ${listDisplay(line)}`));
26
+ const hidden = item.context.length - context.length;
27
+ if (hidden > 0)
28
+ lines.push(style('dim', ` · … ${hidden} more ${hidden === 1 ? 'line' : 'lines'}`));
29
+ }
30
+ const participants = item.participants.slice(0, 3);
31
+ const hidden = item.participants.length - participants.length;
32
+ const names = participants.length === 0 ? 'nobody' : participants.map((name) => participantIdentity(listDisplay(name))).join(' · ');
33
+ lines.push(` ${style('dim', 'participants')} · ${names}${hidden > 0 ? style('dim', ` · … ${hidden} more`) : ''}`);
34
+ if (item.joined?.length)
35
+ lines.push(` ${style('dim', 'joined')} · ${item.joined.map(seat => `${participantIdentity(listDisplay(seat.name))}${seat.pending > 0 ? style('dim', ` (${seat.pending} waiting)`) : ''}`).join(' · ')}`);
36
+ }
37
+ if (remaining > 0) {
38
+ lines.push(style('dim', stopped ? ' … more squares' : ` … ${remaining} more ${remaining === 1 ? 'square' : 'squares'}`));
39
+ if (next !== undefined)
40
+ lines.push(`square list --depth ${projection.depth} --limit ${projection.limit} --after ${quoteShell(next)}`);
41
+ }
42
+ if (stopped)
43
+ lines.push(style('dim', '○ the walk stopped early — deeper squares may still be hiding'));
44
+ return lines.join('\n') + '\n';
45
+ }
8
46
  export const PARTICIPANTS_DEFAULT_LIMIT = 20;
9
47
  export const PARTICIPANTS_MAX_LIMIT = 100;
10
48
  const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
@@ -18,8 +56,8 @@ export function renderJoinResult(data, owner) {
18
56
  now: after.now, preview: 200, perception: event.perception,
19
57
  })).filter(Boolean).join('\n\n');
20
58
  const fallback = after.deliveryAvailable ? ['', '· when someone calls you, you’ll hear them here'] : [
21
- '', `${participantCommandPrefix(after.location, name, owner)} catch --idle 30m`,
22
- ' · stay within earshot — keep this catch open for new activity',
59
+ '', `${participantCommandPrefix(after.location, name, owner)} perceive --idle 30m`,
60
+ ' · stay within earshot — keep this perceive open for new activity',
23
61
  ];
24
62
  const entryLine = data.kind === 'joined' ? '● you stepped into the square'
25
63
  : data.kind === 'reconnected' ? '● you are already in the square'
@@ -49,7 +87,7 @@ export function renderExpression(squarePath, data, owner) {
49
87
  'Unread context · preview only, not consumed',
50
88
  ...context.activities.map(item => `· ${participantIdentity(item.actor)} · ${item.id}\n ${item.preview}${item.truncated ? '… (truncated)' : ''}`),
51
89
  ...(context.remaining === 0 ? [] : [`· ${context.remaining} more unread activities`]),
52
- `${participantCommandPrefix(projection.location, name, owner)} catch`,
90
+ `${participantCommandPrefix(projection.location, name, owner)} perceive`,
53
91
  ].join('\n');
54
92
  const ownCount = projection.ownActivityCount;
55
93
  const capCount = projection.hardCap === null ? '' : ` · ${ownCount}/${projection.hardCap}`;
@@ -73,10 +111,10 @@ function continuationCommand(squarePath, name, options, owner) {
73
111
  args.push('--mention');
74
112
  if (options.limit !== undefined)
75
113
  args.push('--limit', String(options.limit));
76
- return `${participantCommandPrefix(squarePath, name, owner)} catch ${args.join(' ')}`;
114
+ return `${participantCommandPrefix(squarePath, name, owner)} perceive ${args.join(' ')}`;
77
115
  }
78
- /** Render settled catch facts; no selection, terminal decisions, receipts or wait machinery. */
79
- export function renderCatchOutcome(squarePath, data, options = {}, owner) {
116
+ /** Render settled perceive facts; no selection, terminal decisions, receipts or wait machinery. */
117
+ export function renderPerceiveOutcome(squarePath, data, options = {}, owner) {
80
118
  const projection = data.observation;
81
119
  if (projection === undefined)
82
120
  return { stdout: '', exitCode: 0 };
@@ -86,7 +124,7 @@ export function renderCatchOutcome(squarePath, data, options = {}, owner) {
86
124
  const header = { participantCount: projection.standingCount };
87
125
  const statusOptions = {
88
126
  squarePath: location, name, owner, presence: projection.presence,
89
- showCatchHint: !projection.deliveryAvailable,
127
+ showPerceiveHint: !projection.deliveryAvailable,
90
128
  ownActivityCount: projection.ownActivityCount, hardCap: projection.hardCap,
91
129
  };
92
130
  if (stop === 'already-active' || stop === 'replaced') {
@@ -106,7 +144,7 @@ export function renderCatchOutcome(squarePath, data, options = {}, owner) {
106
144
  ...(stop === 'idle' && options.idle !== undefined ? { idleMs: options.idle } : {}),
107
145
  });
108
146
  const fallback = stop === undefined && !projection.deliveryAvailable && data.remaining === 0
109
- ? `${participantCommandPrefix(location, name, owner)} catch --idle 30m\n stay available for new activity` : '';
147
+ ? `${participantCommandPrefix(location, name, owner)} perceive --idle 30m\n stay available for new activity` : '';
110
148
  const hint = data.hint === undefined ? '' : style('dim', `· ${data.hint}`);
111
149
  return {
112
150
  stdout: withPathOutput(location, [terminal, rendered, hint, remaining, fallback].filter(Boolean).join('\n\n'), header),
@@ -187,7 +225,7 @@ export function renderParticipantsResult(squarePath, data, limit = PARTICIPANTS_
187
225
  const participants = data.participants;
188
226
  const lines = participants.slice(0, limit).map((participant) => {
189
227
  const glyph = participant.presence === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 && projection.classificationByName[participant.name] === 'here' ? '●' : '○';
190
- const state = participant.presence === 'watching' ? 'catching' : participant.presence === 'never-joined' ? 'not joined' : participant.presence;
228
+ const state = participant.presence === 'watching' ? 'perceiving' : participant.presence === 'never-joined' ? 'not joined' : participant.presence;
191
229
  const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
192
230
  return ` ${glyph} ${participant.name}${style('dim', ` · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`)}`;
193
231
  });
@@ -223,7 +261,7 @@ export function renderStatusResult(squarePath, data) {
223
261
  ? '◎'
224
262
  : participant.activityCount > 0 ? '●' : '○';
225
263
  const summary = participant.presence === 'watching'
226
- ? 'is nearby · catching'
264
+ ? 'is nearby · perceiving'
227
265
  : participant.activityCount > 0
228
266
  ? `is nearby · ${formatRelativeTime(participant.lastActiveAt ?? data.now, data.now)}`
229
267
  : `is nearby · quiet · ${participant.lastActiveAt === undefined ? 'just now' : formatRelativeTime(participant.lastActiveAt, data.now)}`;
@@ -234,7 +272,7 @@ export function renderStatusResult(squarePath, data) {
234
272
  ? `${participant.pendingMentionCount} ${participant.pendingMentionCount === 1 ? 'mention' : 'mentions'} waiting`
235
273
  : participant.unreadActivityCount > 0
236
274
  ? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
237
- : 'caught up';
275
+ : 'up to date';
238
276
  return ` ${glyph} ${participantIdentity(participant.name)}${style('dim', ` ${summary}${attention === '' ? '' : ` · ${attention}`}`)}`;
239
277
  }));
240
278
  people.push(...away.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map(participant => ` ○ ${participantIdentity(participant.name)}${style('dim', ' · away')}`));
@@ -12,7 +12,10 @@ export class FileLockError extends Error {
12
12
  }
13
13
  }
14
14
  function isBusy(error) {
15
- return error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message);
15
+ const code = error?.code;
16
+ return typeof code === 'string' && /^SQLITE_BUSY(?:_|$)/.test(code)
17
+ || typeof error?.errcode === 'number' && (error.errcode & 0xff) === 5
18
+ || error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
16
19
  }
17
20
  function isNotADatabase(error) {
18
21
  return error instanceof Error && /not a database|file is not a database/i.test(error.message);
@@ -0,0 +1,28 @@
1
+ import { type HydratedAct } from './model.js';
2
+ import type { ActivityIndex, DirectedFilter, SquareRead, SquareWrite } from './ports.js';
3
+ import type { PerceiveOptions, PerceiveReason, PerceivedActivity } from './square-facade.js';
4
+ export interface PerceiveSelection {
5
+ readonly participant: string;
6
+ readonly activities: readonly PerceivedActivity[];
7
+ readonly remaining: number;
8
+ readonly standingCount: number;
9
+ readonly deliveryAvailable: boolean;
10
+ readonly now: number;
11
+ readonly ownActivityCount: number;
12
+ readonly hardCap: number | null;
13
+ readonly terminalStatus: Extract<PerceiveReason, 'capped' | 'quorum'> | undefined;
14
+ readonly hint?: string;
15
+ }
16
+ /** Capture a selected page (or an empty completion) without opening another resource. */
17
+ export declare function observePerceiveSelection(facts: SquareRead, participant: string, activities: readonly HydratedAct[], filter: DirectedFilter, at: number, remaining?: number): PerceiveSelection;
18
+ export declare const PERCEIVE_DEFAULT_LIMIT = 10;
19
+ export declare const PERCEIVE_MAX_LIMIT = 100;
20
+ export declare function perceiveLimit(value: number | undefined): number;
21
+ /** Select non-ID perceive coordinates without hydrating bodies or recording receipts. */
22
+ export declare function selectPerceivePage(facts: SquareRead, requestedName: string, options: PerceiveOptions): {
23
+ readonly participant: string;
24
+ readonly indexes: readonly ActivityIndex[];
25
+ readonly filter: DirectedFilter;
26
+ };
27
+ /** Selection, receipts and resulting cursor belong to one artifact write transaction. */
28
+ export declare function decidePerceive(facts: SquareWrite, requestedName: string, options: PerceiveOptions, at: number): PerceiveSelection;
@@ -3,13 +3,13 @@ import { terminalStatusFor } from './runtime.js';
3
3
  import { SquareError } from './model.js';
4
4
  import { resolveKnownName } from './decisions.js';
5
5
  import { toPerceivedActivity } from './views.js';
6
- import { catchHint } from './hints.js';
6
+ import { perceiveHint } from './hints.js';
7
7
  import { consumeReceived, ReceiveError, selectReceivedHeads } from './receive.js';
8
8
  import { squareReceiveRead } from './square-receive.js';
9
9
  /** Capture a selected page (or an empty completion) without opening another resource. */
10
- export function observeCatchSelection(facts, participant, activities, filter, at, remaining) {
10
+ export function observePerceiveSelection(facts, participant, activities, filter, at, remaining) {
11
11
  if (typeof at !== 'number' || !Number.isFinite(at))
12
- throw new SquareError('invalid_args', 'Catch time must be a finite number.');
12
+ throw new SquareError('invalid_args', 'Perceive time must be a finite number.');
13
13
  const counts = facts.participantCounts(), header = facts.header(), current = facts.participant(participant);
14
14
  return {
15
15
  participant, activities: activities.map(activity => toPerceivedActivity(facts, activity, participant)),
@@ -20,44 +20,44 @@ export function observeCatchSelection(facts, participant, activities, filter, at
20
20
  terminalStatus: terminalStatusFor(current, header.hardCap, counts),
21
21
  };
22
22
  }
23
- export const CATCH_DEFAULT_LIMIT = 10;
24
- export const CATCH_MAX_LIMIT = 100;
25
- export function catchLimit(value) {
26
- const limit = value ?? CATCH_DEFAULT_LIMIT;
27
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > CATCH_MAX_LIMIT) {
28
- throw new SquareError('invalid_args', `Catch limit must be a positive integer no greater than ${CATCH_MAX_LIMIT}.`);
23
+ export const PERCEIVE_DEFAULT_LIMIT = 10;
24
+ export const PERCEIVE_MAX_LIMIT = 100;
25
+ export function perceiveLimit(value) {
26
+ const limit = value ?? PERCEIVE_DEFAULT_LIMIT;
27
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > PERCEIVE_MAX_LIMIT) {
28
+ throw new SquareError('invalid_args', `Perceive limit must be a positive integer no greater than ${PERCEIVE_MAX_LIMIT}.`);
29
29
  }
30
30
  return limit;
31
31
  }
32
- function catchPageSelection(facts, requestedName, options) {
32
+ function perceivePageSelection(facts, requestedName, options) {
33
33
  const participant = resolveKnownName(facts, requestedName);
34
- const limit = catchLimit(options.limit);
34
+ const limit = perceiveLimit(options.limit);
35
35
  const senders = options.from?.map(name => resolveKnownName(facts, name));
36
36
  const filter = { kinds: ['say'], unseenOnly: true, ...(senders === undefined ? {} : { senders }), ...(options.mention === true ? { attentionOnly: true } : {}) };
37
37
  return { participant, filter, selection: { kind: 'page', query: filter, limit } };
38
38
  }
39
- /** Select non-ID catch coordinates without hydrating bodies or recording receipts. */
40
- export function selectCatchPage(facts, requestedName, options) {
41
- const { participant, filter, selection } = catchPageSelection(facts, requestedName, options);
39
+ /** Select non-ID perceive coordinates without hydrating bodies or recording receipts. */
40
+ export function selectPerceivePage(facts, requestedName, options) {
41
+ const { participant, filter, selection } = perceivePageSelection(facts, requestedName, options);
42
42
  return { participant, indexes: selectReceivedHeads(squareReceiveRead(facts), participant, selection).map(item => item.key), filter };
43
43
  }
44
44
  /** Selection, receipts and resulting cursor belong to one artifact write transaction. */
45
- export function decideCatch(facts, requestedName, options, at) {
45
+ export function decidePerceive(facts, requestedName, options, at) {
46
46
  let viewer;
47
47
  let filter = { kinds: ['say'], unseenOnly: true };
48
48
  let selection;
49
49
  if (options.id !== undefined) {
50
50
  viewer = resolveKnownName(facts, requestedName);
51
- catchLimit(options.limit);
51
+ perceiveLimit(options.limit);
52
52
  if (['idle', 'from', 'mention', 'limit'].some((key) => Object.hasOwn(options, key)))
53
- throw new SquareError('invalid_args', 'Catch id cannot be combined with idle, from, mention or limit.');
53
+ throw new SquareError('invalid_args', 'Perceive id cannot be combined with idle, from, mention or limit.');
54
54
  const index = parseActivityId(options.id);
55
55
  if (index === undefined)
56
- throw new SquareError('invalid_args', 'Invalid catch id: expected an activity id like act/12.');
56
+ throw new SquareError('invalid_args', 'Invalid perceive id: expected an activity id like act/12.');
57
57
  selection = { kind: 'exact', key: index, remainingQuery: filter };
58
58
  }
59
59
  else {
60
- const page = catchPageSelection(facts, requestedName, options);
60
+ const page = perceivePageSelection(facts, requestedName, options);
61
61
  viewer = page.participant;
62
62
  filter = page.filter;
63
63
  selection = page.selection;
@@ -87,10 +87,10 @@ export function decideCatch(facts, requestedName, options, at) {
87
87
  });
88
88
  }
89
89
  const delivered = consumed.items.map(item => item.data);
90
- const observed = observeCatchSelection(facts, viewer, delivered, filter, at, consumed.remaining);
91
- // Receipts are the durable catch count: everything directed here minus what is still unseen.
92
- const caught = facts.directedCount(viewer, { kinds: ['say'] }) - facts.directedCount(viewer, { kinds: ['say'], unseenOnly: true });
93
- // An exact id catch is a targeted fetch and stays repeatable, so it carries no nudge.
94
- const hint = options.id === undefined ? catchHint(caught - consumed.newlyConsumed, caught) : undefined;
90
+ const observed = observePerceiveSelection(facts, viewer, delivered, filter, at, consumed.remaining);
91
+ // Receipts are the durable perceive count: everything directed here minus what is still unseen.
92
+ const perceived = facts.directedCount(viewer, { kinds: ['say'] }) - facts.directedCount(viewer, { kinds: ['say'], unseenOnly: true });
93
+ // An exact id perceive is a targeted fetch and stays repeatable, so it carries no nudge.
94
+ const hint = options.id === undefined ? perceiveHint(perceived - consumed.newlyConsumed, perceived) : undefined;
95
95
  return hint === undefined ? observed : { ...observed, hint };
96
96
  }
@@ -1,6 +1,6 @@
1
1
  import { type Audience } from './square-core.js';
2
2
  /** Pure filter over settled audience facts; unread/directed eligibility and paging belong to callers. */
3
- export declare function matchesCatchSelection(actor: string, audience: Audience, filter: {
3
+ export declare function matchesPerceiveSelection(actor: string, audience: Audience, filter: {
4
4
  readonly participants?: readonly string[];
5
5
  readonly mention?: string;
6
6
  }, replyAuthor?: string): boolean;
@@ -10,5 +10,5 @@ export interface RoutedNotification {
10
10
  readonly route: import('./model.js').DirectedNotificationRoute;
11
11
  readonly recipient: string;
12
12
  }
13
- /** A live catch owns every matching notification, independent of its page size. */
13
+ /** A live perceive owns every matching notification, independent of its page size. */
14
14
  export declare function leaseOwnsNotification(lease: import('./model.js').WatchLease, notification: RoutedNotification): boolean;
@@ -1,7 +1,7 @@
1
1
  import { sameName } from './model.js';
2
2
  import { audienceIncludes } from './square-core.js';
3
3
  /** Pure filter over settled audience facts; unread/directed eligibility and paging belong to callers. */
4
- export function matchesCatchSelection(actor, audience, filter, replyAuthor) {
4
+ export function matchesPerceiveSelection(actor, audience, filter, replyAuthor) {
5
5
  if (filter.participants !== undefined && !filter.participants.some((participant) => sameName(participant, actor))) {
6
6
  return false;
7
7
  }
@@ -12,8 +12,8 @@ export function matchesCatchSelection(actor, audience, filter, replyAuthor) {
12
12
  return audienceIncludes(audience, filter.mention)
13
13
  || (replyAuthor !== undefined && sameName(replyAuthor, filter.mention));
14
14
  }
15
- /** A live catch owns every matching notification, independent of its page size. */
15
+ /** A live perceive owns every matching notification, independent of its page size. */
16
16
  export function leaseOwnsNotification(lease, notification) {
17
- return matchesCatchSelection(notification.actor, notification.route === 'bell' ? { kind: 'bell' }
17
+ return matchesPerceiveSelection(notification.actor, notification.route === 'bell' ? { kind: 'bell' }
18
18
  : { kind: 'mentions', names: notification.route === 'mention' ? [notification.recipient] : [] }, lease.filter ?? {}, notification.route === 'reply' ? notification.recipient : undefined);
19
19
  }
@@ -48,8 +48,8 @@ export default {
48
48
  return async () => { };
49
49
  }
50
50
  const env = { ...process.env };
51
- const discovery = await observeOpenCodeSessions(env);
52
51
  const lifetime = new AbortController();
52
+ const discovery = await observeOpenCodeSessions(env, lifetime.signal);
53
53
  const receivers = new Map(); // Operational handles only; presence owns membership.
54
54
  const interrupting = new Set();
55
55
  const cancellationFailed = new Set();
@@ -80,7 +80,7 @@ export default {
80
80
  return target;
81
81
  };
82
82
  const reconcile = async (activeSession) => {
83
- const candidates = new Set(await listOpenCodeSessions(env));
83
+ const candidates = new Set(await listOpenCodeSessions(env, lifetime.signal));
84
84
  if (activeSession)
85
85
  candidates.add(activeSession);
86
86
  for (const sessionId of candidates) {
@@ -93,7 +93,7 @@ export default {
93
93
  const receiver = receivers.get(sessionId);
94
94
  stop(sessionId);
95
95
  if (receiver)
96
- await detachOpenCodeSession(sessionId, receiver.generation, env);
96
+ await detachOpenCodeSession(sessionId, receiver.generation, env, lifetime.signal);
97
97
  continue;
98
98
  }
99
99
  const existing = receivers.get(sessionId);
@@ -171,7 +171,7 @@ export default {
171
171
  const receiver = receivers.get(sessionId);
172
172
  stop(sessionId);
173
173
  if (receiver)
174
- await detachOpenCodeSession(sessionId, receiver.generation, env);
174
+ await detachOpenCodeSession(sessionId, receiver.generation, env, lifetime.signal);
175
175
  await reconcile();
176
176
  });
177
177
  else if (event.type === 'session.execution.interrupted' && event.data.reason === 'user') {
@@ -7,17 +7,17 @@ export interface OpenCodeContextMessage {
7
7
  readonly text?: string;
8
8
  }[];
9
9
  }
10
- export declare function listOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<readonly string[]>;
10
+ export declare function listOpenCodeSessions(env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<readonly string[]>;
11
11
  /** The file observer is a hint; callers must always reread the ledger after an edge. */
12
- export declare function observeOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<{
12
+ export declare function observeOpenCodeSessions(env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<{
13
13
  observer: import("../../file-changes.js").VersionObserver<string>;
14
14
  close: () => Promise<void>;
15
15
  }>;
16
16
  export declare function publishOpenCodeAddress(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<string | undefined>;
17
- export declare function currentOpenCodeGeneration(sessionId: string, env: NodeJS.ProcessEnv): Promise<string | undefined>;
18
- export declare function detachOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void>;
19
- export declare function endOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void>;
20
- export declare function cancelOpenCodeDeliveries(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<boolean>;
17
+ export declare function currentOpenCodeGeneration(sessionId: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<string | undefined>;
18
+ export declare function detachOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<void>;
19
+ export declare function endOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<void>;
20
+ export declare function cancelOpenCodeDeliveries(sessionId: string, generation: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<boolean>;
21
21
  /** Confirmation is observer-level: another host plugin may subsequently remove the message. */
22
22
  export declare function observeOpenCodeContext(sessionId: string, generation: string, messages: readonly OpenCodeContextMessage[], env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void>;
23
23
  export declare function receiveOpenCodePending(sessionId: string, generation: string, transport: DeliveryTransport, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void>;