@astrosheep/square 0.3.69 → 0.3.71

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 (132) hide show
  1. package/dist/activity-feed.d.ts +4 -5
  2. package/dist/activity-feed.js +14 -10
  3. package/dist/artifact.d.ts +26 -33
  4. package/dist/artifact.js +346 -317
  5. package/dist/attention-presentation.js +2 -9
  6. package/dist/automatic-session.js +6 -6
  7. package/dist/boundary-presentation.js +3 -3
  8. package/dist/catch-decisions.d.ts +19 -12
  9. package/dist/catch-decisions.js +32 -68
  10. package/dist/claude-delivery.js +7 -7
  11. package/dist/claude-mod.js +18 -7
  12. package/dist/cli/catch-presentation.d.ts +7 -0
  13. package/dist/cli/catch-presentation.js +55 -0
  14. package/dist/cli/context.d.ts +1 -1
  15. package/dist/cli/context.js +5 -23
  16. package/dist/cli/express-presentation.d.ts +10 -0
  17. package/dist/cli/express-presentation.js +196 -0
  18. package/dist/cli/maintenance-commands.d.ts +4 -0
  19. package/dist/cli/maintenance-commands.js +29 -6
  20. package/dist/cli/observation-commands.d.ts +7 -5
  21. package/dist/cli/observation-commands.js +275 -262
  22. package/dist/cli/program.d.ts +1 -1
  23. package/dist/cli/program.js +43 -56
  24. package/dist/cli/registry.js +2 -1
  25. package/dist/cli/square-commands.d.ts +3 -9
  26. package/dist/cli/square-commands.js +134 -255
  27. package/dist/codex-queue.js +4 -1
  28. package/dist/decisions.d.ts +26 -74
  29. package/dist/decisions.js +190 -335
  30. package/dist/delivery-health.js +13 -22
  31. package/dist/delivery-operations.d.ts +0 -11
  32. package/dist/delivery-operations.js +44 -33
  33. package/dist/delivery.d.ts +33 -32
  34. package/dist/delivery.js +70 -81
  35. package/dist/harness-claude.js +1 -1
  36. package/dist/harness-codex.js +1 -1
  37. package/dist/harness-pi.js +1 -1
  38. package/dist/help.js +6 -1
  39. package/dist/inbox.d.ts +27 -0
  40. package/dist/inbox.js +55 -18
  41. package/dist/index.d.ts +2 -4
  42. package/dist/index.js +1 -1
  43. package/dist/list.d.ts +7 -1
  44. package/dist/list.js +49 -122
  45. package/dist/mcp.d.ts +1 -1
  46. package/dist/mcp.js +7 -37
  47. package/dist/model.d.ts +4 -44
  48. package/dist/model.js +1 -3
  49. package/dist/notifications.d.ts +1 -1
  50. package/dist/notifications.js +13 -7
  51. package/dist/open-square.d.ts +16 -2
  52. package/dist/open-square.js +40 -1
  53. package/dist/perception-projection.d.ts +6 -15
  54. package/dist/perception-projection.js +6 -26
  55. package/dist/{opencode-delivery.d.ts → plugins/opencode/delivery.d.ts} +1 -1
  56. package/dist/{opencode-delivery.js → plugins/opencode/delivery.js} +18 -18
  57. package/dist/{opencode.d.ts → plugins/opencode/index.d.ts} +2 -2
  58. package/dist/{opencode.js → plugins/opencode/index.js} +9 -9
  59. package/dist/ports.d.ts +156 -11
  60. package/dist/presentation-operations.d.ts +1 -1
  61. package/dist/presentation-operations.js +35 -44
  62. package/dist/presentation.d.ts +12 -19
  63. package/dist/presentation.js +33 -54
  64. package/dist/routes.d.ts +7 -14
  65. package/dist/routes.js +33 -62
  66. package/dist/runtime.d.ts +12 -39
  67. package/dist/runtime.js +33 -164
  68. package/dist/square-actions.d.ts +11 -21
  69. package/dist/square-actions.js +171 -271
  70. package/dist/square-application.d.ts +31 -55
  71. package/dist/square-application.js +637 -141
  72. package/dist/square-core.d.ts +3 -94
  73. package/dist/square-core.js +8 -353
  74. package/dist/square-domain.d.ts +24 -0
  75. package/dist/square-domain.js +77 -0
  76. package/dist/square-facade.d.ts +360 -9
  77. package/dist/square-migration.d.ts +24 -0
  78. package/dist/square-migration.js +211 -0
  79. package/dist/square-projections.d.ts +2 -42
  80. package/dist/square-projections.js +1 -37
  81. package/dist/square-sqlite.d.ts +59 -0
  82. package/dist/square-sqlite.js +702 -0
  83. package/dist/stream.d.ts +3 -10
  84. package/dist/stream.js +81 -52
  85. package/dist/views.d.ts +27 -95
  86. package/dist/views.js +217 -87
  87. package/dist/wake-eligibility.d.ts +15 -5
  88. package/dist/wake-eligibility.js +32 -16
  89. package/dist/wakes.d.ts +3 -3
  90. package/dist/wakes.js +31 -27
  91. package/guides/application-facade-delivery-evidence.md +200 -0
  92. package/guides/application-facade-design.md +95 -0
  93. package/guides/change-waits.md +4 -4
  94. package/guides/migration.md +77 -0
  95. package/guides/pi-native-delivery.md +1 -1
  96. package/guides/primitive-catch-consolidation.md +85 -0
  97. package/guides/primitive-history-consolidation.md +76 -0
  98. package/guides/primitive-status-consolidation.md +87 -0
  99. package/guides/primitive-storage-consolidation.md +128 -0
  100. package/guides/row-runtime-delivery-evidence.md +160 -0
  101. package/guides/row-storage-design.md +567 -0
  102. package/package.json +5 -7
  103. package/{claude-plugin → plugins/claude}/.claude-plugin/plugin.json +1 -1
  104. package/{codex-plugin → plugins/codex}/.codex-plugin/plugin.json +1 -1
  105. package/plugins/opencode/delivery.ts +149 -0
  106. package/plugins/opencode/index.ts +136 -0
  107. package/plugins/paseo/OVERVIEW.md +14 -0
  108. package/plugins/paseo/README.md +54 -0
  109. package/plugins/paseo/client/square-message.tsx +106 -0
  110. package/plugins/paseo/client/transform.ts +24 -0
  111. package/plugins/paseo/index.client.tsx +19 -0
  112. package/plugins/paseo/package.json +19 -0
  113. package/plugins/paseo/paseo-plugin.json +7 -0
  114. package/plugins/paseo/shared/message.ts +29 -0
  115. package/{extensions → plugins/pi}/square-pi.js +8 -5
  116. package/dist/activity.d.ts +0 -14
  117. package/dist/activity.js +0 -248
  118. package/dist/square-file-adapter.d.ts +0 -26
  119. package/dist/square-file-adapter.js +0 -85
  120. package/dist/square-state.d.ts +0 -6
  121. package/dist/square-state.js +0 -138
  122. package/dist/square-storage.d.ts +0 -14
  123. package/dist/square-storage.js +0 -188
  124. package/dist/square-wiring.d.ts +0 -33
  125. package/dist/square-wiring.js +0 -109
  126. package/dist/watch.d.ts +0 -8
  127. package/dist/watch.js +0 -252
  128. /package/{claude-plugin → plugins/claude}/hooks/hooks.json +0 -0
  129. /package/{claude-plugin → plugins/claude}/hooks/register.js +0 -0
  130. /package/{claude-plugin → plugins/claude}/skills/square/SKILL.md +0 -0
  131. /package/{claude-plugin → plugins/claude}/types.d.ts +0 -0
  132. /package/{codex-plugin → plugins/codex}/hooks/hooks.json +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.69",
3
+ "version": "0.3.71",
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": {
@@ -10,9 +10,7 @@
10
10
  "dist",
11
11
  "guides",
12
12
  "skills/square",
13
- "claude-plugin",
14
- "extensions",
15
- "codex-plugin"
13
+ "plugins"
16
14
  ],
17
15
  "scripts": {
18
16
  "clean": "node --input-type=module -e \"import fs from 'node:fs'; fs.rmSync('dist',{recursive:true,force:true})\"",
@@ -60,8 +58,8 @@
60
58
  "default": "./dist/paseo.js"
61
59
  },
62
60
  "./server": {
63
- "types": "./dist/opencode.d.ts",
64
- "default": "./dist/opencode.js"
61
+ "types": "./dist/plugins/opencode/index.d.ts",
62
+ "default": "./dist/plugins/opencode/index.js"
65
63
  },
66
64
  "./mcp": {
67
65
  "types": "./dist/mcp.d.ts",
@@ -72,7 +70,7 @@
72
70
  "types": "dist/index.d.ts",
73
71
  "pi": {
74
72
  "extensions": [
75
- "./extensions/square-pi.js"
73
+ "./plugins/pi/square-pi.js"
76
74
  ],
77
75
  "skills": [
78
76
  "./skills"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.69",
3
+ "version": "0.3.71",
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.69",
3
+ "version": "0.3.71",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -0,0 +1,149 @@
1
+ import type { EvidenceRecord, NativeDeliveryEvidence, PresenceRecord } from '../../host-ledger.js';
2
+ import type { SquareRead, WakeTransportPort } from '../../ports.js';
3
+ import { nameKey } from '../../model.js';
4
+ import { nativePendingPreview, observeSessionPending, sessionInbox } from '../../inbox.js';
5
+ import { hostLedgerForEnv } from '../../registry.js';
6
+ import { openSquare } from '../../open-square.js';
7
+ import { closeOpenSquare } from '../../open-square.js';
8
+ import { presentationSuppressesWake, projectPresentationEvidence } from '../../square-projections.js';
9
+ import { presentPending } from '../../presentation-operations.js';
10
+ import { deliverPending } from '../../delivery-operations.js';
11
+ import { createWakeTransport } from '../../notifications.js';
12
+ import { publishWakeRoute } from '../../routes.js';
13
+ import { formatActivityId, parseActivityId } from '../../square-core.js';
14
+ import { createNativeInputId, sendNativeText, type OpenCodeNativeSession } from '../../packages/agent-delivery/src/opencode-native.js';
15
+
16
+ const routeKind = 'opencode-server' as const;
17
+ type NativeRow = EvidenceRecord & { claimToken: string; nativeDelivery: Extract<NativeDeliveryEvidence, { harness: 'opencode' }> };
18
+ export interface OpenCodeContextMessage { readonly id?: string; readonly role: string; readonly content: readonly { readonly type: string; readonly text?: string }[] }
19
+ const key = (location: string, participant: string, index: number) => `${location}\0${nameKey(participant)}\0${index}`;
20
+
21
+ /** The one native pending preview: the audience/catch/epoch fence before send and inside presentation. */
22
+ function preview(facts: SquareRead, owner: PresenceRecord, index: number) {
23
+ return nativePendingPreview(facts, { location: owner.location, routeKind, sessionId: owner.session, participant: owner.participant, actIndex: index, epoch: owner.epoch, address: { sessionId: owner.session }, cancelledThrough: owner.cancelledThrough, now: Date.now() });
24
+ }
25
+
26
+ function createOpenCodeWakeTransport(target: OpenCodeNativeSession, env: NodeJS.ProcessEnv, lifetime: AbortSignal): WakeTransportPort {
27
+ const ledger = hostLedgerForEnv(env);
28
+ return {
29
+ probe: async (route) => !lifetime.aborted && route.kind === routeKind && route.sessionId === target.sessionId && route.address.sessionId === target.sessionId,
30
+ async attempt(request, timeoutMs, beforeSend) {
31
+ const signal = AbortSignal.any([lifetime, AbortSignal.timeout(timeoutMs)]);
32
+ const deadline = Date.now() + timeoutMs;
33
+ // Start the native call under the ownership fence, but await it OUTSIDE: prompt may
34
+ // enter our context hook synchronously and that hook needs the same ownership lock.
35
+ const started = await ledger.withClaimLock(async () => {
36
+ if (signal.aborted || request.route.sessionId !== target.sessionId || !request.claimToken
37
+ || !await (beforeSend?.() ?? Promise.resolve(true))) return undefined;
38
+ const owner = (await ledger.listPresence({ location: request.location, participant: request.participant, session: target.sessionId }))
39
+ .find((binding) => binding.channel === 'opencode' && binding.epoch === request.route.epoch);
40
+ const index = parseActivityId(request.activity);
41
+ if (!owner || index === undefined) return undefined;
42
+ const square = await openSquare(owner.location, { hostLedger: ledger, env, signal });
43
+ try {
44
+ const { value: prepared } = await square.artifact.read((facts) => preview(facts, owner, index), signal);
45
+ if (!prepared) return undefined;
46
+ const row: NativeRow & { routeKind: typeof routeKind } = { location: owner.location, participant: owner.participant, session: target.sessionId,
47
+ activity: request.activity, kind: 'wake', outcome: 'dispatching', routeKind, attemptN: request.attemptN, claimToken: request.claimToken,
48
+ nativeDelivery: { harness: 'opencode', epoch: owner.epoch ?? 0, inputId: createNativeInputId(), payload: prepared.payload } };
49
+ if (!await ledger.prepareNativeWake(row) || !await (beforeSend?.() ?? Promise.resolve(true)) || signal.aborted
50
+ || (await square.artifact.read((facts) => preview(facts, owner, index)?.payload, signal)).value !== prepared.payload) return undefined;
51
+ return { sending: sendNativeText(target, prepared.payload, { inputId: row.nativeDelivery.inputId, delivery: 'steer',
52
+ timeoutMs: Math.max(1, deadline - Date.now()), signal }) };
53
+ } finally { await closeOpenSquare(square); }
54
+ }, signal);
55
+ if (!started) return { outcome: 'not-capable', diagnostic: 'OpenCode attention, binding or local delivery generation changed before send.' };
56
+ const result = await started.sending;
57
+ if (result.state === 'accepted') return { outcome: 'accepted' };
58
+ if (result.state === 'unavailable') return { outcome: 'failed', unavailable: true, retainRoute: true, signature: result.code };
59
+ return { outcome: 'unknown', signature: result.code };
60
+ },
61
+ };
62
+ }
63
+
64
+ /** Only the matching prepared primary context authorizes presentation, never admission. */
65
+ export async function observeOpenCodeContext(sessionId: string, messages: readonly OpenCodeContextMessage[], env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
66
+ const ledger = hostLedgerForEnv(env);
67
+ await ledger.withClaimLock(async () => {
68
+ if (signal.aborted) return;
69
+ const rows = (await ledger.listEvidence({ kind: 'wake', session: sessionId })).filter((row): row is NativeRow =>
70
+ row.nativeDelivery?.harness === 'opencode' && !!row.claimToken && ['dispatching', 'unknown', 'accepted'].includes(row.outcome));
71
+ for (const row of rows) {
72
+ if (signal.aborted || !messages.some((message) => message.id === row.nativeDelivery.inputId && message.role === 'user'
73
+ && message.content.every((part) => part.type === 'text' && typeof part.text === 'string')
74
+ && message.content.map((part) => part.text).join('') === row.nativeDelivery.payload)) continue;
75
+ const owner = (await ledger.listPresence({ location: row.location, participant: row.participant, session: sessionId }))
76
+ .find((binding) => binding.channel === 'opencode' && binding.epoch === row.nativeDelivery.epoch);
77
+ const index = parseActivityId(row.activity);
78
+ if (!owner || index === undefined) continue;
79
+ const square = await openSquare(row.location, { hostLedger: ledger, env, signal });
80
+ try {
81
+ const { value: prepared } = await square.artifact.read((facts) => preview(facts, owner, index), signal);
82
+ if (!prepared || prepared.payload !== row.nativeDelivery.payload) continue;
83
+ const prior = await projectPresentationEvidence({ hostLedger: ledger, location: row.location, participant: row.participant, sessionId, activity: row.activity });
84
+ if (presentationSuppressesWake(prior)) continue;
85
+ await presentPending({ artifact: square.artifact, hostLedger: ledger, location: row.location, participant: row.participant, session: sessionId,
86
+ activity: row.activity, sink: { present() {} }, markSeen: !prepared.clipped, signal,
87
+ current: (facts) => !signal.aborted && preview(facts, owner, index)?.payload === row.nativeDelivery.payload });
88
+ } finally { await closeOpenSquare(square); }
89
+ }
90
+ }, signal);
91
+ }
92
+
93
+ /** One existing observer and the existing dispatch owner; no second queue or evidence store. */
94
+ export async function receiveOpenCodePending(target: OpenCodeNativeSession, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
95
+ const ledger = hostLedgerForEnv(env);
96
+ const observer = await observeSessionPending(target.sessionId, env);
97
+ const excluded = new Set<string>();
98
+ const refreshExclusions = async () => {
99
+ excluded.clear();
100
+ const owners = await ledger.listPresence({ session: target.sessionId });
101
+ for (const row of await ledger.listEvidence({ kind: 'wake', session: target.sessionId })) {
102
+ const index = parseActivityId(row.activity);
103
+ const current = owners.some((owner) => owner.location === row.location && nameKey(owner.participant) === nameKey(row.participant) && owner.epoch === row.nativeDelivery?.epoch);
104
+ if (index !== undefined && (['accepted', 'unknown'].includes(row.outcome) || row.outcome === 'dispatching' && current && row.nativeDelivery?.harness === 'opencode')) excluded.add(key(row.location, row.participant, index));
105
+ }
106
+ for (const membership of await sessionInbox(target.sessionId, env, signal)) {
107
+ const owner = owners.find((binding) => binding.location === membership.squarePath && nameKey(binding.participant) === nameKey(membership.name));
108
+ for (const note of membership.notifications) if (note.actIndex <= (owner?.cancelledThrough ?? -1)) excluded.add(key(membership.squarePath, membership.name, note.actIndex));
109
+ }
110
+ };
111
+ const transport = createWakeTransport([], ledger, Date.now, { [routeKind]: createOpenCodeWakeTransport(target, env, signal) });
112
+ try {
113
+ await refreshExclusions();
114
+ let pending = await observer.wait(Infinity, { signal, excludeKeys: excluded });
115
+ while (!signal.aborted && pending.length > 0) {
116
+ let armed!: (value: boolean) => void;
117
+ const ready = new Promise<boolean>((resolve) => { armed = resolve; });
118
+ // Capture the next state edge BEFORE dispatch. Evidence failures alone do not retry.
119
+ const next = observer.wait(Infinity, { signal, excludeKeys: excluded, skipImmediate: true, onChangeArmed: armed })
120
+ .catch(() => { armed(false); return []; });
121
+ if (!await ready || signal.aborted) break;
122
+ for (const membership of pending) {
123
+ const square = await openSquare(membership.squarePath, { hostLedger: ledger, env, signal });
124
+ try {
125
+ for (const note of membership.notifications) {
126
+ if (signal.aborted || excluded.has(key(membership.squarePath, membership.name, note.actIndex))) continue;
127
+ await deliverPending({ artifact: square.artifact, hostLedger: ledger, transport, location: square.location, activity: formatActivityId(note.actIndex), now: Date.now() });
128
+ }
129
+ } finally { await closeOpenSquare(square); }
130
+ }
131
+ await refreshExclusions();
132
+ pending = await next;
133
+ }
134
+ } finally { observer.close(); }
135
+ }
136
+
137
+ export async function publishOpenCodeRoutes(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<boolean> {
138
+ const ledger = hostLedgerForEnv(env);
139
+ return ledger.withClaimLock(async () => {
140
+ const owners = (await ledger.listPresence({ session: sessionId })).filter((binding) => binding.channel === 'opencode');
141
+ for (const owner of owners) {
142
+ const square = await openSquare(owner.location, { hostLedger: ledger, env, signal });
143
+ try { await publishWakeRoute(square.artifact, { location: owner.location, participant: owner.participant, sessionId, channel: 'opencode',
144
+ kind: routeKind, address: { sessionId }, epoch: owner.epoch }, { requireCurrentSession: true }); }
145
+ finally { await closeOpenSquare(square); }
146
+ }
147
+ return owners.length > 0;
148
+ }, signal);
149
+ }
@@ -0,0 +1,136 @@
1
+ import { automaticSessionStart, operationEnv } from '../../automatic-session.js';
2
+ import { hostLedgerForEnv } from '../../registry.js';
3
+ import { canonicalPath } from '../../canonical-path.js';
4
+ import { openSquare } from '../../open-square.js';
5
+ import { closeOpenSquare } from '../../open-square.js';
6
+ import { Square } from '../../square-application.js';
7
+ import { connectNative, type OpenCodeNativeSession, type OpenCodeSessionCapability } from '../../packages/agent-delivery/src/opencode-native.js';
8
+ import { observeOpenCodeContext, publishOpenCodeRoutes, receiveOpenCodePending, type OpenCodeContextMessage } from './delivery.js';
9
+
10
+ interface OpenCodePluginContext {
11
+ readonly app: { readonly version: string };
12
+ readonly location: { readonly directory: string; readonly project: { readonly id: string }; readonly workspaceID?: string };
13
+ readonly session: OpenCodeSessionCapability & {
14
+ hook(name: 'prompt', callback: (event: { sessionID: string }) => Promise<void>): Promise<unknown>;
15
+ hook(name: 'context', callback: (event: { sessionID: string; messages: OpenCodeContextMessage[] }) => Promise<void>): Promise<unknown>;
16
+ };
17
+ readonly event: { subscribe(options: { signal: AbortSignal }): AsyncIterable<{ type: string; data: { sessionID?: string; reason?: string } }> };
18
+ }
19
+ interface Receiver { readonly abort: AbortController; readonly running: Promise<void>; settled: boolean }
20
+
21
+ /** Actual OpenCode 2.0.20 server plugin. Harness identity belongs to ctx, never process.env. */
22
+ export default {
23
+ id: 'square',
24
+ async setup(ctx: OpenCodePluginContext): Promise<() => Promise<void>> {
25
+ if (ctx.app.version !== '2.0.20') return async () => {};
26
+ const env = { ...process.env };
27
+ const ledger = hostLedgerForEnv(env);
28
+ const lifetime = new AbortController();
29
+ const receivers = new Map<string, Receiver>(); // Operational handles only; presence owns membership.
30
+ const directory = await canonicalPath(ctx.location.directory);
31
+ let coordinating = Promise.resolve();
32
+ const coordinate = (operation: () => Promise<void>) => {
33
+ const task = coordinating.then(() => lifetime.signal.aborted ? undefined : operation());
34
+ coordinating = task.catch(() => {}); // Optional Square hooks must never fail primary request preparation.
35
+ return coordinating;
36
+ };
37
+ const stop = (sessionId: string) => {
38
+ const receiver = receivers.get(sessionId);
39
+ receiver?.abort.abort();
40
+ receivers.delete(sessionId);
41
+ return receiver?.running;
42
+ };
43
+ const validate = async (sessionId: string): Promise<OpenCodeNativeSession | undefined> => {
44
+ let info: unknown;
45
+ const target = await connectNative({ sessionId, timeoutMs: 3000, signal: lifetime.signal, session: {
46
+ async get(input) { info = await ctx.session.get(input); return info; },
47
+ prompt: (input) => ctx.session.prompt(input),
48
+ } }).catch(() => undefined);
49
+ const record = info as { projectID?: string; location?: { directory?: string; workspaceID?: string } } | undefined;
50
+ if (!target || !record?.location?.directory || record.projectID !== ctx.location.project.id
51
+ || record.location.workspaceID !== ctx.location.workspaceID
52
+ || await canonicalPath(record.location.directory) !== directory) return undefined;
53
+ return target;
54
+ };
55
+ const reconcile = async (activeSession?: string) => {
56
+ const candidates = new Set((await ledger.listPresence({})).filter((binding) => binding.channel === 'opencode').map((binding) => binding.session));
57
+ if (activeSession) candidates.add(activeSession);
58
+ for (const sessionId of candidates) {
59
+ if (lifetime.signal.aborted) return;
60
+ const target = await validate(sessionId);
61
+ if (!target) { stop(sessionId); continue; }
62
+ const scopedEnv = operationEnv('opencode', sessionId, env);
63
+ if (sessionId === activeSession) await automaticSessionStart('opencode', sessionId, directory, scopedEnv);
64
+ if (!await publishOpenCodeRoutes(sessionId, scopedEnv, lifetime.signal)) { stop(sessionId); continue; }
65
+ if (receivers.get(sessionId)?.settled === false) continue;
66
+ const abort = new AbortController();
67
+ const signal = AbortSignal.any([lifetime.signal, abort.signal]);
68
+ const receiver: Receiver = { abort, settled: false, running: receiveOpenCodePending(target, scopedEnv, signal).catch(() => {}).finally(() => { receiver.settled = true; }) };
69
+ receivers.set(sessionId, receiver);
70
+ }
71
+ for (const sessionId of receivers.keys()) if (!candidates.has(sessionId)) stop(sessionId);
72
+ };
73
+ const cancel = async (sessionId: string) => {
74
+ if (!receivers.has(sessionId)) return;
75
+ // Stop synchronously; persist the cutoff before allowing reconciliation to start fresh work.
76
+ stop(sessionId);
77
+ await ledger.withClaimLock(async () => {
78
+ for (const owner of (await ledger.listPresence({ session: sessionId })).filter((binding) => binding.channel === 'opencode')) {
79
+ const square = await openSquare(owner.location, { hostLedger: ledger, env, signal: lifetime.signal });
80
+ try { await ledger.suppressPresence(owner, (await square.artifact.read((facts) => facts.header().nextActIndex - 1, lifetime.signal)).value); }
81
+ finally { await closeOpenSquare(square); }
82
+ }
83
+ }, lifetime.signal);
84
+ await reconcile();
85
+ };
86
+ const end = async (sessionId: string) => {
87
+ stop(sessionId);
88
+ const memberships = (await ledger.listPresence({ session: sessionId })).filter((binding) => binding.channel === 'opencode');
89
+ for (const owner of memberships) {
90
+ const square = await Square.at({ path: owner.location, hostLedger: ledger, env: operationEnv('opencode', sessionId, env) });
91
+ try { await square.endOwnedSession(owner.participant, sessionId, owner.epoch); }
92
+ finally { await square.close(); }
93
+ }
94
+ };
95
+
96
+ await ctx.session.hook('prompt', (event) => coordinate(() => reconcile(event.sessionID)));
97
+ await ctx.session.hook('context', async (event) => {
98
+ await coordinate(() => reconcile(event.sessionID));
99
+ const receiver = receivers.get(event.sessionID);
100
+ if (!receiver || lifetime.signal.aborted) return;
101
+ await observeOpenCodeContext(event.sessionID, event.messages, operationEnv('opencode', event.sessionID, env),
102
+ AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => {});
103
+ });
104
+ const changes = await ledger.observeChanges();
105
+ changes.evidence.close(); // Manual joins activate an idle receiver; evidence already has a session observer.
106
+ const presenceLoop = (async () => {
107
+ while (!lifetime.signal.aborted) {
108
+ const baseline = await changes.presence.read(lifetime.signal);
109
+ await coordinate(() => reconcile());
110
+ await changes.presence.changed(baseline, Infinity, lifetime.signal);
111
+ }
112
+ })().catch(() => {});
113
+ const eventLoop = (async () => {
114
+ for await (const event of ctx.event.subscribe({ signal: lifetime.signal })) {
115
+ const sessionId = event.data.sessionID;
116
+ if (!sessionId) continue;
117
+ if (event.type === 'session.deleted') await coordinate(() => end(sessionId));
118
+ else if (event.type === 'session.moved') await coordinate(async () => { stop(sessionId); await reconcile(); });
119
+ else if (event.type === 'session.execution.interrupted' && event.data.reason === 'user') {
120
+ receivers.get(sessionId)?.abort.abort();
121
+ await coordinate(() => cancel(sessionId));
122
+ }
123
+ }
124
+ })().catch(() => {});
125
+ // Do not await any native prompt in setup: plugin activation itself is lazy.
126
+ return async () => {
127
+ lifetime.abort();
128
+ changes.presence.close();
129
+ const tasks = [presenceLoop, eventLoop, coordinating, ...[...receivers.keys()].map(stop)];
130
+ let timer: ReturnType<typeof setTimeout> | undefined;
131
+ try { await Promise.race([Promise.allSettled(tasks), new Promise<void>((resolve) => { timer = setTimeout(resolve, 3000); })]); }
132
+ finally { if (timer !== undefined) clearTimeout(timer); }
133
+ // Instance unload is not session deletion: retain memberships and uncertain correlations.
134
+ };
135
+ },
136
+ };
@@ -0,0 +1,14 @@
1
+ # Square
2
+
3
+ Expandable Square context in the Paseo conversation. A compact card tells you
4
+ who reached you and where. Expand it to see the original custom-message content,
5
+ select text, or copy it in full. The card follows the active Paseo theme on
6
+ desktop and mobile.
7
+
8
+ Requires Paseo 0.11.2 or newer with custom-message tool rows (#6090). Square
9
+ 0.3.69 or newer supplies structured card headings. Older messages and joining
10
+ context use a neutral “Square context” heading.
11
+
12
+ The plugin changes presentation only. It does not send prompts or alter model
13
+ context. If Square clipped a body before delivery, expansion shows that same
14
+ preview and the command for reading the rest, not an additional fetch.
@@ -0,0 +1,54 @@
1
+ # Square for Paseo
2
+
3
+ A client-only presentation plugin for Square custom messages. No daemon restart,
4
+ backend entry, extra notifications, or model-context changes.
5
+
6
+ ## Install from GitHub
7
+
8
+ ```bash
9
+ paseo plugin install git:astrosheep-zero/square:plugins/paseo
10
+ ```
11
+
12
+ Use `--ref <tag-or-branch>` to select an existing ref that contains the plugin
13
+ directory. Square tags up to v0.3.69 do not contain it.
14
+
15
+ Paseo supplies all runtime modules. No Git preparation step is needed. Plugins
16
+ are trusted, unsandboxed code running inside the Paseo app.
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.
21
+
22
+ ## Display
23
+
24
+ Only synthetic `tool_call` items with `metadata.customType === "square"` and
25
+ plain-text detail are projected into a card. Real tools, other custom messages,
26
+ and unrecognized rows remain unchanged.
27
+
28
+ The collapsed heading uses `metadata.details.items`; it never reverse-parses
29
+ the content. Expansion shows `detail.text` verbatim, including fences, clipping
30
+ notices, and catch commands. Copy content copies that exact string. It does not
31
+ fetch extra activity or mark anything seen. Live events and fetched history use
32
+ the same transformer.
33
+
34
+ The card uses Paseo theme colors, selectable text, accessible expansion state,
35
+ and compact layout on narrow displays. Removing the plugin restores Paseo's
36
+ ordinary custom-message row.
37
+
38
+ ## Develop
39
+
40
+ ```bash
41
+ cd plugins/paseo
42
+ npm ci
43
+ npm run typecheck
44
+ npm test
45
+ paseo plugin install "$PWD"
46
+ ```
47
+
48
+ After editing an installed directory source:
49
+
50
+ ```bash
51
+ paseo plugin reload square
52
+ ```
53
+
54
+ No Paseo restart is needed. The plugin ID is `square`.
@@ -0,0 +1,106 @@
1
+ import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
2
+ import { copyText, Icon, useToast } from "@getpaseo/plugin/client/react-native";
3
+ import { useMemo, useState } from "react";
4
+ import { Platform, Pressable, Text, View } from "react-native";
5
+ import { squareHeadline, type SquareMessageData } from "../shared/message";
6
+
7
+ function withOpacity(color: string, opacity: number): string {
8
+ const hex = /^#([\da-f]{3}|[\da-f]{6})$/iu.exec(color);
9
+ if (hex === null) return color;
10
+ const value = hex[1]!.length === 3 ? [...hex[1]!].map((part) => part + part).join("") : hex[1]!;
11
+ const alpha = Math.round(opacity * 255).toString(16).padStart(2, "0");
12
+ return `#${value}${alpha}`;
13
+ }
14
+
15
+ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
16
+ const [expanded, setExpanded] = useState(false);
17
+ const toast = useToast();
18
+ const { text, items } = item.data;
19
+ const first = items?.[0];
20
+ const muted = theme.colors.foregroundMuted;
21
+ const foreground = theme.colors.foreground;
22
+ const padding = layout.compact ? 10 : 14;
23
+ const styles = useMemo(() => ({
24
+ marker: { color: theme.colors.accent, fontSize: 10, marginTop: 2 },
25
+ headline: { color: foreground, fontSize: 13, lineHeight: 19, flexShrink: 1 },
26
+ meta: { color: muted, fontSize: 11, lineHeight: 16, flexShrink: 1 },
27
+ bodyPanel: {
28
+ backgroundColor: theme.colors.surface1,
29
+ borderRadius: 12,
30
+ marginTop: 7,
31
+ marginLeft: 17,
32
+ overflow: "hidden" as const,
33
+ },
34
+ bodyText: {
35
+ color: foreground,
36
+ fontSize: 12,
37
+ lineHeight: 21,
38
+ fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
39
+ paddingHorizontal: layout.compact ? 12 : 14,
40
+ paddingTop: 12,
41
+ },
42
+ }), [expanded, foreground, layout.compact, muted, theme.colors.accent, theme.colors.surface1]);
43
+
44
+ return (
45
+ <View>
46
+ <Pressable
47
+ accessibilityRole="button"
48
+ accessibilityLabel={first !== undefined ? squareHeadline(first) : "Square context"}
49
+ aria-expanded={expanded}
50
+ onPress={() => setExpanded(value => !value)}
51
+ style={({ pressed }) => ({
52
+ flexDirection: "row",
53
+ alignItems: "flex-start",
54
+ gap: 7,
55
+ paddingVertical: 2,
56
+ opacity: pressed ? 0.6 : 1,
57
+ })}
58
+ >
59
+ <Text style={styles.marker}>■</Text>
60
+ <View style={{
61
+ flex: 1,
62
+ minWidth: 0,
63
+ flexDirection: layout.compact ? "column" : "row",
64
+ alignItems: layout.compact ? "flex-start" : "center",
65
+ gap: layout.compact ? 2 : 8,
66
+ }}>
67
+ <Text numberOfLines={layout.compact ? 2 : 1} style={styles.headline}>
68
+ {first !== undefined ? squareHeadline(first) : "Square context"}
69
+ </Text>
70
+ {first && <Text numberOfLines={1} style={styles.meta}>
71
+ {first.square}{items && items.length > 1 ? ` · +${items.length - 1} more` : ""}
72
+ </Text>}
73
+ </View>
74
+ <View style={{ marginTop: 3 }}><Icon name={expanded ? "ChevronDown" : "ChevronRight"} size={14} color={muted} /></View>
75
+ </Pressable>
76
+ {expanded && <View style={styles.bodyPanel}>
77
+ <Text selectable style={styles.bodyText}>{text}</Text>
78
+ <View style={{
79
+ marginHorizontal: layout.compact ? 12 : 14,
80
+ marginTop: 12,
81
+ borderTopWidth: 1,
82
+ borderTopColor: withOpacity(theme.colors.border, 0.55),
83
+ paddingVertical: 7,
84
+ flexDirection: "row",
85
+ justifyContent: "flex-end",
86
+ }}>
87
+ <Pressable
88
+ accessibilityRole="button"
89
+ accessibilityLabel="Copy Square content"
90
+ onPress={() => { void copyText(text).catch(() => toast.error("Could not copy Square content")); }}
91
+ style={({ pressed }) => ({
92
+ flexDirection: "row",
93
+ alignItems: "center",
94
+ gap: 5,
95
+ paddingHorizontal: 2,
96
+ opacity: pressed ? 0.6 : 1,
97
+ })}
98
+ >
99
+ <Icon name="Copy" size={12} color={muted} />
100
+ <Text style={{ color: muted, fontSize: 11 }}>Copy content</Text>
101
+ </Pressable>
102
+ </View>
103
+ </View>}
104
+ </View>
105
+ );
106
+ }
@@ -0,0 +1,24 @@
1
+ import type { PluginTimelineTransformerContribution } from "@getpaseo/plugin/client";
2
+ import { squareCardDetailsSchema } from "../shared/message.ts";
3
+
4
+ // Recognition is metadata-based, not a guess from a tool name or body text.
5
+ // The same projection applies to live events and fetched history.
6
+ export const transformSquareMessage: PluginTimelineTransformerContribution<"tool_call">["transform"] = ({ item }) => {
7
+ if (item.metadata?.synthetic !== true ||
8
+ item.metadata.customType !== "square" ||
9
+ item.detail.type !== "plain_text" ||
10
+ typeof item.detail.text !== "string") return;
11
+
12
+ const details = squareCardDetailsSchema.safeParse(item.metadata.details);
13
+ return {
14
+ items: [{
15
+ type: "plugin",
16
+ kind: "square-message",
17
+ version: 1,
18
+ data: {
19
+ text: item.detail.text,
20
+ ...(details.success ? { items: details.data.items } : {}),
21
+ },
22
+ }],
23
+ };
24
+ };
@@ -0,0 +1,19 @@
1
+ import type { PluginClientContext } from "@getpaseo/plugin/client";
2
+ import { SquareMessage } from "./client/square-message";
3
+ import { transformSquareMessage } from "./client/transform";
4
+ import { squareMessageSchema } from "./shared/message";
5
+
6
+ export default function contribute(client: PluginClientContext) {
7
+ client.addTimelineTransformer({
8
+ id: "square-message",
9
+ query: { itemType: "tool_call" },
10
+ transform: transformSquareMessage,
11
+ });
12
+ client.addTimelineRenderer({
13
+ kind: "square-message",
14
+ version: 1,
15
+ schema: squareMessageSchema,
16
+ Component: SquareMessage,
17
+ });
18
+ return () => {};
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@astrosheep/square-paseo",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "typecheck": "tsc --noEmit",
8
+ "test": "node --experimental-strip-types --test test/*.test.ts"
9
+ },
10
+ "devDependencies": {
11
+ "@getpaseo/plugin": "0.11.2",
12
+ "@types/react": "~19.2.0",
13
+ "@types/node": "^22",
14
+ "react": "19.1.0",
15
+ "react-native": "0.81.5",
16
+ "typescript": "^5.9.3",
17
+ "zod": "^4.4.3"
18
+ }
19
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "square",
3
+ "name": "Square",
4
+ "requirements": {
5
+ "paseo": ">=0.11.2"
6
+ }
7
+ }
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+
3
+ export const squareCardDetailsSchema = z.object({
4
+ items: z.array(z.object({
5
+ square: z.string(),
6
+ actor: z.string().optional(),
7
+ recipient: z.string().optional(),
8
+ route: z.enum(["mention", "reply", "attention", "bell"]).optional(),
9
+ description: z.string(),
10
+ })).min(1),
11
+ });
12
+
13
+ /** Human-facing summary; the stored description is Square's agent voice, and the human is not the recipient. */
14
+ export function squareHeadline(item: z.output<typeof squareCardDetailsSchema>["items"][number]): string {
15
+ if (item.actor === undefined || item.route === undefined) return item.description;
16
+ const to = item.recipient;
17
+ switch (item.route) {
18
+ case "mention": return to === undefined ? `${item.actor} mentioned you` : `${item.actor} mentioned ${to}`;
19
+ case "reply": return to === undefined ? `${item.actor} replied to you` : `${item.actor} replied to ${to}`;
20
+ case "attention": return `New from ${item.actor}`;
21
+ case "bell": return `${item.actor} called everyone`;
22
+ }
23
+ }
24
+
25
+ export const squareMessageSchema = z.object({
26
+ text: z.string(),
27
+ items: squareCardDetailsSchema.shape.items.optional(),
28
+ });
29
+ export type SquareMessageData = z.output<typeof squareMessageSchema>;
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
2
 
3
- import { presentPendingAtBoundary, renderPendingAtBoundary } from '../dist/boundary-presentation.js';
4
- import { automaticSessionEnd, automaticSessionStart } from '../dist/automatic-session.js';
5
- import { sessionInbox, observeSessionPending } from '../dist/inbox.js';
6
- import { renderAttentionDescription } from '../dist/attention-presentation.js';
7
- import { createPiReceiver, sendPiMessage } from '../dist/packages/agent-delivery/src/pi.js';
3
+ import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
4
+ import { automaticSessionEnd, automaticSessionStart } from '../../dist/automatic-session.js';
5
+ import { sessionInbox, observeSessionPending } from '../../dist/inbox.js';
6
+ import { renderAttentionDescription } from '../../dist/attention-presentation.js';
7
+ import { createPiReceiver, sendPiMessage } from '../../dist/packages/agent-delivery/src/pi.js';
8
8
 
9
9
  class PiDeliveryDroppedError extends Error {
10
10
  constructor() {
@@ -35,6 +35,9 @@ function framePiMessage(content) {
35
35
  export function pendingCardDetails(pending) {
36
36
  const items = pending.flatMap((membership) => membership.notifications.map((note) => ({
37
37
  square: path.basename(membership.squarePath, '.square'),
38
+ actor: note.actor,
39
+ recipient: membership.name,
40
+ route: note.route,
38
41
  description: renderAttentionDescription({ actor: note.actor, recipient: membership.name, route: note.route }),
39
42
  })));
40
43
  return items.length === 0 ? undefined : { items };