@astrosheep/square 0.3.101 → 0.3.103

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.
@@ -11,4 +11,4 @@ export declare function renderPendingAtBoundary(inbox: PendingMembership[]): str
11
11
  * after success, captured-seat commits record only its included items without an eligibility veto.
12
12
  * A source that cannot verify its reuse must read the inbox again.
13
13
  */
14
- export declare function presentPendingAtBoundary<T>(identity: Owner, present: (context: string) => T | Promise<T>, lookup?: (identity: Owner, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<PendingMembership[]> | PendingMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
14
+ export declare function presentPendingAtBoundary<T>(identity: Owner, present: (context: string, delivered: PendingMembership[]) => T | Promise<T>, lookup?: (identity: Owner, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<PendingMembership[]> | PendingMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
@@ -87,12 +87,14 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
87
87
  }
88
88
  const report = await presentPreparedBatch(claims.map(({ claim }) => claim), eligibleClaims => {
89
89
  const eligible = new Set(claims.filter(({ claim }) => eligibleClaims.includes(claim)).map(({ key }) => key));
90
- const fresh = renderBoundary(pending.map(membership => ({ ...membership,
90
+ const delivered = pending.map(membership => ({ ...membership,
91
91
  notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
92
- })));
92
+ })).filter(membership => membership.notifications.length > 0);
93
+ const fresh = renderBoundary(delivered);
93
94
  const included = new Set(fresh.complete.map(entry => pendingKey(entry.membership, entry.actIndex)));
94
- return { payload: fresh.context, included: claims.filter(({ key }) => included.has(key)).map(({ claim }) => claim) };
95
- }, present, { signal, classifyError: () => 'failed' });
95
+ return { payload: { context: fresh.context, delivered },
96
+ included: claims.filter(({ key }) => included.has(key)).map(({ claim }) => claim) };
97
+ }, payload => present(payload.context, payload.delivered), { signal });
96
98
  if (report.observation.kind === 'threw')
97
99
  throw report.observation.error;
98
100
  for (const { result } of report.settlements)
@@ -144,7 +144,7 @@ export async function observeClaudeDelivery(sessionId, text, operation, env = pr
144
144
  if (operation === 'stored')
145
145
  await presentPending({ artifact: row.artifact, location: request.location,
146
146
  // Claude's stored event consumes even Agora previews; retain that contract for both modes.
147
- participant: request.participant, activity: request.activity, seat: request.seat, markSeen: true, sink: { present() { } },
147
+ participant: request.participant, activity: request.activity, seat: request.seat, observationOf: request.claimToken, markSeen: true, sink: { present() { } },
148
148
  current: facts => currentPayload(facts, request) === payload });
149
149
  }
150
150
  return { recognized, current: true };
@@ -64,7 +64,7 @@ export function presentationBlocksDispatch(attempts, now) {
64
64
  finite(now);
65
65
  return attempts.some(row => row.purpose === 'presentation'
66
66
  && (row.outcome === 'presented' || row.outcome === 'clipped' || row.outcome === 'unknown'
67
- || row.outcome === 'dispatching' && (row.expiresAt === undefined || row.expiresAt > now)));
67
+ || row.outcome === 'dispatching'));
68
68
  }
69
69
  /** An expired dispatch is uncertain, not permission for automatic replay. */
70
70
  export function automaticDispatchEligible(attempts, now) {
@@ -29,7 +29,7 @@ export async function classifyDeliveryHealth(squarePath, opts) {
29
29
  for (const { note, seat, evidence } of current) {
30
30
  const ageMs = Math.max(0, now - note.at);
31
31
  const presentation = evidence.some((row) => row.kind === 'presentation' && (row.outcome === 'presented' || row.outcome === 'clipped' || row.outcome === 'unknown'
32
- || row.outcome === 'dispatching' && (row.expiresAt === undefined || row.expiresAt > now)));
32
+ || row.outcome === 'dispatching'));
33
33
  const terminal = evidence.findLast((row) => row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown'));
34
34
  const attemptRow = evidence.findLast((row) => row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown' || row.outcome === 'failed'));
35
35
  const attempt = attemptRow === undefined || seat === undefined ? undefined : deliveryAttempt(squarePath, attemptRow, { squarePath, recipient: seat.participant, actIndex: note.index });
package/dist/inbox.js CHANGED
@@ -27,7 +27,7 @@ function eligibleNotifications(facts, membership, now = Date.now()) {
27
27
  const notifications = membership.notifications.filter(notification => {
28
28
  if (cancelledThrough !== undefined && notification.actIndex <= cancelledThrough
29
29
  || facts.deliveryEvidence(seat.participant, seat.generation, formatActivityId(notification.actIndex))
30
- .some(row => row.kind === 'presentation' && (row.outcome === 'presented' || row.outcome === 'clipped')))
30
+ .some(row => row.outcome !== 'failed' && row.outcome !== 'released'))
31
31
  return false;
32
32
  const deferredUntil = deliveryDeferralExpiry(facts, seat, notification.actIndex, now);
33
33
  if (deferredUntil === undefined)
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, squareToolsEnabled, squareToolFailure, squareToolFailureText } from './square-tools.js';
3
+ import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, SquareExpressDrafts, squareToolsEnabled, squareToolFailure, squareToolFailureText } from './square-tools.js';
4
4
  import { operationEnv } from './participant-identity.js';
5
5
  import { codexWakeAddress } from './codex-address.js';
6
6
  import { readClaudeMcpCall } from './claude-mcp-context.js';
@@ -12,6 +12,7 @@ export function createSquareMcpServer(options) {
12
12
  const baseEnv = { ...(resourceOptions.env ?? process.env) };
13
13
  const toolsEnabled = squareToolsEnabled(baseEnv);
14
14
  const resources = createSquareResourceOwner();
15
+ const drafts = new SquareExpressDrafts();
15
16
  // Calls to the configured square borrow one server-owned connection.
16
17
  const inFlight = new Set();
17
18
  const server = new McpServer({ name: 'square', version: SQUARE_IDENTITY.packageVersion }, {
@@ -22,21 +23,30 @@ export function createSquareMcpServer(options) {
22
23
  server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args, extra) => {
23
24
  const call = (async () => {
24
25
  let square;
26
+ let toolArgs = args;
27
+ let owner;
28
+ let draftToken;
29
+ let expressAttempted = false;
25
30
  try {
26
- const { location } = args;
27
31
  const threadId = extra._meta?.threadId;
28
32
  const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
29
33
  // Claude's plugin endpoint requires the mod's call-local coordinate, never startup identity.
30
34
  const claude = baseEnv.SQUARE_CLAUDE_MCP_CONTEXT === '1'
31
35
  ? await readClaudeMcpCall(extra._meta?.['claudecode/toolUseId'], baseEnv)
32
36
  : undefined;
33
- const owner = claude !== undefined
37
+ owner = claude !== undefined
34
38
  ? harnessOwner({ channel: 'claude-code', session: claude.sessionId, ...(claude.agentId === undefined ? {} : { agent: claude.agentId }) })
35
39
  : typeof threadId === 'string' && threadId.trim()
36
40
  ? harnessOwner({ channel: 'codex', session: threadId })
37
41
  : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
38
42
  ? harnessOwner({ channel: 'opencode', session: opencodeSessionId })
39
43
  : configuredOwner;
44
+ if (tool.name === 'express') {
45
+ const resolved = drafts.resolve(toolArgs, owner);
46
+ toolArgs = resolved.args;
47
+ draftToken = resolved.token;
48
+ }
49
+ const { location } = toolArgs;
40
50
  const env = claude !== undefined
41
51
  ? { ...operationEnv('claude', claude.sessionId, baseEnv),
42
52
  // Preserve Claude's session + child flag semantics; an agent loop ID is not a session ID.
@@ -59,10 +69,12 @@ export function createSquareMcpServer(options) {
59
69
  ? wakeAddress : undefined;
60
70
  let text;
61
71
  try {
62
- text = await tool.run(square, args, { signal: extra.signal }, {
72
+ expressAttempted = tool.name === 'express';
73
+ text = await tool.run(square, toolArgs, { signal: extra.signal }, {
63
74
  owner, wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
64
75
  ? codexWakeAddress(threadId, env) : callWake,
65
76
  });
77
+ drafts.consume(draftToken);
66
78
  }
67
79
  finally {
68
80
  await square.close();
@@ -71,10 +83,12 @@ export function createSquareMcpServer(options) {
71
83
  }
72
84
  catch (error) {
73
85
  const failure = squareToolFailure(error, extra.signal);
74
- const participant = args.as;
86
+ const participant = toolArgs.as;
87
+ const draft = tool.name === 'express' ? drafts.retain(toolArgs, owner, draftToken, failure, expressAttempted) : undefined;
75
88
  return { isError: true, content: [{ type: 'text', text: squareToolFailureText(failure, {
76
89
  location: square?.location ?? options.path,
77
90
  participant: typeof participant === 'string' ? participant : undefined,
91
+ draft,
78
92
  }) }] };
79
93
  }
80
94
  })();
@@ -115,7 +115,7 @@ export async function observeOpenCodeContext(sessionId, messages, env, signal) {
115
115
  if (signal.aborted || evidence.claimToken === undefined)
116
116
  return;
117
117
  await presentPending({ artifact, location, participant: seat.participant,
118
- activity: formatActivityId(index), seat: asSeatRef(seat), sink: { present() { } }, markSeen: !clipped, signal,
118
+ activity: formatActivityId(index), seat: asSeatRef(seat), observationOf: evidence.claimToken, sink: { present() { } }, markSeen: !clipped, signal,
119
119
  current: (facts) => {
120
120
  const current = facts.seat(seat.participant);
121
121
  const prepared = current === undefined || !sameSeat(current, seat) ? undefined : preview(facts, location, seat, index);
package/dist/ports.d.ts CHANGED
@@ -133,6 +133,8 @@ export interface DeliveryClaimInput {
133
133
  readonly seat: SeatRef;
134
134
  readonly activity: ActivityId;
135
135
  readonly kind: 'wake' | 'presentation';
136
+ /** Observe an existing wake's exact token; never authorizes another content injection. */
137
+ readonly observationOf?: string;
136
138
  readonly route?: WakeAddress;
137
139
  readonly at: number;
138
140
  readonly leaseMs: number;
@@ -322,6 +324,8 @@ export interface PresentPendingInput {
322
324
  readonly participant: string;
323
325
  readonly activity: string | number;
324
326
  readonly sink: PresentationSinkPort;
327
+ /** Existing wake observed by a native receiver; the sink must not inject content. */
328
+ readonly observationOf?: string;
325
329
  /** Captured artifact seat; used to fence both presentation and consumption. */
326
330
  readonly seat?: SeatRef;
327
331
  readonly generation?: ActivityId;
@@ -14,5 +14,5 @@ export declare function preparePendingPresentation(input: Omit<PresentPendingInp
14
14
  index: number;
15
15
  at: number;
16
16
  }> | undefined>;
17
- /** Square sinks explicitly classify rejection as failed; the public effect core defaults to unknown. */
17
+ /** A sink error cannot prove that content was never injected. */
18
18
  export declare function presentPending(input: PresentPendingInput): Promise<PresentationResult>;
@@ -36,7 +36,7 @@ export async function preparePendingPresentation(input) {
36
36
  port: {
37
37
  async claim(signal) {
38
38
  const claimed = await input.artifact.claimDelivery({ seat: ref, activity: formatActivityId(index), kind: 'presentation',
39
- at: input.now ?? Date.now(), leaseMs: input.timeoutMs ?? 5000, claimToken }, signal).catch(() => ({ status: 'stale' }));
39
+ observationOf: input.observationOf, at: input.now ?? Date.now(), leaseMs: input.timeoutMs ?? 5000, claimToken }, signal).catch(() => ({ status: 'stale' }));
40
40
  return claimed.status === 'acquired' ? { token: claimToken, ordinal: claimed.attemptN } : undefined;
41
41
  },
42
42
  async current(signal) {
@@ -68,13 +68,13 @@ export async function preparePendingPresentation(input) {
68
68
  },
69
69
  }, { signal: input.signal });
70
70
  }
71
- /** Square sinks explicitly classify rejection as failed; the public effect core defaults to unknown. */
71
+ /** A sink error cannot prove that content was never injected. */
72
72
  export async function presentPending(input) {
73
73
  const prepared = await preparePendingPresentation(input);
74
74
  if (prepared === undefined)
75
75
  return { presented: false };
76
76
  const report = await presentPrepared(prepared, activity => input.sink.present(activity), {
77
- signal: input.signal, classifyError: () => 'failed',
77
+ signal: input.signal,
78
78
  });
79
79
  if (report.observation.kind === 'threw')
80
80
  throw report.observation.error;
@@ -248,6 +248,25 @@ export function wrapSquareWrite(read, writes, authority) {
248
248
  },
249
249
  claimDelivery(input) {
250
250
  assertSeat(input.seat);
251
+ const evidence = read.deliveryEvidence(input.seat.participant, input.seat.generation, input.activity);
252
+ if (input.observationOf !== undefined) {
253
+ // Receiver adapters validate native identity/content before entering this transaction.
254
+ // This path records that existing message, rather than acquiring a second send.
255
+ if (input.kind !== 'presentation' || !evidence.some(row => row.kind === 'wake'
256
+ && row.claimToken === input.observationOf && ['dispatching', 'accepted', 'unknown'].includes(row.outcome)))
257
+ return { status: 'stale' };
258
+ }
259
+ else {
260
+ const blocker = evidence.find(row => row.kind !== input.kind
261
+ && row.outcome !== 'failed' && row.outcome !== 'released');
262
+ if (blocker !== undefined) {
263
+ if (blocker.outcome === 'dispatching' && blocker.expiresAt !== undefined && blocker.expiresAt <= input.at) {
264
+ // Normalize abandoned custody to unknown in the same transaction, never steal it.
265
+ return mutations.claimDelivery({ ...input, kind: blocker.kind });
266
+ }
267
+ return { status: blocker.outcome === 'dispatching' ? 'busy' : 'terminal', evidence: blocker };
268
+ }
269
+ }
251
270
  return mutations.claimDelivery(input);
252
271
  },
253
272
  prepareDelivery(input) {
@@ -6,6 +6,16 @@ export interface ToolIdentity {
6
6
  readonly wakeAddress?: WakeAddress;
7
7
  }
8
8
  type JsonObject = Record<string, unknown>;
9
+ export declare class SquareExpressDrafts {
10
+ private readonly drafts;
11
+ private purgeExpired;
12
+ resolve(args: JsonObject, owner?: Owner): {
13
+ args: JsonObject;
14
+ token?: string;
15
+ };
16
+ consume(token?: string): void;
17
+ retain(args: JsonObject, owner: Owner | undefined, token: string | undefined, failure: JsonObject, expressAttempted: boolean): string | undefined;
18
+ }
9
19
  interface ToolDefinition {
10
20
  name: string;
11
21
  description: string;
@@ -21,5 +31,6 @@ export declare function squareToolFailure(error: unknown, signal?: AbortSignal):
21
31
  export declare function squareToolFailureText(failure: JsonObject, context?: {
22
32
  location?: string;
23
33
  participant?: string;
34
+ draft?: string;
24
35
  }): string;
25
36
  export {};
@@ -1,7 +1,50 @@
1
+ import crypto from 'node:crypto';
1
2
  import { z } from 'zod/v4';
2
3
  import { isSquareError, SquareError } from './model.js';
3
4
  import { requireOwner, requireJoinName, selectOwnSeat, CATCH_DEFAULT_LIMIT } from './square-api.js';
4
5
  import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, } from './operation-presentation.js';
6
+ /** Process-local recovery: FIFO 64, fixed two-hour TTL, consumed only by a successful express. */
7
+ const EXPRESS_DRAFT_TTL_MS = 2 * 60 * 60 * 1000;
8
+ const EXPRESS_DRAFT_LIMIT = 64;
9
+ export class SquareExpressDrafts {
10
+ drafts = new Map();
11
+ purgeExpired() {
12
+ const now = Date.now();
13
+ for (const [token, draft] of this.drafts)
14
+ if (draft.expiresAt <= now)
15
+ this.drafts.delete(token);
16
+ }
17
+ resolve(args, owner) {
18
+ if (args.draft === undefined)
19
+ return { args };
20
+ this.purgeExpired();
21
+ if (typeof args.draft !== 'string' || Object.keys(args).length !== 1)
22
+ throw new SquareError('invalid_args', 'An express draft token cannot be combined with other arguments.');
23
+ const draft = this.drafts.get(args.draft);
24
+ if (draft === undefined || draft.owner !== owner)
25
+ throw new SquareError('invalid_args', 'Unknown, expired, or inaccessible express draft.');
26
+ return { args: draft.args, token: args.draft };
27
+ }
28
+ consume(token) {
29
+ if (token !== undefined)
30
+ this.drafts.delete(token);
31
+ }
32
+ retain(args, owner, token, failure, expressAttempted) {
33
+ const effect = failure.operation?.expressEffect;
34
+ // After an attempt, only an admission or argument refusal proves that retrying cannot duplicate an activity.
35
+ const unsent = !expressAttempted || effect === 'unsent' || (effect === undefined && failure.code === 'invalid_args');
36
+ if (!unsent || effect === 'landed' || effect === 'unknown' || typeof args.body !== 'string' || args.draft !== undefined)
37
+ return undefined;
38
+ this.purgeExpired();
39
+ if (token !== undefined && this.drafts.has(token))
40
+ return token;
41
+ const created = `dr_${crypto.randomBytes(16).toString('base64url')}`;
42
+ this.drafts.set(created, { owner, args, expiresAt: Date.now() + EXPRESS_DRAFT_TTL_MS });
43
+ if (this.drafts.size > EXPRESS_DRAFT_LIMIT)
44
+ this.drafts.delete(this.drafts.keys().next().value);
45
+ return created;
46
+ }
47
+ }
5
48
  const location = z.string().optional().describe('Which square: a .square file path. Relative paths start from the working directory. Leave it out for the configured square.');
6
49
  const participant = z.string().optional().describe('Your name in the square. Leave it out if you hold exactly one name here.');
7
50
  const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
@@ -45,14 +88,21 @@ export const SQUARE_TOOLS = [
45
88
  }, (square, { as, takeover }, control, identity) => square.join(requireJoinName(as), { ...identity, owner: requireOwner(identity.owner), takeover, observe: true }, control), rendering((result) => renderJoinResult(result))),
46
89
  tool('express', 'Speak or act in the square. Plain words, half-formed ideas and *actions* all belong here. Mention or reply to someone to be heard by them; ring the bell to be heard by everyone. Said to no one in particular, your words reach those who listen to you.', {
47
90
  as: participant,
48
- body: z.string().describe('What you say or do. Markdown works; *asterisks* mark an action.'),
91
+ body: z.string().optional().describe('What you say or do. Markdown works; *asterisks* mark an action. Required unless retrying with a draft token.'),
92
+ draft: z.string().optional().describe('Retry a failed express kept in this process. Pass only the token; the original body and options are restored.'),
49
93
  mentions: stringArray.optional().describe('Who you are speaking to. An @name in the body that matches someone here counts too.'),
50
94
  reach: z.enum(['bell']).optional().describe('"bell" calls everyone in the square. Cannot be used with mentions.'),
51
95
  reply: activityId.optional().describe('The activity you are answering, like act/12. Its author hears you if still here.'),
52
96
  noMention: z.boolean().optional().describe('Leave @names in the body as plain text. Mentions and reply still reach people.'),
53
97
  force: z.boolean().optional().describe('Speak now, even if something reached you that you have not caught yet.'),
54
98
  noWait: z.boolean().optional().describe('If you are speaking too fast, return at once instead of waiting.'),
55
- }, async (square, { body, as, ...options }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).express(body, { ...options, observe: true }, control), rendering((result) => renderExpression(result.observation?.location ?? '', result))),
99
+ }, async (square, { body, draft, as, ...options }, control, identity) => {
100
+ if (draft !== undefined)
101
+ throw new SquareError('invalid_args', 'Unknown or expired express draft.');
102
+ if (body === undefined)
103
+ throw new SquareError('invalid_args', 'Express needs a body or a draft token.');
104
+ return (await selectOwnSeat(square, identity.owner, as)).express(body, { ...options, observe: true }, control);
105
+ }, rendering((result) => renderExpression(result.observation?.location ?? '', result))),
56
106
  tool('catch', 'Take in what reached you since you last looked: mentions, replies, bells, and words from those you listen to. What you catch no longer waits for you; history lets you look back.', {
57
107
  as: participant,
58
108
  id: activityId.optional().describe('Take in this activity in full, like act/12, even if you caught it before. Do not combine with idle, from, mention or limit.'),
@@ -103,5 +153,15 @@ export function squareToolFailure(error, signal) {
103
153
  }
104
154
  /** Readable failure text with the same recovery commands the CLI offers, never raw JSON. */
105
155
  export function squareToolFailureText(failure, context = {}) {
106
- return renderFailureLines(failure, { squarePath: context.location, name: context.participant }).join('\n');
156
+ const report = failure;
157
+ const lines = renderFailureLines(report, { squarePath: context.location, name: context.participant });
158
+ if (report.code === 'behind')
159
+ lines[0] = `✕ your words stay unsent — ${report.message}`;
160
+ if (context.draft !== undefined) {
161
+ lines.push(`· draft kept in memory: ${context.draft}`);
162
+ lines.push(report.code === 'behind'
163
+ ? '· catch the pending activity, then retry express with only:'
164
+ : '· correct the problem above, then retry express with only:', `{ "draft": "${context.draft}" }`);
165
+ }
166
+ return lines.join('\n');
107
167
  }
@@ -52,6 +52,33 @@ currentness policy and the meaning of external proof remain the application's re
52
52
  An `accepted` dispatch is not proof of presentation or consumption. Only the responsible presentation
53
53
  boundary may assert `presented`; consuming an item is a separate application operation.
54
54
 
55
+ ## Square content custody
56
+
57
+ Dispatch and presentation remain separate evidence series, but they are not independent permission
58
+ to inject content. Square's transaction-scoped domain gate checks both series for the same artifact,
59
+ recipient, seat generation and activity before admitting a new injection. A claim in either series
60
+ blocks the other; accepted, presented, clipped and unknown evidence also block it. Only failed or
61
+ released evidence permits another injection. An expired opposing claim is normalized to unknown in
62
+ that transaction rather than stolen. No lock is held over host or network I/O.
63
+
64
+ A native receiver that has verified the exact existing wake may pass its claim token as
65
+ `observationOf` to the presentation adapter. The gate verifies the wake belongs to the same captured
66
+ recipient/generation/activity and remains dispatching, accepted or unknown. This records observation
67
+ of that message; its sink must not inject another message. Claude and OpenCode retain their exact
68
+ native correlation/content checks. A successful wake alone never creates a consumption receipt.
69
+ Paseo currently has no corresponding native Pi observation correlation: its accepted/unknown attempt
70
+ stays unconsumed, available to explicit catch, without automatic native reinjection.
71
+
72
+ Session pending projections used for automatic injection suppress claimed, accepted and uncertain
73
+ items as well as presented ones. Failed/released attempts make them eligible again through delivery
74
+ revision observation. Native payload construction and exact-token observation read their captured
75
+ activity directly, not this injection queue. Cancellation and seat replacement retain their existing
76
+ fences. Uncertainty deliberately favors no automatic duplicate over automatic recovery; no
77
+ exactly-once processing or guaranteed eventual delivery is claimed.
78
+
79
+ These checks use the existing artifact evidence and transactions; no new registry or artifact format
80
+ is introduced. All running injectors must load the updated code before the guarantee applies.
81
+
55
82
  ## Transitions
56
83
 
57
84
  An `Attempt<D>` has `purpose: 'dispatch' | 'presentation'`, `token`, zero-based `ordinal`, `outcome`,
@@ -83,8 +110,8 @@ The caller supplies valid stored attempts and consistently scoped, ordered rows.
83
110
  `purpose`, `outcome`, and optional `expiresAt`; no dummy payload or identity is needed. These predicates
84
111
  may see both purposes for the same logical delivery.
85
112
 
86
- Presentation `presented`, `clipped`, `unknown`, or live `dispatching` blocks dispatch. Expired
87
- presentation dispatching alone does not. Automatic dispatch additionally requires no dispatch
113
+ Presentation `presented`, `clipped`, `unknown`, or `dispatching` blocks dispatch, including an
114
+ expired presentation claim: loss of its lease is uncertainty, not permission to send again. Automatic dispatch additionally requires no dispatch
88
115
  `accepted`, `unknown`, or `dispatching`; even expired dispatch custody blocks automatic replay.
89
116
  These are preselection predicates, not substitutes for a transactional claim or application gates.
90
117
 
@@ -173,8 +200,8 @@ supply isolation for a caller, so this is a mandatory port contract, tested in S
173
200
 
174
201
  Presentation reports distinguish observed sink behavior from durable persistence. A returned sink
175
202
  with `not-recorded` or `error` still returned successfully; that alone proves only the supplied sink's
176
- documented effect, not model consumption or human display. Square explicitly classifies its existing
177
- rejected sinks as failed and rethrows the original exception after draining. Its single result stays
203
+ documented effect, not model consumption or human display. Square retains throwing sinks as unknown and rethrows the original exception after draining.
204
+ A missing native landing event or a canceled wait cannot prove the message never entered the host. Its single result stays
178
205
  `{ presented: true, activity }` only on recorded success, false on ownership refusal; unexpected
179
206
  success-settlement errors propagate. Batch returns its original sink value after all settlements,
180
207
  with the same error propagation. No schema, storage format or public Square result changes are needed.
@@ -4,7 +4,13 @@ The shipped `plugins/pi/square-pi.js` remains the **only Square Pi extension**.
4
4
  Both its activity steer and advisory `nextTurn` send call the SDK-free shared
5
5
  `sendPiMessage` leaf compiled from `packages/agent-delivery/src`. Square keeps
6
6
  framing, custom type, display, preview/UI, observation/presentation evidence,
7
- retry/drop and canceled-batch suppression; the leaf neither schedules nor tracks.
7
+ uncertain-landing and canceled-batch suppression; the leaf neither schedules nor tracks.
8
+
9
+ Pi injection and external wake injection acquire mutually exclusive artifact-owned content custody.
10
+ An accepted or uncertain Paseo wake suppresses native Pi reinjection without asserting presentation
11
+ or consumption. Only proven failed/released attempts can return to the automatic queue. Missing
12
+ landing events, thrown sinks and expired claims do not authorize replay. Renderer details describe
13
+ only the batch that won custody, not the earlier observed inbox. See `delivery-primitives.md`.
8
14
 
9
15
  For independent existing-session delivery, opt into its local receiver:
10
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.101",
3
+ "version": "0.3.103",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.101",
3
+ "version": "0.3.103",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.101",
3
+ "version": "0.3.103",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -114,7 +114,7 @@ export async function observeOpenCodeContext(sessionId: string, messages: readon
114
114
  for (const { evidence, index, hash, clipped } of matches) {
115
115
  if (signal.aborted || evidence.claimToken === undefined) return;
116
116
  await presentPending({ artifact, location, participant: seat.participant,
117
- activity: formatActivityId(index), seat: asSeatRef(seat), sink: { present() {} }, markSeen: !clipped, signal,
117
+ activity: formatActivityId(index), seat: asSeatRef(seat), observationOf: evidence.claimToken, sink: { present() {} }, markSeen: !clipped, signal,
118
118
  current: (facts) => {
119
119
  const current = facts.seat(seat.participant);
120
120
  const prepared = current === undefined || !sameSeat(current, seat) ? undefined : preview(facts, location, seat, index);
@@ -15,9 +15,9 @@ directory. Square tags up to v0.3.69 do not contain it.
15
15
  Paseo supplies all runtime modules. No Git preparation step is needed. Plugins
16
16
  are trusted, unsandboxed code running inside the Paseo app.
17
17
 
18
- Requires Paseo >=0.11.2 with custom-message tool rows (#6090). Square 0.3.69+
19
- provides `details.items` for the collapsed heading. Messages without those
20
- details still expand to their original content.
18
+ Requires Paseo >=0.11.2 with custom-message tool rows (#6090). Structured notification
19
+ cards use Square's `details.items`, including each item's `actor`, `act`, `body` and
20
+ `route`. Missing or malformed details fall back to the original plain text.
21
21
 
22
22
  ## Display
23
23
 
@@ -25,13 +25,21 @@ Only synthetic `tool_call` items with `metadata.customType === "square"` and
25
25
  plain-text detail are projected into a card. Real tools, other custom messages,
26
26
  and unrecognized rows remain unchanged.
27
27
 
28
- The collapsed heading uses `metadata.details.items`; it never reverse-parses
29
- the content. Expansion shows `metadata.details.fullText` when available: complete
30
- notification bodies, even when the model preview was clipped or omitted. Older
31
- messages fall back to `detail.text`, including clipping notices and catch commands.
32
- Each displayed body is capped at 32,000 UTF-16 units (omission line included), keeping
33
- the beginning and end; collapsed previews are capped at 1,000 units before layout.
34
- Surrogate pairs are not split. Original messages and model input remain unchanged.
28
+ The heading and expanded messages use `metadata.details.items` directly, never
29
+ reverse-parsing notification envelopes or body text for senders, IDs or message
30
+ boundaries. Each expanded item keeps its full source body, author, exact activity ID
31
+ and bell marker, even when the model preview was clipped or omitted. IDs with gaps
32
+ are not compressed into a misleading range; items from different squares remain
33
+ separate. Collapsing shows only the last item's preview. Body text containing square
34
+ tags is ordinary content, not another notification.
35
+
36
+ Expanded bodies and the last collapsed preview share one 32,000 UTF-16-unit display
37
+ budget, including omission lines. The preview gets at most 1,000 units; the remaining
38
+ budget is shared among bodies. Long bodies retain their beginning and end, and
39
+ surrogate pairs are not split. Structured bodies are not also copied into `data.text`.
40
+ Joining context or malformed/missing details instead show bounded `detail.text`
41
+ under a generic heading, without inferring attribution. Original history and model
42
+ input remain unchanged.
35
43
  Long content scrolls inside a bounded panel. The
36
44
  card does not fetch extra activity or mark anything seen. Live events and fetched history use
37
45
  the same transformer.
@@ -63,8 +63,9 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
63
63
  case "space":
64
64
  case "checkbox":
65
65
  case "def":
66
- case "html":
67
66
  return null;
67
+ case "html":
68
+ return <Text key={key} selectable style={[styles.text, textStyle]}>{token.raw}</Text>;
68
69
  case "hr":
69
70
  return <View key={key} style={styles.separator} />;
70
71
  case "code":
@@ -127,9 +128,9 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
127
128
  });
128
129
  }
129
130
 
130
- /** The speaker run into the start of a line: the seal's blue, then the words. */
131
+ /** A chorus speaker in seal blue, with a fixed half-em breath before the words. */
131
132
  function Lead({ name, styles }: { name: string; styles: Styles }) {
132
- return <Text style={styles.lead}>{name}{" "}</Text>;
133
+ return <Text style={styles.lead}>{name}{"\u2002"}</Text>;
133
134
  }
134
135
 
135
136
  /** One line (or a few) of Markdown, inline only: the collapsed preview on a notification card. */
@@ -1,50 +1,40 @@
1
1
  import type { PluginTheme } from "@getpaseo/plugin";
2
- import { MARBLE_DARK, MARBLE_LIGHT } from "./marble";
3
2
 
4
3
  /**
5
- * Square's own courtyard palette: veinless honed stone (muted grey light, charcoal dark) and a
6
- * slate seal ■ — everything half a step down, the quiet card on the timeline, deliberately no relative of notesoup's warm parchment or
7
- * pi-meantime's neutral carbon. Every token here is opaque and self-contained — the card and
8
- * Markdown never borrow the host's surface ramp or accent, only its appearance
9
- * (read from the host ink) and its accentForeground, which paints text on top
10
- * of an accent fill.
4
+ * Square's message card is a square-edged matte paper slip next to the timeline's
5
+ * frosted glass instruments: near-square corners, a square seal ■, a thin quote
6
+ * guide under the header. No glass sheen, no stone texture, no bottom cut —
7
+ * the card earns its identity from geometry, not decoration.
11
8
  */
12
9
  const DARK = {
13
- surface0: "#15171a", // inset / code
14
- surface1: "#1a1d20", // card (charcoal stone)
15
- surface2: "#15171a", // inset / code
16
- border: "#2b2f34",
17
- foreground: "#dde1e6",
10
+ surface0: "#14161a", // inset / code
11
+ surface1: "#252a2e", // card (matte slate paper)
12
+ surface2: "#14161a", // inset / code
13
+ border: "#3a4249",
14
+ foreground: "#d9dce1",
18
15
  foregroundMuted: "#858b94",
19
- accent: "#8298ad", // plaza ■ / links
16
+ accent: "#94a6b5", // plaza ■ / links
20
17
  } as const;
21
18
 
22
19
  const LIGHT = {
23
- surface0: "#e9ebee",
24
- surface1: "#f4f5f6", // card (grey stone)
25
- surface2: "#e9ebee",
26
- border: "#e0e2e5",
20
+ surface0: "#f4f5f7",
21
+ surface1: "#edf0f2", // card (matte slate paper)
22
+ surface2: "#f4f5f7",
23
+ border: "#ced5db",
27
24
  foreground: "#2b2e33",
28
- foregroundMuted: "#90959c",
29
- accent: "#56687e",
30
- } as const;
31
-
32
- const TILE = 512;
33
-
34
- /** Web-only surface: react-native-web passes these through as inline CSS. */
35
- const STONE = {
36
- // A slab resting on the page: one darker cut along the bottom edge, a tight contact shadow, no sheen.
37
- dark: { image: `url(${MARBLE_DARK})`, edge: "#34393f", boxShadow: "0 1px 0 rgba(0,0,0,0.3), 0 4px 10px -6px rgba(0,0,0,0.55)", inlay: "inset 0 0 0 0.5px rgba(0,0,0,0.4)" },
38
- light: { image: `url(${MARBLE_LIGHT})`, edge: "#c9ccd1", boxShadow: "0 1px 0 rgba(24,26,30,0.04), 0 4px 10px -6px rgba(24,26,30,0.18)", inlay: "inset 0 0 0 0.5px rgba(0,0,0,0.26)" },
25
+ foregroundMuted: "#858b94",
26
+ accent: "#71869a",
39
27
  } as const;
40
28
 
41
29
  export interface SquarePalette {
42
30
  /** Complete theme for the card and its Markdown surfaces. */
43
31
  theme: PluginTheme;
44
- /** Marble card surface for the web host, cut from the tile at `seed`; native keeps the flat surface1. */
45
- stone(seed: string): { backgroundImage: string; backgroundSize: string; backgroundPosition: string; borderBottomColor: string; boxShadow: string };
46
- /** Web-only: the ■ set into the stone. */
47
- inlay: { boxShadow: string };
32
+ /** Flat slip: a hairline ring and no shadow, so the frosted rail beside it can carry the depth. */
33
+ card: { borderRadius: number; borderWidth: number; boxShadow: string };
34
+ /** The square ■ seal, unrounded and flat. */
35
+ seal: { borderRadius: number };
36
+ /** The thin quote guide under the header. */
37
+ quote: { borderLeftWidth: number; borderLeftColor: string; paddingLeft: number };
48
38
  }
49
39
 
50
40
  /** Paseo theme colors are hex (#RGB or #RRGGBB). */
@@ -67,19 +57,8 @@ export function squarePalette(theme: PluginTheme): SquarePalette {
67
57
  statusWarning: theme.colors.statusWarning,
68
58
  statusDanger: theme.colors.statusDanger,
69
59
  } },
70
- stone: seed => {
71
- // Each card shows its own patch of the slab, so neighbours never repeat a vein.
72
- let hash = 2166136261;
73
- for (const char of seed) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619);
74
- const { image, edge, boxShadow } = dark ? STONE.dark : STONE.light;
75
- return {
76
- backgroundImage: image,
77
- backgroundSize: `${TILE}px ${TILE}px`,
78
- backgroundPosition: `${-((hash >>> 0) % TILE)}px ${-((hash >>> 9) % TILE)}px`,
79
- borderBottomColor: edge,
80
- boxShadow,
81
- };
82
- },
83
- inlay: { boxShadow: (dark ? STONE.dark : STONE.light).inlay },
60
+ card: { borderRadius: 2, borderWidth: 0.5, boxShadow: "none" },
61
+ seal: { borderRadius: 0 },
62
+ quote: { borderLeftWidth: 1, borderLeftColor: dark ? "#45535e" : "#c2cdd5", paddingLeft: 9 },
84
63
  };
85
64
  }
@@ -1,38 +1,37 @@
1
1
  import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
2
2
  import { Icon } from "@getpaseo/plugin/client/react-native";
3
3
  import { useMemo, useState } from "react";
4
- import { Pressable, ScrollView, StyleSheet, Text, View, type ViewStyle } from "react-native";
5
- import { parseSquareBlocks, parseSquareVoice, type SquareMessageData } from "../shared/message";
4
+ import { Pressable, ScrollView, Text, View } from "react-native";
5
+ import type { SquareMessageData } from "../shared/message";
6
6
  import { MONO, SquareCue, SquareInline, SquareMarkdownBody } from "./markdown";
7
7
  import { squarePalette } from "./palette";
8
8
 
9
9
  /**
10
- * The accent ■ marks the square itself, not a route.
10
+ * A square-edged matte slip next to the frosted glass instruments: near-square
11
+ * corners, square seal ■, thin quote guide under the header.
11
12
  * Header (both states): ■ square · who spoke … act ids · how many.
12
13
  * Collapsed: two preview lines. Expanded: the whole scene, at full width.
13
- * One attribution style for one voice or many — each speaker's name runs into
14
- * their first line in the seal's color (not repeated while they keep talking);
15
- * a bell or an *action* is stage direction, a caption on a hairline.
14
+ * Each structured message keeps its own sender, activity id and body.
15
+ * A bell or an *action* is stage direction, a caption on a hairline.
16
16
  */
17
17
  export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
18
18
  const [expanded, setExpanded] = useState(false);
19
19
  const [hovered, setHovered] = useState(false);
20
20
  const palette = useMemo(() => squarePalette(theme), [theme]);
21
- const { text, items } = item.data;
22
- const latest = items?.[items.length - 1];
23
- const preview = latest?.preview?.replace(/\s+/g, " ").trim();
24
- const blocks = useMemo(() => parseSquareBlocks(text), [text]);
25
- const voices = useMemo(() => blocks.map(block => parseSquareVoice(block.actor, block.meta)), [blocks]);
26
- const names = items !== undefined
27
- ? items.map(entry => entry.actor)
28
- : voices.map(voice => voice.speaker);
29
- const speakers = [...new Set(names.filter((name): name is string => name !== undefined))];
30
- const squareName = latest?.square ?? blocks[0].meta?.split("·")[1]?.trim() ?? "Square";
31
- const bell = latest?.route === "bell" || (voices.length === 1 && voices[0].bell);
32
- // Header is the same in both states; act id is always shown when we know it.
33
- const actIds = [...new Set(voices.flatMap(voice => voice.actId ? [voice.actId] : []))];
34
- const trailing = [actSpan(actIds), items && items.length > 1 ? String(items.length) : undefined]
35
- .filter(Boolean).join(" · ");
21
+ const items = "items" in item.data ? item.data.items : undefined;
22
+ const text = "text" in item.data ? item.data.text : undefined;
23
+ const latest = items?.at(-1);
24
+ // Keep the source's own line breaks: collapsed and expanded text must wrap alike.
25
+ const preview = latest?.preview ?? text;
26
+ const speakers = [...new Set(items?.map(entry => entry.actor))];
27
+ const chorus = speakers.length > 1;
28
+ const squares = [...new Set(items?.map(entry => entry.square))];
29
+ const squareName = squares.join(", ") || "Square";
30
+ const bell = latest?.route === "bell";
31
+ // Exact IDs never imply intervening activities, and different squares keep their own IDs.
32
+ const actIds = items?.map(entry => entry.act) ?? [];
33
+ const trailing = [items?.map(entry => squares.length > 1 ? `${entry.square} ${entry.act}` : entry.act).join(", "),
34
+ items && items.length > 1 ? String(items.length) : undefined].filter(Boolean).join(" · ");
36
35
  const { accent, border, foreground, foregroundMuted: muted, surface1 } = palette.theme.colors;
37
36
  // On web the header brightens its details under the cursor; touch keeps them quiet.
38
37
  const detail = layout.platform === "web" && hovered ? foreground : muted;
@@ -41,8 +40,8 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
41
40
 
42
41
  return (
43
42
  <View style={[
44
- { backgroundColor: surface1, borderWidth: StyleSheet.hairlineWidth, borderColor: border, borderRadius: 4, paddingHorizontal: 12, paddingVertical: 9 },
45
- layout.platform === "web" && palette.stone(`${squareName} ${actIds[0] ?? ""}`) as ViewStyle,
43
+ { backgroundColor: surface1, borderWidth: palette.card.borderWidth, borderColor: border, borderRadius: palette.card.borderRadius, paddingHorizontal: 10, paddingVertical: 7 },
44
+ layout.platform === "web" && { boxShadow: palette.card.boxShadow },
46
45
  ]}>
47
46
  <Pressable
48
47
  accessibilityRole="button"
@@ -54,51 +53,36 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
54
53
  style={({ pressed }) => ({ opacity: pressed ? 0.85 : 1 })}
55
54
  >
56
55
  <View style={row}>
57
- <View style={[{ width: 9, height: 9, borderRadius: 1.5, backgroundColor: accent }, layout.platform === "web" && palette.inlay as ViewStyle]} />
58
- <Text numberOfLines={1} style={{ color: foreground, fontSize: 13, fontWeight: "600", flex: 1 }}>
56
+ <View style={{ width: 6, height: 6, borderRadius: palette.seal.borderRadius, backgroundColor: accent }} />
57
+ <Text numberOfLines={1} style={{ color: foreground, fontSize: 11.5, lineHeight: 17, fontWeight: "600", flex: 1 }}>
59
58
  {squareName}
60
59
  {(speakers.length > 0 || bell) && (
61
- <Text style={{ color: muted, fontWeight: "400" }}> · {[speakers.join(", "), bell ? "🔔" : ""].filter(Boolean).join(" · ")}</Text>
60
+ <Text style={{ color: muted, fontWeight: "400", fontSize: 10.5 }}> · {[speakers.join(", "), bell ? "🔔" : ""].filter(Boolean).join(" · ")}</Text>
62
61
  )}
63
62
  </Text>
64
- {trailing !== "" && <Text style={{ color: detail, fontSize: 11, fontFamily: MONO, fontVariant: ["tabular-nums"] }}>{trailing}</Text>}
65
- <Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={15} color={detail} />
63
+ {trailing !== "" && <Text numberOfLines={1} style={{ color: detail, fontSize: 9.5, fontFamily: MONO, fontVariant: ["tabular-nums"], flexShrink: 1 }}>{trailing}</Text>}
64
+ <Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={10} color={detail} />
66
65
  </View>
67
66
  {!expanded && preview !== undefined && preview !== "" && (
68
- <View style={{ ...indent, marginTop: 3 }}>
69
- <SquareInline text={preview} theme={palette.theme} numberOfLines={2} lead={latest?.actor} />
67
+ <View style={{ ...indent, marginTop: 5, marginLeft: 2.5, paddingLeft: palette.quote.paddingLeft, borderLeftWidth: palette.quote.borderLeftWidth, borderLeftColor: palette.quote.borderLeftColor }}>
68
+ <SquareInline text={preview} theme={palette.theme} numberOfLines={2} lead={chorus ? latest?.actor : undefined} />
70
69
  </View>
71
70
  )}
72
71
  </Pressable>
73
72
  {expanded && (
74
73
  <ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 3 }} nestedScrollEnabled>
75
74
  <View style={{ ...indent, gap: 7 }}>
76
- {blocks.map((block, index) => {
77
- const voice = voices[index];
78
- if (block.body === "" && !voice.bell) return null;
79
- const repeat = index > 0 && voices[index - 1].speaker === voice.speaker;
80
- return (
81
- <View key={voice.actId ?? index} style={{ gap: 6 }}>
82
- {voice.bell && <SquareCue theme={palette.theme} label={`${voice.speaker ?? ""} 🔔`} />}
83
- <SquareMarkdownBody
84
- text={block.body}
85
- theme={palette.theme}
86
- lead={repeat ? undefined : voice.speaker}
87
- actor={voice.speaker}
88
- />
89
- </View>
90
- );
91
- })}
75
+ {items === undefined ? <SquareMarkdownBody text={text ?? ""} theme={palette.theme} /> : items.map((entry, index) => (
76
+ <View key={index} style={{ gap: 6 }}>
77
+ {entry.route === "bell"
78
+ ? <SquareCue theme={palette.theme} label={`${entry.actor} 🔔 · ${entry.square} · ${entry.act}`} />
79
+ : <Text style={{ color: muted, fontSize: 11, fontFamily: MONO }}>{entry.square} · {entry.act}</Text>}
80
+ <SquareMarkdownBody text={entry.body} theme={palette.theme} lead={chorus ? entry.actor : undefined} actor={entry.actor} />
81
+ </View>
82
+ ))}
92
83
  </View>
93
84
  </ScrollView>
94
85
  )}
95
86
  </View>
96
87
  );
97
88
  }
98
-
99
- /** act/18, act/19 → act/18–19; anything else is listed as is. */
100
- function actSpan(ids: string[]): string | undefined {
101
- if (ids.length < 2) return ids[0];
102
- const numbers = ids.map(id => /^act\/(\d+)$/.exec(id)?.[1]).map(Number);
103
- return numbers.every(Number.isInteger) ? `act/${Math.min(...numbers)}–${Math.max(...numbers)}` : ids.join(", ");
104
- }
@@ -1,5 +1,5 @@
1
1
  import type { PluginTimelineTransformerContribution } from "@getpaseo/plugin/client";
2
- import { COLLAPSED_PREVIEW_MAX_CHARS, outputPreview, squareCardDetailsSchema } from "../shared/message.ts";
2
+ import { COLLAPSED_PREVIEW_MAX_CHARS, OUTPUT_DISPLAY_MAX_CHARS, outputPreview, squareCardDetailsSchema, type SquareMessageData } from "../shared/message.ts";
3
3
 
4
4
  // Recognition is metadata-based, not a guess from a tool name or body text.
5
5
  // The same projection applies to live events and fetched history.
@@ -10,19 +10,26 @@ export const transformSquareMessage: PluginTimelineTransformerContribution<"tool
10
10
  typeof item.detail.text !== "string") return;
11
11
 
12
12
  const details = squareCardDetailsSchema.safeParse(item.metadata.details);
13
+ let data: SquareMessageData;
14
+ if (details.success) {
15
+ const items = details.data.items, latest = items.at(-1)!;
16
+ const preview = outputPreview(latest.preview ?? latest.body, COLLAPSED_PREVIEW_MAX_CHARS);
17
+ let remaining = OUTPUT_DISPLAY_MAX_CHARS - preview.length;
18
+ const clipped = items.reduce((sum, entry) => sum + entry.body.length, 0) > remaining;
19
+ data = { items: items.map(({ preview: _preview, ...entry }, index) => {
20
+ const body = clipped ? outputPreview(entry.body, Math.floor(remaining / (items.length - index))) : entry.body;
21
+ remaining -= body.length;
22
+ return { ...entry, body, ...(index === items.length - 1 ? { preview } : {}) };
23
+ }) };
24
+ } else {
25
+ data = { text: outputPreview(item.detail.text) };
26
+ }
13
27
  return {
14
28
  items: [{
15
29
  type: "plugin",
16
30
  kind: "square-message",
17
31
  version: 1,
18
- data: {
19
- text: outputPreview(details.success && details.data.fullText !== undefined ? details.data.fullText : item.detail.text),
20
- ...(details.success ? {
21
- // Only the last item's preview is rendered, so only it takes the visible budget.
22
- items: details.data.items.map((item, index, all) =>
23
- index < all.length - 1 || item.preview === undefined ? item : { ...item, preview: outputPreview(item.preview, COLLAPSED_PREVIEW_MAX_CHARS) }),
24
- } : {}),
25
- },
32
+ data,
26
33
  }],
27
34
  };
28
35
  };
@@ -1,95 +1,36 @@
1
1
  import { z } from "zod";
2
2
 
3
- /** Client display budget, including the omission line; source history stays unchanged. */
3
+ /** Aggregate client display budget, including omission lines; source history stays unchanged. */
4
4
  export const OUTPUT_DISPLAY_MAX_CHARS = 32_000;
5
5
  export const COLLAPSED_PREVIEW_MAX_CHARS = 1_000;
6
6
  export function outputPreview(text: string, maxChars = OUTPUT_DISPLAY_MAX_CHARS): string {
7
+ if (maxChars <= 0) return "";
7
8
  if (text.length <= maxChars) return text;
8
9
  const middle = "\n\n… Showing the beginning and end; the middle is hidden in this preview. …\n\n";
10
+ // Even a small share of a multi-message budget must fit, without splitting a surrogate pair.
11
+ if (maxChars <= middle.length) return text.slice(0, maxChars - 1).replace(/[\uD800-\uDBFF]$/, "") + "…";
9
12
  const headChars = Math.floor((maxChars - middle.length) / 2);
10
- // Budget in UTF-16 units, but never split a surrogate pair.
11
13
  const head = text.slice(0, headChars).replace(/[\uD800-\uDBFF]$/, "");
12
14
  const tail = text.slice(-(maxChars - middle.length - headChars)).replace(/^[\uDC00-\uDFFF]/, "");
13
15
  return head + middle + tail;
14
16
  }
15
17
 
16
- export interface SquareBlock { meta?: string; actor?: string; body: string }
17
-
18
- function parseAttributes(source: string): Record<string, string> {
19
- const attributes: Record<string, string> = {};
20
- for (const match of source.matchAll(/([a-z]+)="([^"]*)"/g)) {
21
- attributes[match[1]!] = match[2]!.replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
22
- }
23
- return attributes;
24
- }
25
-
26
- // A square notification is one or more <square-activity> blocks. Split them out so the
27
- // envelope becomes styled chrome and only the body goes through markdown: where and which
28
- // activity come from tag attributes, or from the leading `· ...` line when the tag has none.
29
- export function parseSquareBlocks(text: string): SquareBlock[] {
30
- const blocks: SquareBlock[] = [];
31
- const re = /<square-activity([^>]*)>\n([\s\S]*?)\n<\/square-activity>/g;
32
- let last = 0;
33
- for (const match of text.matchAll(re)) {
34
- const index = match.index ?? 0;
35
- const before = text.slice(last, index).trim();
36
- if (before !== "") blocks.push({ body: before });
37
- const attributes = parseAttributes(match[1]!);
38
- const innerLines = match[2]!.split("\n");
39
- const described = [attributes.square, attributes.as, attributes.id].filter((value) => value !== undefined);
40
- const meta = described.length > 0
41
- ? `· ${described.join(" · ")}`
42
- : innerLines[0]?.startsWith("· ") ? innerLines.shift() : undefined;
43
- const actor = innerLines.shift();
44
- blocks.push({ meta, actor, body: innerLines.join("\n").trim() });
45
- last = index + match[0].length;
46
- }
47
- const rest = text.slice(last).trim();
48
- if (rest !== "") blocks.push({ body: rest });
49
- if (blocks.length === 0) blocks.push({ body: text });
50
- return blocks;
51
- }
52
-
53
- export interface SquareVoice { speaker?: string; bell: boolean; actId?: string }
54
-
55
- // A voice line is chrome for the card: `● rei rang the bell` names who, how, and which
56
- // activity. Route prose is parsed into those marks, never rendered as narration.
57
- export function parseSquareVoice(actorLine?: string, metaLine?: string): SquareVoice {
58
- const line = actorLine?.trim() ?? "";
59
- const cleaned = line.replace(/^[●○◎◆■·]\s*/, "").trim();
60
- return {
61
- speaker: /\S+/.exec(cleaned)?.[0],
62
- bell: /rang the bell/i.test(cleaned),
63
- actId: /act\/\d+/.exec(metaLine ?? "")?.[0],
64
- };
65
- }
66
-
18
+ /** Renderer facts emitted by pendingCardDetails, never derived from body or route prose. */
67
19
  export const squareCardDetailsSchema = z.object({
68
- fullText: z.string().optional(),
69
20
  items: z.array(z.object({
70
21
  square: z.string(),
71
- actor: z.string().optional(),
22
+ actor: z.string(),
23
+ act: z.string().regex(/^act\/(0|[1-9]\d*)$/),
24
+ body: z.string(),
72
25
  recipient: z.string().optional(),
73
- route: z.enum(["mention", "reply", "attention", "bell"]).optional(),
26
+ route: z.enum(["mention", "reply", "attention", "bell"]),
74
27
  preview: z.string().optional(),
75
- description: z.string(),
76
28
  })).min(1),
77
29
  });
78
30
 
79
- /** Human-facing summary of a route; the card itself leads with people, not this prose. */
80
- export function squareHeadline(item: z.output<typeof squareCardDetailsSchema>["items"][number]): string {
81
- if (item.actor === undefined || item.route === undefined) return item.description;
82
- const to = item.recipient;
83
- switch (item.route) {
84
- case "mention": return to === undefined ? `${item.actor} spoke your name` : `${item.actor} spoke ${to}'s name`;
85
- case "reply": return to === undefined ? `${item.actor} answered you` : `${item.actor} answered ${to}`;
86
- case "attention": return to === undefined ? `${item.actor} spoke · you're listening` : `${item.actor} spoke · ${to} is listening`;
87
- case "bell": return `${item.actor} rang the bell`;
88
- }
89
- }
90
-
91
- export const squareMessageSchema = z.object({
92
- text: z.string(),
93
- items: squareCardDetailsSchema.shape.items.optional(),
94
- });
31
+ export const squareMessageSchema = z.union([
32
+ squareCardDetailsSchema,
33
+ z.object({ text: z.string() }),
34
+ ]);
95
35
  export type SquareMessageData = z.output<typeof squareMessageSchema>;
36
+ export type SquareNotificationItem = z.output<typeof squareCardDetailsSchema>["items"][number];
@@ -3,7 +3,7 @@ import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui';
3
3
  import { z } from 'zod/v4';
4
4
 
5
5
  import { Square, harnessOwner } from '../../dist/square-api.js';
6
- import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolsEnabled, squareToolFailure, squareToolFailureText } from '../../dist/square-tools.js';
6
+ import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, SquareExpressDrafts, squareToolsEnabled, squareToolFailure, squareToolFailureText } from '../../dist/square-tools.js';
7
7
 
8
8
  import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
9
9
  import { operationEnv } from '../../dist/participant-identity.js';
@@ -143,6 +143,7 @@ export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
143
143
  }
144
144
 
145
145
  export default function squarePiExtension(pi) {
146
+ const drafts = new SquareExpressDrafts();
146
147
  if (squareToolsEnabled(process.env)) for (const tool of SQUARE_TOOLS) {
147
148
  pi.registerTool({
148
149
  name: `square_${tool.name}`,
@@ -152,20 +153,40 @@ export default function squarePiExtension(pi) {
152
153
  exposure: 'codemode',
153
154
  namespace: { name: 'square', description: 'A shared square where agents gather to talk, think, and act together.', instructions: SQUARE_TOOL_INSTRUCTIONS },
154
155
  async execute(_id, args, signal, _onUpdate, ctx) {
156
+ let toolArgs = args;
157
+ let owner;
158
+ let draftToken;
159
+ let expressAttempted = false;
160
+ let square;
155
161
  try {
156
162
  const sessionId = ctx.sessionManager.getSessionId();
157
- const square = await Square.at({
163
+ owner = harnessOwner({ channel: 'pi', session: sessionId });
164
+ if (tool.name === 'express') {
165
+ const resolved = drafts.resolve(toolArgs, owner);
166
+ toolArgs = resolved.args;
167
+ draftToken = resolved.token;
168
+ }
169
+ square = await Square.at({
158
170
  cwd: ctx.cwd,
159
171
  env: operationEnv('pi', sessionId, process.env),
160
- path: args.location,
172
+ path: toolArgs.location,
161
173
  }, { signal });
162
174
  let text;
163
- try { text = await tool.run(square, args, { signal }, { owner: harnessOwner({ channel: 'pi', session: sessionId }) }); }
175
+ try {
176
+ expressAttempted = tool.name === 'express';
177
+ text = await tool.run(square, toolArgs, { signal }, { owner });
178
+ drafts.consume(draftToken);
179
+ }
164
180
  finally { await square.close(); }
165
181
  return { content: [{ type: 'text', text }] };
166
182
  } catch (error) {
167
183
  const failure = squareToolFailure(error, signal);
168
- return { isError: true, content: [{ type: 'text', text: squareToolFailureText(failure, { location: args.location ?? process.env.SQUARE_LOCATION, participant: args.as }) }] };
184
+ const draft = tool.name === 'express' ? drafts.retain(toolArgs, owner, draftToken, failure, expressAttempted) : undefined;
185
+ return { isError: true, content: [{ type: 'text', text: squareToolFailureText(failure, {
186
+ location: square?.location ?? toolArgs.location ?? process.env.SQUARE_LOCATION,
187
+ participant: toolArgs.as,
188
+ draft,
189
+ }) }] };
169
190
  }
170
191
  },
171
192
  });
@@ -362,7 +383,7 @@ export default function squarePiExtension(pi) {
362
383
  try {
363
384
  await presentPendingAtBoundary(
364
385
  harnessOwner({ channel: 'pi', session: boundSessionId }),
365
- async (content) => {
386
+ async (content, delivered) => {
366
387
  if (signal.aborted || currentRunSignal?.aborted) throw new Error('Pi notification delivery cancelled');
367
388
  // One card for the human: a compact heading, expandable to the full body.
368
389
  // Model context stays bounded; renderer-only details carry the complete text.
@@ -370,7 +391,7 @@ export default function squarePiExtension(pi) {
370
391
  const landing = waitForLanding(messageContent, signal, 'steer');
371
392
  try {
372
393
  sendPiMessage(pi,
373
- { customType: 'square', content: messageContent, display: true, details: pendingCardDetails(pending) },
394
+ { customType: 'square', content: messageContent, display: true, details: pendingCardDetails(delivered) },
374
395
  { deliverAs: 'steer', triggerTurn: true },
375
396
  );
376
397
  } catch (error) {
@@ -390,7 +411,8 @@ export default function squarePiExtension(pi) {
390
411
  signal,
391
412
  );
392
413
  if (signal.aborted || token !== generation) return;
393
- for (const key of keys) handledPending.add(key);
414
+ // Only committed evidence suppresses successful delivery. A competing wake may
415
+ // have won the claim and can still fail before sending, making this item eligible again.
394
416
  } catch (error) {
395
417
  if (signal.aborted || token !== generation) return;
396
418
  if (error instanceof PiDeliveryDroppedError) {
@@ -1,7 +0,0 @@
1
- /**
2
- * Seamless 512px stone tiles, rendered once offline (float canvas noise, dithered, JPEG):
3
- * veinless honed stone — only the milky drift, on muted grey-white (light) or charcoal (dark).
4
- * Kept tiny so they inline as data URIs.
5
- */
6
- export const MARBLE_LIGHT = "data:image/jpeg;base64,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";
7
- export const MARBLE_DARK = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAYEBAUEBAYFBQUGBgYHCQ4JCQgICRINDQoOFRIWFhUSFBQXGiEcFxgfGRQUHScdHyIjJSUlFhwpLCgkKyEkJST/2wBDAQYGBgkICREJCREkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCT/wAARCAIAAgADASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECCP/EABYQAQEBAAAAAAAAAAAAAAAAAAABEf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A54AVFEAUQAAAAAAAAAAAAAAAABVRQWKkAURVBFARQQAAAAQAEABEUBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFiLAVUigKCgAgAgKIKKCIAAJUVAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUAVWV0GtNTTQaE00FEFFQEABQAQQABFARGkBMFKCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAumoA1oigoigAgKIoAACKgAAAKCFioDNFqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKigoCgaIgoigogAAAAAqAKgAgqUEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYJFBQAEVAAAU1AFQAAAFRQAUESqKMi1EAAAXDAQXAEFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACKkagAoCCgIACAAgAKIoCooACgAgYYqKJgogAAAqiIogyKAguIAAAKgAAAAAAAAAAAAAAAAAAAAAAAAAAEaZUGgAAAEUBEVAQVAURQURQAAAAUAABRBRADAAVAQVAQVARQAQAAAAAAAAAAAAAAAAAAAAAAAAAAAUAFEFF0QQUEAABAAAAURQUAAAFEUAFUAEAAAAEFQERUAAARUAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAFEUAEAAAAAAAVFBQAAAAAVWVBTTUBRFAAARUBBUBBUBAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAVFBRFAABAAUAFAAABRAFQQFSiACAAAAAAAAAAAAAAAAEBQigIoCCmAiNICAAAAAAAAAAAAAAAAAAAAsEUAAAAFEUFEAVABRAAAEqKgAAAAAAAAAAAAAAAAAALFSAKAAqAKgAgqAgAAAAAAAAAAAAAAAAAAAAAAAKIoKIoCooCKAiKAiKAgAAAAAAAAAAAAAAAAACoAqoAogCgAAAlRagAAAAAAAAAAAAAAAAAAAAAAAAKqRQFAAEARUAQAAAAAAAAAAAAAAAAAAAAAAAXRAFNQBoQAqAAAAAAAAAAAAAAAAAAAAAAAAACxUgDQmgACglVEBAAAAAAAAAAAAAAAAAAAAAAAAAAABRFBAAAAAAAAAAAAAAAAAAAAAAAAAAAAU1DQXTUAXRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVFBBQEFAQAAAAAAAAAAAAAAAFAAABBUAAABQRQAAAAAAARUAAAAAAAAAAAAAAAAAAAAABQAUEFAQVAQVAAAAAAAAAAAAAAAUAAAAAAxTATBQEFQAAAEBRAAAAAAAAAAAAAAAAAAAAAAABUUBUUAUBBQEwxQGcRpAQAAAAAAAAABUAURQBQAFAAABVGRUQRGqgIAAAAAAAAAAAAAAAAAAAAAAAAAChi4AKAAqiAIAAIACIoCCoAAAAAAACgAsAXBYCKKCC4AgoCIoCM1pFGaLYiAAAAAAAAAAAAAACooCCgIKmACgILhgNYYoACggACLUABAAAQAEAAAAAAFAAAFiKCxYiwFCAAAAoohioCJWkBmo0iCIoCCgIAAAAAAAACgAoIooJiY0YDKrgCLgqigoIKAiNIDIqICKAgUARQERQEAAUAFAAUBBQBQAVBRQAFQBUABFQBFQERRBEVAAAAAAAAAFiKCgoAKAAAACKALFZVRQAEBBEVKCAKACACAIqAAoAAKqAKAACgAoIKigoIAgoAIAAAIoJiiCVlpKCAAAAAAAAKigsVAFABRFABFFEEFisqCiCioCCBUAAAAAQKCAAKigAAKigoAKEVQBAURQEAAAAQQFQBUAAAERQERUAAAAAABRFBQAAAFQBRNUARQFRQAABACoACCAuiAKgAAAAAoACgAoAoigCCiiAKIIAAAgCgAAAJVQBABKAAAAAAAAqAKqQBQAAQFEAURQFZAaEAUQARagCAAAAAAAAAAqKAqAKqKACqIoAgCAigIAACAqoAoigIqAgIAAAAAAAAAAAACiAKgAaaALpqACoAqooAACCUAAAAAAAAAAAABUUFVFAUAEVAAFAUQRFAQVAAAAAEABFQAAAAAAAAAAAAAAAAAAAAAAFAAAARUAAAAAAAAAAAAAABqKysBoRQAFEAQAVRBQEMUQQAERUAQAAAAAAAAAAAAAAAAAAAAAAAf/9k=";