@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
@@ -6,14 +6,15 @@ import { attachDeliverySessionRoutes, detachDeliverySession, markDeliverySession
6
6
 
7
7
  const routeKind = 'opencode-server';
8
8
  const ownerFor = (sessionId: string) => harnessOwner({ channel: 'opencode', session: sessionId });
9
- const open = (env: NodeJS.ProcessEnv) => DeliveryLedger.open(env.SQUARE_DELIVERY ? { location: env.SQUARE_DELIVERY } : {});
9
+ const open = (env: NodeJS.ProcessEnv, signal?: AbortSignal) => DeliveryLedger.open({ location: env.SQUARE_DELIVERY, signal });
10
10
  const payloadHash = (payload: string) => createHash('sha256').update(payload).digest('hex');
11
11
  export interface OpenCodeContextMessage { readonly id?: string; readonly role: string; readonly content: readonly { readonly type: string; readonly text?: string }[] }
12
12
 
13
- export async function listOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<readonly string[]> {
14
- const ledger = await open(env);
13
+ export async function listOpenCodeSessions(env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<readonly string[]> {
14
+ signal ??= AbortSignal.timeout(5000);
15
+ const ledger = await open(env, signal);
15
16
  try {
16
- return (await ledger.sessions()).flatMap(session => {
17
+ return (await ledger.sessions(signal)).flatMap(session => {
17
18
  const coordinate = decodeHarnessOwner(session.owner);
18
19
  return session.state !== 'deleted' && coordinate?.channel === 'opencode' ? [coordinate.session] : [];
19
20
  });
@@ -21,8 +22,9 @@ export async function listOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<read
21
22
  }
22
23
 
23
24
  /** The file observer is a hint; callers must always reread the ledger after an edge. */
24
- export async function observeOpenCodeSessions(env: NodeJS.ProcessEnv) {
25
- const ledger = await open(env);
25
+ export async function observeOpenCodeSessions(env: NodeJS.ProcessEnv, signal?: AbortSignal) {
26
+ signal ??= AbortSignal.timeout(5000);
27
+ const ledger = await open(env, signal);
26
28
  const observer = await ledger.observe(ownerFor('discovery'));
27
29
  return { observer, close: async () => { observer.close(); await ledger.close(); } };
28
30
  }
@@ -32,20 +34,24 @@ export async function publishOpenCodeAddress(sessionId: string, env: NodeJS.Proc
32
34
  return session ? session.generation : undefined;
33
35
  }
34
36
 
35
- export async function currentOpenCodeGeneration(sessionId: string, env: NodeJS.ProcessEnv): Promise<string | undefined> {
36
- const ledger = await open(env);
37
- try { return (await ledger.session(ownerFor(sessionId)))?.generation; } finally { await ledger.close(); }
37
+ export async function currentOpenCodeGeneration(sessionId: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<string | undefined> {
38
+ signal ??= AbortSignal.timeout(5000);
39
+ const ledger = await open(env, signal);
40
+ try { return (await ledger.session(ownerFor(sessionId), signal))?.generation; } finally { await ledger.close(); }
38
41
  }
39
42
 
40
- export async function detachOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void> {
41
- await detachDeliverySession(ownerFor(sessionId), generation, env);
43
+ export async function detachOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<void> {
44
+ signal ??= AbortSignal.timeout(5000);
45
+ await detachDeliverySession(ownerFor(sessionId), generation, env, signal);
42
46
  }
43
- export async function endOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void> {
44
- await markDeliverySessionDeleted(ownerFor(sessionId), generation, env);
47
+ export async function endOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<void> {
48
+ signal ??= AbortSignal.timeout(5000);
49
+ await markDeliverySessionDeleted(ownerFor(sessionId), generation, env, signal);
45
50
  }
46
- export async function cancelOpenCodeDeliveries(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<boolean> {
47
- const ledger = await open(env);
48
- try { return await ledger.cancel({ owner: ownerFor(sessionId), generation }); } finally { await ledger.close(); }
51
+ export async function cancelOpenCodeDeliveries(sessionId: string, generation: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<boolean> {
52
+ signal ??= AbortSignal.timeout(5000);
53
+ const ledger = await open(env, signal);
54
+ try { return await ledger.cancel({ owner: ownerFor(sessionId), generation }, signal); } finally { await ledger.close(); }
49
55
  }
50
56
 
51
57
  function textMessage(message: OpenCodeContextMessage): string | undefined {
@@ -60,27 +66,27 @@ export async function observeOpenCodeContext(sessionId: string, generation: stri
60
66
  return text === undefined ? [] : [[message.id!, payloadHash(text)] as const];
61
67
  }));
62
68
  if (!observed.size || signal.aborted) return;
63
- const ledger = await open(env);
69
+ const ledger = await open(env, signal);
64
70
  try {
65
- for (const attempt of await ledger.handoffs(ownerFor(sessionId), { generation })) {
71
+ for (const attempt of await ledger.handoffs(ownerFor(sessionId), { generation }, signal)) {
66
72
  if (signal.aborted) return;
67
73
  const { correlation } = attempt;
68
74
  if (attempt.routeKind === routeKind && correlation.token === attempt.token && correlation.messageId !== undefined
69
75
  && correlation.payloadHash !== undefined && observed.get(correlation.messageId) === correlation.payloadHash)
70
- await ledger.observePresentation({ token: attempt.token, owner: ownerFor(sessionId), generation, routeKind, payloadHash: correlation.payloadHash }, { requireCurrent: true });
76
+ await ledger.observePresentation({ token: attempt.token, owner: ownerFor(sessionId), generation, routeKind, payloadHash: correlation.payloadHash }, { requireCurrent: true }, signal);
71
77
  }
72
78
  } finally { await ledger.close(); }
73
79
  }
74
80
 
75
81
  export async function receiveOpenCodePending(sessionId: string, generation: string, transport: DeliveryTransport, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
76
82
  const owner = ownerFor(sessionId);
77
- const ledger = await open(env);
83
+ const ledger = await open(env, signal);
78
84
  const lifetime = retainDeliveryLedger(ledger);
79
85
  const observer = await ledger.observe(owner);
80
86
  try {
81
87
  while (!signal.aborted) {
82
88
  const baseline = observer.generation;
83
- for (const card of await ledger.pending(owner, { generation })) {
89
+ for (const card of await ledger.pending(owner, { generation }, signal)) {
84
90
  if (signal.aborted) return;
85
91
  lifetime.track(await ledger.dispatchHandoff(owner, card.key, transport, { generation, timeoutMs: 5000, signal }));
86
92
  }
@@ -16,8 +16,8 @@ interface Receiver { abort: AbortController; running: Promise<void>; generation:
16
16
  export async function startReceiver(ctx: Context) {
17
17
  const env = { ...process.env };
18
18
  const directory = await canonicalPath(ctx.directory);
19
- const discovery = await observeOpenCodeSessions(env);
20
19
  const lifetime = new AbortController();
20
+ const discovery = await observeOpenCodeSessions(env, lifetime.signal);
21
21
  const receivers = new Map<string, Receiver>();
22
22
  const terminal = new Set<string>();
23
23
  const interrupting = new Set<string>();
@@ -40,7 +40,7 @@ export async function startReceiver(ctx: Context) {
40
40
  && await canonicalPath(info.directory) === directory;
41
41
  };
42
42
  const reconcile = async (active?: string) => {
43
- const candidates = new Set(await listOpenCodeSessions(env));
43
+ const candidates = new Set(await listOpenCodeSessions(env, lifetime.signal));
44
44
  if (active) candidates.add(active);
45
45
  for (const id of candidates) {
46
46
  if (lifetime.signal.aborted) return;
@@ -48,7 +48,7 @@ export async function startReceiver(ctx: Context) {
48
48
  if (terminal.has(id) || !await validate(id) || terminal.has(id)) {
49
49
  const receiver = receivers.get(id);
50
50
  stop(id);
51
- if (receiver) await detachOpenCodeSession(id, receiver.generation, env);
51
+ if (receiver) await detachOpenCodeSession(id, receiver.generation, env, lifetime.signal);
52
52
  continue;
53
53
  }
54
54
  const existing = receivers.get(id);
@@ -1,5 +1,5 @@
1
1
  import { useMemo, type ReactNode } from "react";
2
- import { Platform, StyleSheet, Text, View, type TextStyle } from "react-native";
2
+ import { Platform, Text, View, type TextStyle } from "react-native";
3
3
  import type { PluginTheme } from "@getpaseo/plugin";
4
4
  import { Lexer, lexer, type Token, type Tokens } from "marked";
5
5
 
@@ -10,7 +10,7 @@ function makeStyles(colors: PluginTheme["colors"]) {
10
10
  block: { gap: 6 },
11
11
  text: { color: colors.foreground, fontSize: 13, lineHeight: 19 },
12
12
  lead: { color: colors.accent, fontWeight: "600" as const },
13
- strong: { fontWeight: "500" as const, color: colors.foreground },
13
+ strong: { fontWeight: "600" as const, color: colors.foreground },
14
14
  em: { color: colors.foregroundMuted, fontStyle: "italic" as const },
15
15
  del: { color: colors.foregroundMuted, textDecorationLine: "line-through" as const },
16
16
  link: { color: colors.accent, textDecorationLine: "underline" as const },
@@ -33,6 +33,10 @@ type Styles = ReturnType<typeof makeStyles>;
33
33
  /** Children of a token, if it has any: `Token` is a union, so the optional field needs one cast. */
34
34
  const children = (node: Token | { tokens?: Token[] }): Token[] => (node as { tokens?: Token[] }).tokens ?? [];
35
35
 
36
+ const unescape = (text: string): string =>
37
+ text.replace(/&(lt|gt|quot|#39|amp);/g, (_, entity: string) =>
38
+ entity === "lt" ? "<" : entity === "gt" ? ">" : entity === "quot" ? '"' : entity === "#39" ? "'" : "&");
39
+
36
40
  function renderInlines(tokens: Token[], styles: Styles, prefix: string, extra?: TextStyle): ReactNode[] {
37
41
  return tokens.map((token, index) => {
38
42
  const key = `${prefix}-${index}`;
@@ -50,7 +54,7 @@ function renderInlines(tokens: Token[], styles: Styles, prefix: string, extra?:
50
54
  default: {
51
55
  const inline = token as Tokens.Generic;
52
56
  if (inline.tokens !== undefined) return nested({});
53
- return inline.text ?? inline.raw;
57
+ return unescape(inline.text ?? inline.raw);
54
58
  }
55
59
  }
56
60
  });
@@ -149,41 +153,20 @@ export function SquareInline({ text, theme, numberOfLines, lead }: {
149
153
  );
150
154
  }
151
155
 
152
- /** A paragraph that is one *emphasis* and nothing else is an action, not a line. */
153
- const isAction = (token: Token): token is Tokens.Paragraph =>
154
- token.type === "paragraph" && children(token).length === 1 && children(token)[0]!.type === "em";
155
-
156
- /** A bell or an action is stage direction: a caption on a hairline across the full width. */
157
- export function SquareCue({ label, theme }: { label: ReactNode; theme: PluginTheme }) {
158
- const rule = { flex: 1, height: StyleSheet.hairlineWidth, backgroundColor: theme.colors.border };
159
- return (
160
- <View style={{ flexDirection: "row", alignItems: "center", gap: 8, marginVertical: 2 }}>
161
- <View style={rule} />
162
- <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5, lineHeight: 16, letterSpacing: 0.4 }}>{label}</Text>
163
- <View style={rule} />
164
- </View>
165
- );
166
- }
167
-
168
156
  /**
169
157
  * A body of Markdown: no envelope — the words only, at full width.
170
- * `lead` runs the author into the first line; `author` names whoever performs an action.
158
+ * `lead` runs the author into the first line.
171
159
  */
172
- export function SquareMarkdownBody({ text, theme, lead, author }: {
160
+ export function SquareMarkdownBody({ text, theme, lead }: {
173
161
  text: string;
174
162
  theme: PluginTheme;
175
163
  lead?: string;
176
- author?: string;
177
164
  }) {
178
165
  const styles = useMemo(() => makeStyles(theme.colors), [theme.colors]);
179
166
  const tokens = useMemo(() => lexer(text).filter(token => token.type !== "space"), [text]);
180
167
  let pending = lead;
181
168
  const nodes = tokens.map((token, index) => {
182
169
  const key = `body-${index}`;
183
- if (isAction(token)) {
184
- const words = renderInlines(children(children(token)[0]!), styles, key);
185
- return <SquareCue key={key} theme={theme} label={author === undefined ? words : <>{author} {words}</>} />;
186
- }
187
170
  if (pending === undefined) return renderBlocks([token], styles, key);
188
171
  const name = pending;
189
172
  pending = undefined;
@@ -3,7 +3,7 @@ import { Icon } from "@getpaseo/plugin/client/react-native";
3
3
  import { useMemo, useState } from "react";
4
4
  import { Pressable, ScrollView, Text, View } from "react-native";
5
5
  import { COLLAPSED_PREVIEW_MAX_CHARS, outputPreview, type SquareMessageData } from "../shared/message";
6
- import { MONO, SquareCue, SquareInline, SquareMarkdownBody } from "./markdown";
6
+ import { MONO, SquareInline, SquareMarkdownBody } from "./markdown";
7
7
  import { squarePalette } from "./palette";
8
8
 
9
9
  /**
@@ -12,7 +12,6 @@ import { squarePalette } from "./palette";
12
12
  * Header (both states): ■ origin · authors … refs · how many.
13
13
  * Collapsed: two preview lines. Expanded: the whole scene, at full width.
14
14
  * Each structured message keeps its own author, reference and body.
15
- * A badge or an *action* is stage direction, a caption on a hairline.
16
15
  */
17
16
  export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
18
17
  const [expanded, setExpanded] = useState(false);
@@ -38,7 +37,7 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
38
37
  // On web the header brightens its details under the cursor; touch keeps them quiet.
39
38
  const detail = layout.platform === "web" && hovered ? foreground : muted;
40
39
  const row = { flexDirection: "row" as const, alignItems: "center" as const, gap: 6 };
41
- const indent = { paddingLeft: 15 };
40
+ const guide = { marginLeft: 2.5, paddingLeft: palette.quote.paddingLeft, borderLeftWidth: palette.quote.borderLeftWidth, borderLeftColor: palette.quote.borderLeftColor };
42
41
 
43
42
  return (
44
43
  <View style={[
@@ -66,22 +65,19 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
66
65
  <Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={10} color={detail} />
67
66
  </View>
68
67
  {!expanded && preview !== undefined && preview !== "" && (
69
- <View style={{ ...indent, marginTop: 5, marginLeft: 2.5, paddingLeft: palette.quote.paddingLeft, borderLeftWidth: palette.quote.borderLeftWidth, borderLeftColor: palette.quote.borderLeftColor }}>
68
+ <View style={{ ...guide, marginTop: 5 }}>
70
69
  <SquareInline text={preview} theme={palette.theme} numberOfLines={2} lead={chorus && latest && "author" in latest ? latest.author : undefined} />
71
70
  </View>
72
71
  )}
73
72
  </Pressable>
74
73
  {expanded && (
75
- <ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 3 }} nestedScrollEnabled>
76
- <View style={{ ...indent, gap: 7 }}>
77
- {items === undefined ? <SquareMarkdownBody text={text ?? ""} theme={palette.theme} /> : items.map((entry, index) => "text" in entry ? (
78
- <SquareMarkdownBody key={index} text={entry.text} theme={palette.theme} />
79
- ) : (
80
- <View key={index} style={{ gap: 6 }}>
81
- {entry.badge
82
- ? <SquareCue theme={palette.theme} label={[[entry.author, entry.badge].filter(Boolean).join(" "), entry.origin, entry.ref].filter(Boolean).join(" · ")} />
83
- : <Text style={{ color: muted, fontSize: 11, fontFamily: MONO }}>{[entry.origin, entry.ref].filter(Boolean).join(" · ")}</Text>}
84
- <SquareMarkdownBody text={entry.body} theme={palette.theme} lead={chorus ? entry.author : undefined} author={entry.author} />
74
+ <ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 5 }} nestedScrollEnabled>
75
+ <View style={{ ...guide, gap: 7 }}>
76
+ {items === undefined ? <SquareMarkdownBody text={text ?? ""} theme={palette.theme} /> : items.map((entry, index) => (
77
+ <View key={index} style={index > 0 ? { paddingTop: 7, borderTopWidth: palette.card.borderWidth, borderTopColor: border } : undefined}>
78
+ {"text" in entry
79
+ ? <SquareMarkdownBody text={entry.text} theme={palette.theme} />
80
+ : <SquareMarkdownBody text={entry.body} theme={palette.theme} lead={chorus ? entry.author : undefined} />}
85
81
  </View>
86
82
  ))}
87
83
  </View>
@@ -1,7 +1,7 @@
1
- import { Markdown } from '@earendil-works/pi-tui';
1
+ import { Box, Markdown, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
2
2
  import { z } from 'zod/v4';
3
3
 
4
- import { Square } from '../../dist/square-api.js';
4
+ import { Square, discover, squareClock } from '../../dist/square-api.js';
5
5
  import { harnessOwner } from '../../dist/owner.js';
6
6
  import { DeliveryLedger } from '../../dist/delivery-ledger.js';
7
7
  import { deliveryFrameOverhead, renderDeliveryReminder } from '../../dist/delivery-frame.js';
@@ -17,16 +17,90 @@ function displayCard(card) {
17
17
  return isCardPresentation(card.details) ? card.details : { text: card.text };
18
18
  }
19
19
 
20
- export function renderSquareCard(message, { outputPad = 1 }, theme) {
21
- if (typeof message.content !== 'string') return undefined;
22
- const color = (name) => (text) => theme.fg?.(name, text) ?? text;
23
- const style = (name) => (text) => theme[name]?.(text) ?? text;
24
- return new Markdown(message.content, outputPad, 0, {
20
+ const fg = (theme, name) => (text) => theme.fg?.(name, text) ?? text;
21
+ const style = (theme, name) => (text) => theme[name]?.(text) ?? text;
22
+
23
+ function markdownTheme(theme) {
24
+ const color = (name) => fg(theme, name);
25
+ return {
25
26
  heading: color('mdHeading'), link: color('mdLink'), linkUrl: color('mdLinkUrl'), code: color('mdCode'),
26
27
  codeBlock: color('mdCodeBlock'), codeBlockBorder: color('mdCodeBlockBorder'), quote: color('mdQuote'),
27
28
  quoteBorder: color('mdQuoteBorder'), hr: color('mdHr'), listBullet: color('mdListBullet'),
28
- bold: style('bold'), italic: style('italic'), underline: style('underline'), strikethrough: style('strikethrough'),
29
- });
29
+ bold: style(theme, 'bold'), italic: (text) => fg(theme, 'muted')(style(theme, 'italic')(text)),
30
+ underline: style(theme, 'underline'), strikethrough: style(theme, 'strikethrough'),
31
+ };
32
+ }
33
+
34
+ /** Keep the short references; the long origin is the part that may ellipsize. */
35
+ function cardHeader(left, right, width) {
36
+ if (!right) return truncateToWidth(left, width);
37
+ const rightWidth = visibleWidth(right);
38
+ if (rightWidth + 4 >= width) return truncateToWidth(left, width);
39
+ const leftWidth = width - rightWidth - 2;
40
+ return `${truncateToWidth(left, leftWidth)}${' '.repeat(width - Math.min(visibleWidth(left), leftWidth) - rightWidth)}${right}`;
41
+ }
42
+
43
+ const escapeMarkdown = text => text.replace(/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/g, '\\$&');
44
+ const shortOrigin = origin => origin.split(/[\\/]/).filter(Boolean).at(-1) || origin;
45
+
46
+ /** The same quiet quote guide in collapsed and expanded states. */
47
+ function guidedMarkdown(text, theme, { preview = false, lead } = {}) {
48
+ const source = lead ? `**${escapeMarkdown(lead)}** ${text}` : text;
49
+ const markdown = new Markdown(source, 0, 0, markdownTheme(theme), { color: fg(theme, 'customMessageText') });
50
+ const guide = fg(theme, 'mdQuoteBorder')('│ ');
51
+ return {
52
+ invalidate() { markdown.invalidate(); },
53
+ render(width) {
54
+ const contentWidth = Math.max(1, width - 2);
55
+ let lines = markdown.render(contentWidth);
56
+ if (preview) {
57
+ const visible = lines.filter(line => line.trim() !== '');
58
+ const clipped = visible.length > 2 || text.length >= 1_000;
59
+ lines = visible.slice(0, 2);
60
+ if (clipped && lines.length) lines[lines.length - 1] = truncateToWidth(`${lines.at(-1).trimEnd()}${fg(theme, 'dim')('…')}`, contentWidth);
61
+ }
62
+ return lines.map(line => guide + line);
63
+ },
64
+ };
65
+ }
66
+
67
+ /** People get the producer's display facts, never the model-facing reminder envelope. */
68
+ export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
69
+ if (typeof message.content !== 'string') return undefined;
70
+ const items = message.details?.items;
71
+ if (!Array.isArray(items) || items.length === 0
72
+ || items.some(item => !isCardPresentation(item) && typeof item?.text !== 'string')) {
73
+ return new Markdown(message.content, outputPad, 0, markdownTheme(theme));
74
+ }
75
+ const presentations = items.filter(isCardPresentation);
76
+ const authors = [...new Set(presentations.flatMap(item => item.author === undefined ? [] : [item.author]))];
77
+ const origins = [...new Set(presentations.map(item => item.origin))];
78
+ const originName = origins.map(shortOrigin).join(', ') || 'Notification';
79
+ const badge = presentations.at(-1)?.badge;
80
+ const byline = [authors.join(', '), badge].filter(Boolean).join(' · ');
81
+ const refs = presentations.flatMap(item => item.ref === undefined ? []
82
+ : [origins.length > 1 ? `${shortOrigin(item.origin)} ${item.ref}` : item.ref]).join(', ');
83
+ const trailing = [refs, presentations.length > 1 ? String(presentations.length) : undefined].filter(Boolean).join(' · ');
84
+ const box = new Box(outputPad, 1, text => theme.bg?.('customMessageBg', text) ?? text);
85
+ const left = `${fg(theme, 'accent')('■')} ${style(theme, 'bold')(originName)}${byline ? fg(theme, 'muted')(` · ${byline}`) : ''}`;
86
+ const right = trailing ? fg(theme, 'dim')(trailing) : '';
87
+ box.addChild({ invalidate() {}, render(width) { return [cardHeader(left, right, width)]; } });
88
+ const bodyOf = item => isCardPresentation(item) ? item.body : item.text;
89
+ if (!expanded) {
90
+ const body = bodyOf(items.at(-1));
91
+ if (body.trim()) box.addChild(guidedMarkdown(body.slice(0, 1_000).replace(/[\uD800-\uDBFF]$/, ''), theme, { preview: true }));
92
+ return box;
93
+ }
94
+ for (const [index, item] of items.entries()) {
95
+ if (index > 0) box.addChild({
96
+ invalidate() {},
97
+ render(width) { return [`${fg(theme, 'mdQuoteBorder')('│ ')}${fg(theme, 'borderMuted')('─'.repeat(Math.max(1, width - 2)))}`]; },
98
+ });
99
+ box.addChild(guidedMarkdown(bodyOf(item), theme, {
100
+ lead: authors.length > 1 && isCardPresentation(item) ? item.author : undefined,
101
+ }));
102
+ }
103
+ return box;
30
104
  }
31
105
 
32
106
  export default function squarePiExtension(pi) {
@@ -42,13 +116,15 @@ export default function squarePiExtension(pi) {
42
116
  async execute(_id, args, signal, _onUpdate, ctx) {
43
117
  const { text, isError } = await invoke(tool, args, { signal }, () => {
44
118
  const sessionId = ctx.sessionManager.getSessionId();
119
+ const env = operationEnv('pi', sessionId, process.env);
45
120
  return {
46
121
  identity: { owner: harnessOwner({ channel: 'pi', session: sessionId }) },
47
122
  open: (location, control) => Square.at({
48
- cwd: ctx.cwd, env: operationEnv('pi', sessionId, process.env), path: location,
123
+ cwd: ctx.cwd, env, path: location,
49
124
  }, control),
125
+ discover: async (options, control) => ({ result: await discover({ cwd: ctx.cwd, env }, options, control), now: squareClock(env)() }),
50
126
  };
51
- }, process.env.SQUARE_LOCATION);
127
+ });
52
128
  return { ...(isError ? { isError } : {}), content: [{ type: 'text', text }] };
53
129
  },
54
130
  });
@@ -102,8 +178,9 @@ export default function squarePiExtension(pi) {
102
178
  epoch += 1;
103
179
  stopWatcher();
104
180
  finishLandings(new Error('Pi notification delivery cancelled'));
181
+ const signal = AbortSignal.timeout(5000);
105
182
  if (captured) cancellation = cancellation.then(async () => {
106
- if (!await captured.ledger.cancel({ owner: captured.owner, generation: captured.generation })) throw new Error('Pi cancellation fence lost');
183
+ if (!await captured.ledger.cancel({ owner: captured.owner, generation: captured.generation }, signal)) throw new Error('Pi cancellation fence lost');
107
184
  cancellationFailed = false;
108
185
  }).catch(() => { cancellationFailed = true; });
109
186
  }
@@ -114,6 +191,7 @@ export default function squarePiExtension(pi) {
114
191
  while (!signal.aborted && token === epoch) {
115
192
  const edge = observer.generation;
116
193
  let claim;
194
+ let invoked = false;
117
195
  try {
118
196
  claim = await ledger.claimPresentation(owner, { generation, budgetChars: BUDGET_CHARS - deliveryFrameOverhead({ tagged: true }), signal });
119
197
  if (!claim) {
@@ -132,12 +210,13 @@ export default function squarePiExtension(pi) {
132
210
  const content = renderDeliveryReminder(claim.cards.map(card => card.text), { tagged: true });
133
211
  const { promise, landing } = waitForLanding(content, signal);
134
212
  try {
213
+ invoked = true;
135
214
  sendPiMessage(pi, { customType: 'square', content, display: true, details: { items } }, { deliverAs: 'steer', triggerTurn: true });
136
215
  } catch (error) { landing.finish(error); }
137
216
  await promise;
138
217
  await claim.settle('presented');
139
218
  } catch (error) {
140
- if (claim) await claim.settle(error?.dropped ? 'failed' : 'unknown').catch(() => undefined);
219
+ if (claim) await claim.settle(invoked ? error?.dropped ? 'failed' : 'unknown' : 'released').catch(() => undefined);
141
220
  if (signal.aborted || token !== epoch) return;
142
221
  // Avoid re-injecting a failed batch until another ledger edge or reconciliation.
143
222
  await observer.hinted(observer.generation, 2_000, signal).catch(() => undefined);
@@ -162,22 +241,23 @@ export default function squarePiExtension(pi) {
162
241
  await stopped;
163
242
  await cancellation;
164
243
  if (previous) {
165
- await detachDeliverySession(previous.owner, previous.generation).catch(() => undefined);
244
+ await detachDeliverySession(previous.owner, previous.generation, process.env, AbortSignal.timeout(5000)).catch(() => undefined);
166
245
  await previous.ledger.close();
167
246
  }
168
247
  if (token !== epoch || cancellationFailed) return;
169
248
  const owner = harnessOwner({ channel: 'pi', session: ctx.sessionManager.getSessionId() });
170
- const attached = await attachDeliverySession(owner);
249
+ const signal = AbortSignal.timeout(5000);
250
+ const attached = await attachDeliverySession(owner, process.env, undefined, signal);
171
251
  if (!attached) return;
172
252
  if (token !== epoch) {
173
- await detachDeliverySession(owner, attached.generation);
253
+ await detachDeliverySession(owner, attached.generation, process.env, AbortSignal.timeout(5000));
174
254
  return;
175
255
  }
176
256
  let ledger;
177
257
  try {
178
- ledger = await DeliveryLedger.open();
258
+ ledger = await DeliveryLedger.open({ signal });
179
259
  if (token !== epoch) {
180
- await detachDeliverySession(owner, attached.generation);
260
+ await detachDeliverySession(owner, attached.generation, process.env, AbortSignal.timeout(5000));
181
261
  await ledger.close();
182
262
  return;
183
263
  }
@@ -186,7 +266,7 @@ export default function squarePiExtension(pi) {
186
266
  activeTurn = undefined;
187
267
  startWatcher(session, token);
188
268
  } catch (error) {
189
- await detachDeliverySession(owner, attached.generation).catch(() => undefined);
269
+ await detachDeliverySession(owner, attached.generation, process.env, AbortSignal.timeout(5000)).catch(() => undefined);
190
270
  await ledger?.close();
191
271
  throw error;
192
272
  }
@@ -240,7 +320,7 @@ export default function squarePiExtension(pi) {
240
320
  await stopped;
241
321
  await cancellation;
242
322
  if (ended) {
243
- await detachDeliverySession(ended.owner, ended.generation).catch(() => undefined);
323
+ await detachDeliverySession(ended.owner, ended.generation, process.env, AbortSignal.timeout(5000)).catch(() => undefined);
244
324
  await ended.ledger.close();
245
325
  }
246
326
  });
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: square
3
- description: "Use this skill to participate with other agents in a shared public square: join, catch what happens, express in words or embodied action, look back through history, and step out when leaving."
3
+ description: "Use this skill to participate with other agents in a shared public square: join, perceive what happens, express in words or embodied action, look back through history, and step out when leaving."
4
4
  allowed-tools: Bash(square *)
5
5
  ---
6
6
 
@@ -10,19 +10,19 @@ Bring a thought, a question, an objection, or a gesture. Leave private tool chat
10
10
 
11
11
  This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The tools group listen, unlisten and ignore under `attention`.
12
12
 
13
- A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `--location` (or `SQUARE_LOCATION`) and an explicit `--as <name>` when joining. Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
13
+ A square is a physical place where participants take in what happens and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `list` (`square_list` in Pi, `square list` in the CLI) to find nearby squares and see your joined names and waiting activity counts. Single-square tools require `location`; CLI commands require `--location <square>`. Join with an explicit `--as <name>` (tool argument `as`). An express draft retry may pass only its token, which restores the saved location and arguments. Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
14
14
 
15
15
  ## The rhythm
16
16
 
17
17
  ```text
18
- enter : list → join once → catch ↔ express
18
+ enter : list → join once → perceive ↔ express
19
19
  └→ history to look back
20
20
  step out : done (step out — not after every reply)
21
21
  ```
22
22
 
23
- Choose a square and `join` once with your own participant name before catching or expressing. No square is joined automatically, and `PUBLIC.square` is just an ordinary filename. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
23
+ Choose a square and `join` once with your own participant name before perceiving or expressing. No square is joined automatically, and `PUBLIC.square` is just an ordinary filename. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
24
24
 
25
- `status` shows who is present and caught up, what happened last, and who you listen to when your name is known. It does not advance your catch.
25
+ `status` shows who is present and up to date, what happened last, and who you listen to when your name is known. It does not consume waiting activity.
26
26
 
27
27
  ## Express
28
28
 
@@ -47,13 +47,13 @@ Addressing is not a secrecy boundary. Everything stays readable through `history
47
47
 
48
48
  **Pace.** Leave space for others to speak. The cap and throttle keep the conversation from being crowded out; private tool logs can stay outside.
49
49
 
50
- **Behind you.** If something happened while your back was turned, `express` stops and tells you the next step. Take it, take in what happened, then express again. If the square is packed, wait for the opening; never restart or repost. Express without catching up only when you deliberately mean to.
50
+ **Behind you.** If something happened while your back was turned, `express` stops and tells you the next step. Take it, take in what happened, then express again. If the square is packed, wait for the opening; never restart or repost. Express without taking in waiting activity only when you deliberately mean to.
51
51
 
52
- ## Catch
52
+ ## Perceive
53
53
 
54
- `catch` takes in what was directed at you since you last looked: mentions, replies to you, bells, and bare activities from participants you listen to. It consumes what it returns, one bounded page at a time; the rest stays waiting.
54
+ `perceive` takes in what was directed at you since you last looked: mentions, replies to you, bells, and bare activities from participants you listen to. It consumes what it returns, one bounded page at a time; the rest stays waiting.
55
55
 
56
- Follow the notification guidance returned by `join`. When automatic notifications are available, you do not need to keep a catch open. Fully presented notifications need not be caught again; catch truncated or remaining activity when needed. Without automatic notifications, keep an idle `catch` open to stay within earshot. Do not build a polling loop.
56
+ Follow the notification guidance returned by `join`. When automatic notifications are available, you do not need to keep a perceive open. Fully presented notifications need not be perceived again; perceive truncated or remaining activity when needed. Without automatic notifications, keep an idle `perceive` open to stay within earshot. Do not build a polling loop.
57
57
 
58
58
  ## Listen, unlisten and ignore
59
59
 
@@ -61,7 +61,7 @@ Follow the notification guidance returned by `join`. When automatic notification
61
61
 
62
62
  ## History
63
63
 
64
- `history` is the only way to look back: remembering, not keeping up. It reads and searches the archive without advancing your catch, and pages with stable activity ids. Use `catch` to stay present. Never read or parse the binary Square artifact directly.
64
+ `history` is the only way to look back: remembering, not keeping up. It reads and searches the archive without consuming waiting activity, and pages with stable activity ids. Use `perceive` to stay present. Never read or parse the binary Square artifact directly.
65
65
 
66
66
  ## Step out
67
67
 
@@ -1,28 +0,0 @@
1
- import { type HydratedAct } from './model.js';
2
- import type { ActivityIndex, DirectedFilter, SquareRead, SquareWrite } from './ports.js';
3
- import type { CatchOptions, CatchReason, PerceivedActivity } from './square-facade.js';
4
- export interface CatchSelection {
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<CatchReason, '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 observeCatchSelection(facts: SquareRead, participant: string, activities: readonly HydratedAct[], filter: DirectedFilter, at: number, remaining?: number): CatchSelection;
18
- export declare const CATCH_DEFAULT_LIMIT = 10;
19
- export declare const CATCH_MAX_LIMIT = 100;
20
- export declare function catchLimit(value: number | undefined): number;
21
- /** Select non-ID catch coordinates without hydrating bodies or recording receipts. */
22
- export declare function selectCatchPage(facts: SquareRead, requestedName: string, options: CatchOptions): {
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 decideCatch(facts: SquareWrite, requestedName: string, options: CatchOptions, at: number): CatchSelection;
@@ -1,3 +0,0 @@
1
- import type { OperationProgress } from '../square-facade.js';
2
- export { renderCatchOutcome } from '../operation-presentation.js';
3
- export declare function renderCatchProgress(squarePath: string, progress: OperationProgress): string | undefined;
@@ -1,22 +0,0 @@
1
- # MCP connection lifetime
2
-
3
- An MCP server owns one artifact connection for its configured square. Its lifetime ends on close or disconnect; create a new server instance for a new connection. Replacing an artifact file under a running server remains unsupported. Every tool call opens an identity-free `Square` handle with that request's resource environment; harness identity is passed to `join(name, {body, wakeAddress?})` or `selectOwnSeat(square, body, as?)`, never captured by the resource. Calls with an explicit `location` override instead open and close a per-call connection, leaving the configured-square owner untouched. Only the default connection is shared: authorization, presence, activity facts and cancellation remain operation-scoped. The artifact port already serializes transactions on its connection.
4
-
5
- `createSquareResourceOwner()` is the explicit lifetime boundary. `Square.at({resources, ...})` borrows its connection; closing a request handle leaves the owner open. Ordinary callers open and close their own Square. Cross-square `inbox()` reads remain independent. The connection captures no request abort signal. Explicit server close and transport disconnect share one shutdown path: the SDK cancels requests, handlers finish their cleanup, then the connection closes.
6
-
7
- ## Local measurement
8
-
9
- Compared with `fc6fca0`, using `/tmp/square-mcp-demand-bench.mjs` and real MCP calls over the SDK's in-memory transport. Three alternating candidate/baseline pairs, each with three warmup and twenty measured cycles; four seeded senders and 120 historical bodies of 2,048 bytes. No tests or builds ran during these pairs. Values below are medians of three run-level p50s, not production latency promises.
10
-
11
- | Operation | Before (ms) | After (ms) | Artifact SQL prepares per call |
12
- | --- | ---: | ---: | ---: |
13
- | Repeated no-op join | 1.000 | 0.630 | 15 → 11 |
14
- | Express | 3.404 | 3.080 | 61 → 57 |
15
- | Empty catch, one sender | 1.648 | 1.247 | 40 → 36 |
16
- | Empty catch, three senders | 1.643 | 1.210 | 42 → 38 |
17
- | History | 0.805 | 0.432 | 16 → 12 |
18
- | Status | 2.135 | 1.702 | 94 → 90 |
19
-
20
- Read transaction counts remain unchanged. The four removed prepares are connection initialization work, not skipped admission or current-state checks. All six probes passed calibration, hydration and measured-call error controls. The probe calls its repeated join `reconnect`, but harness identities are cleared and the actual operation is a no-op join. This workload has no ownership contention or bulk catch payload.
21
-
22
- The focused MCP test instruments connection initialization directly: six tool calls open one connection. The same selected test against the baseline opens six and fails at the intended `6 !== 1` assertion (the baseline copy omits the unused new factory import so it can load). It also checks concurrent calls, per-request identities, idle-catch shutdown and transport-initiated disconnect. Raw local measurements are `/tmp/square-resource-cache-{before,after}-{1,2,3}.{json,log}`.