@astrosheep/square 0.4.0 → 0.4.2

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 (196) hide show
  1. package/dist/artifact.d.ts +1 -3
  2. package/dist/artifact.js +12 -36
  3. package/dist/attention-presentation.d.ts +2 -22
  4. package/dist/attention-presentation.js +9 -12
  5. package/dist/boundary-presentation.d.ts +3 -14
  6. package/dist/boundary-presentation.js +29 -104
  7. package/dist/card-presentation.d.ts +1 -0
  8. package/dist/card-presentation.js +2 -0
  9. package/dist/catch-decisions.js +1 -7
  10. package/dist/claude-boundary-state.d.ts +20 -0
  11. package/dist/claude-boundary-state.js +84 -0
  12. package/dist/claude-hook.d.ts +2 -0
  13. package/dist/claude-hook.js +231 -0
  14. package/dist/claude-mcp-context.d.ts +1 -1
  15. package/dist/claude-mcp-context.js +12 -3
  16. package/dist/cli/context.js +7 -9
  17. package/dist/cli/observation-commands.d.ts +4 -1
  18. package/dist/cli/observation-commands.js +37 -4
  19. package/dist/cli/registry.js +5 -2
  20. package/dist/codex-address.d.ts +2 -2
  21. package/dist/codex-boundary-state.d.ts +1 -5
  22. package/dist/codex-boundary-state.js +0 -4
  23. package/dist/codex-hook.d.ts +7 -10
  24. package/dist/codex-hook.js +69 -33
  25. package/dist/codex-queue.d.ts +10 -11
  26. package/dist/codex-queue.js +32 -32
  27. package/dist/codex-watch.d.ts +2 -0
  28. package/dist/codex-watch.js +100 -0
  29. package/dist/decisions.js +2 -2
  30. package/dist/delivery-deferral.d.ts +8 -17
  31. package/dist/delivery-deferral.js +0 -28
  32. package/dist/delivery-frame.d.ts +7 -0
  33. package/dist/delivery-frame.js +11 -0
  34. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  35. package/dist/delivery-ledger-sqlite.js +496 -0
  36. package/dist/delivery-ledger.d.ts +196 -0
  37. package/dist/delivery-ledger.js +268 -0
  38. package/dist/delivery-lifetime.d.ts +6 -0
  39. package/dist/delivery-lifetime.js +45 -0
  40. package/dist/harness-claude.js +3 -4
  41. package/dist/harness-links.js +63 -7
  42. package/dist/harness.js +12 -17
  43. package/dist/help.js +4 -1
  44. package/dist/http-delivery.d.ts +5 -9
  45. package/dist/http-delivery.js +20 -77
  46. package/dist/inbox.d.ts +6 -49
  47. package/dist/inbox.js +24 -371
  48. package/dist/index.d.ts +6 -3
  49. package/dist/index.js +4 -1
  50. package/dist/mcp.js +2 -3
  51. package/dist/model.d.ts +7 -47
  52. package/dist/model.js +1 -7
  53. package/dist/notifications.d.ts +24 -20
  54. package/dist/notifications.js +39 -108
  55. package/dist/operation-control.d.ts +4 -0
  56. package/dist/operation-control.js +20 -0
  57. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  58. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  59. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  60. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  61. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  62. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  63. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  64. package/dist/packages/agent-delivery/src/codex.js +41 -4
  65. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  66. package/dist/packages/agent-delivery/src/index.js +1 -1
  67. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  68. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  69. package/dist/paseo-delivery.d.ts +5 -5
  70. package/dist/paseo-delivery.js +19 -97
  71. package/dist/paseo.d.ts +1 -1
  72. package/dist/paseo.js +1 -1
  73. package/dist/plugins/opencode/index.d.ts +5 -3
  74. package/dist/plugins/opencode/index.js +99 -21
  75. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  76. package/dist/plugins/opencode-shared/delivery.js +109 -0
  77. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  78. package/dist/plugins/opencode-v1/index.js +41 -0
  79. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  80. package/dist/plugins/opencode-v1/runtime.js +152 -0
  81. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  82. package/dist/plugins/opencode-v1/transport.js +50 -0
  83. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  84. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  85. package/dist/ports.d.ts +31 -90
  86. package/dist/session-lifecycle.d.ts +8 -7
  87. package/dist/session-lifecycle.js +40 -42
  88. package/dist/sqlite-square.d.ts +14 -23
  89. package/dist/sqlite-square.js +132 -139
  90. package/dist/square-api.js +4 -7
  91. package/dist/square-core.d.ts +0 -1
  92. package/dist/square-domain.d.ts +5 -17
  93. package/dist/square-domain.js +104 -221
  94. package/dist/square-facade.d.ts +18 -15
  95. package/dist/square-post.d.ts +53 -0
  96. package/dist/square-post.js +4 -0
  97. package/dist/square-posting.d.ts +13 -0
  98. package/dist/square-posting.js +174 -0
  99. package/dist/square-producer-sqlite.d.ts +11 -0
  100. package/dist/square-producer-sqlite.js +210 -0
  101. package/dist/square-sqlite.d.ts +15 -25
  102. package/dist/square-sqlite.js +150 -226
  103. package/dist/views.d.ts +2 -2
  104. package/dist/views.js +0 -1
  105. package/dist/wake-sink.d.ts +4 -11
  106. package/dist/wake-sink.js +8 -32
  107. package/dist/wake-transports.d.ts +4 -7
  108. package/dist/wake-transports.js +7 -39
  109. package/examples/symmetric-domains.mjs +29 -249
  110. package/guides/change-waits.md +13 -53
  111. package/guides/claude-native.md +7 -171
  112. package/guides/codex-delivery.md +55 -80
  113. package/guides/delivery-primitives.md +48 -207
  114. package/guides/mcp.md +1 -1
  115. package/guides/operation-cancellation.md +2 -5
  116. package/guides/participant-ownership.md +25 -89
  117. package/guides/pi-native-delivery.md +19 -56
  118. package/guides/pi-tools.md +6 -8
  119. package/guides/square-api.md +113 -615
  120. package/package.json +8 -8
  121. package/plugins/claude/.claude-plugin/plugin.json +2 -3
  122. package/plugins/claude/hooks/hooks.json +11 -1
  123. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  124. package/plugins/codex/hooks/hooks.json +11 -23
  125. package/plugins/opencode/index.ts +91 -23
  126. package/plugins/opencode-shared/delivery.ts +91 -0
  127. package/plugins/opencode-v1/index.ts +36 -0
  128. package/plugins/opencode-v1/runtime.ts +134 -0
  129. package/plugins/opencode-v1/transport.ts +41 -0
  130. package/plugins/paseo/OVERVIEW.md +5 -5
  131. package/plugins/paseo/README.md +7 -6
  132. package/plugins/paseo/client/markdown.tsx +5 -5
  133. package/plugins/paseo/client/square-message.tsx +28 -24
  134. package/plugins/paseo/client/transform.ts +8 -5
  135. package/plugins/paseo/shared/card-presentation.ts +16 -0
  136. package/plugins/paseo/shared/message.ts +6 -11
  137. package/plugins/pi/square-pi.js +156 -417
  138. package/dist/channel-artifact.d.ts +0 -10
  139. package/dist/channel-artifact.js +0 -55
  140. package/dist/channel-delivery.d.ts +0 -11
  141. package/dist/channel-delivery.js +0 -51
  142. package/dist/channel-domain.d.ts +0 -3
  143. package/dist/channel-domain.js +0 -83
  144. package/dist/channel-facade.d.ts +0 -76
  145. package/dist/channel-facade.js +0 -1
  146. package/dist/channel-internal.d.ts +0 -126
  147. package/dist/channel-internal.js +0 -440
  148. package/dist/channel-metadata.d.ts +0 -11
  149. package/dist/channel-metadata.js +0 -1
  150. package/dist/channel-ports.d.ts +0 -241
  151. package/dist/channel-ports.js +0 -1
  152. package/dist/channel-presentation.d.ts +0 -17
  153. package/dist/channel-presentation.js +0 -22
  154. package/dist/channel-sqlite-schema.d.ts +0 -13
  155. package/dist/channel-sqlite-schema.js +0 -81
  156. package/dist/channel-transport.d.ts +0 -62
  157. package/dist/channel-transport.js +0 -1
  158. package/dist/channel.d.ts +0 -84
  159. package/dist/channel.js +0 -60
  160. package/dist/claude-delivery-status.d.ts +0 -13
  161. package/dist/claude-delivery-status.js +0 -46
  162. package/dist/claude-delivery.d.ts +0 -12
  163. package/dist/claude-delivery.js +0 -108
  164. package/dist/claude-mod.d.ts +0 -8
  165. package/dist/claude-mod.js +0 -172
  166. package/dist/claude-transport.d.ts +0 -5
  167. package/dist/claude-transport.js +0 -58
  168. package/dist/cmd/notify-once.d.ts +0 -2
  169. package/dist/cmd/notify-once.js +0 -29
  170. package/dist/delivery-attempts.d.ts +0 -52
  171. package/dist/delivery-attempts.js +0 -73
  172. package/dist/delivery-effects.d.ts +0 -139
  173. package/dist/delivery-effects.js +0 -163
  174. package/dist/delivery-health.d.ts +0 -51
  175. package/dist/delivery-health.js +0 -173
  176. package/dist/delivery-operations.d.ts +0 -22
  177. package/dist/delivery-operations.js +0 -198
  178. package/dist/delivery-primitives.d.ts +0 -5
  179. package/dist/delivery-primitives.js +0 -3
  180. package/dist/delivery.d.ts +0 -56
  181. package/dist/delivery.js +0 -80
  182. package/dist/plugins/opencode/delivery.d.ts +0 -19
  183. package/dist/plugins/opencode/delivery.js +0 -223
  184. package/dist/presentation-operations.d.ts +0 -5
  185. package/dist/presentation-operations.js +0 -86
  186. package/dist/sqlite-channel.d.ts +0 -15
  187. package/dist/sqlite-channel.js +0 -600
  188. package/dist/square-index.d.ts +0 -16
  189. package/dist/square-index.js +0 -106
  190. package/dist/wake-preview.d.ts +0 -7
  191. package/dist/wake-preview.js +0 -15
  192. package/plugins/claude/hooks/register.js +0 -132
  193. package/plugins/claude/types.d.ts +0 -7
  194. package/plugins/opencode/delivery.ts +0 -198
  195. /package/dist/{channel-values.d.ts → square-values.d.ts} +0 -0
  196. /package/dist/{channel-values.js → square-values.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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": {
@@ -50,13 +50,9 @@
50
50
  "ws": "^8.21.3"
51
51
  },
52
52
  "exports": {
53
- "./channel": {
54
- "types": "./dist/channel.d.ts",
55
- "default": "./dist/channel.js"
56
- },
57
- "./delivery": {
58
- "types": "./dist/delivery-primitives.d.ts",
59
- "default": "./dist/delivery-primitives.js"
53
+ "./delivery-ledger": {
54
+ "types": "./dist/delivery-ledger.d.ts",
55
+ "default": "./dist/delivery-ledger.js"
60
56
  },
61
57
  "./receive": {
62
58
  "types": "./dist/receive.d.ts",
@@ -74,6 +70,10 @@
74
70
  "types": "./dist/paseo.d.ts",
75
71
  "default": "./dist/paseo.js"
76
72
  },
73
+ "./opencode-v1": {
74
+ "types": "./dist/plugins/opencode-v1/index.d.ts",
75
+ "default": "./dist/plugins/opencode-v1/index.js"
76
+ },
77
77
  "./server": {
78
78
  "types": "./dist/plugins/opencode/index.d.ts",
79
79
  "default": "./dist/plugins/opencode/index.js"
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.4.0",
4
- "description": "Native Claude Code inbox delivery for Square participants",
5
- "types": "./types.d.ts",
3
+ "version": "0.4.2",
4
+ "description": "Native Claude Code hook delivery for Square participants",
6
5
  "author": {
7
6
  "name": "Square"
8
7
  }
@@ -1 +1,11 @@
1
- {"modules":["./register.js"]}
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
4
+ "SessionEnd": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 3 }] }],
5
+ "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
6
+ "PreToolUse": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
7
+ "PostToolUse": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
8
+ "PostToolUseFailure": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
9
+ "Stop": [{ "hooks": [{ "type": "command", "command": "square claude-watch", "asyncRewake": true, "timeout": 86400 }] }]
10
+ }
11
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -3,38 +3,26 @@
3
3
  "SessionStart": [
4
4
  { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
5
5
  ],
6
- "SessionResume": [
7
- { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
8
- ],
9
6
  "SessionEnd": [
10
7
  { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 3, "statusMessage": "Square" }] }
11
8
  ],
12
9
  "UserPromptSubmit": [
13
10
  { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
14
11
  ],
12
+ "PreToolUse": [
13
+ { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
14
+ ],
15
15
  "PostToolUse": [
16
- {
17
- "hooks": [
18
- {
19
- "type": "command",
20
- "command": "square codex-hook",
21
- "timeout": 5,
22
- "statusMessage": "Square"
23
- }
24
- ]
25
- }
16
+ { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
26
17
  ],
27
18
  "Stop": [
28
- {
29
- "hooks": [
30
- {
31
- "type": "command",
32
- "command": "square codex-hook",
33
- "timeout": 5,
34
- "statusMessage": "Square"
35
- }
36
- ]
37
- }
19
+ { "hooks": [
20
+ { "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" },
21
+ { "type": "command", "command": "square codex-watch", "timeout": 86400, "async": true, "statusMessage": "Square" }
22
+ ] }
23
+ ],
24
+ "Interrupt": [
25
+ { "hooks": [{ "type": "command", "command": "square codex-interrupt", "timeout": 3, "statusMessage": "Square" }] }
38
26
  ]
39
27
  }
40
28
  }
@@ -1,7 +1,32 @@
1
- import { observeSquareIndex } from '../../square-index.js';
1
+ import { createHash } from 'node:crypto';
2
+ import { renderDeliveryReminder } from '../../delivery-frame.js';
3
+ import type { DeliveryTransport } from '../../delivery-ledger.js';
2
4
  import { canonicalPath } from '../../canonical-path.js';
3
- import { connectNative, type OpenCodeNativeSession, type OpenCodeSessionCapability } from '../../packages/agent-delivery/src/opencode-native.js';
4
- import { cancelOpenCodeDeliveries, endOpenCodeSession, indexedOpenCodeSessions, observeOpenCodeContext, publishOpenCodeAddress, receiveOpenCodePending, type OpenCodeContextMessage } from './delivery.js';
5
+ import { connectNative, createNativeInputId, sendNativeText, type OpenCodeNativeSession, type OpenCodeSessionCapability } from '../../packages/agent-delivery/src/opencode-native.js';
6
+ import { cancelOpenCodeDeliveries, currentOpenCodeGeneration, detachOpenCodeSession, endOpenCodeSession, listOpenCodeSessions, observeOpenCodeContext, observeOpenCodeSessions, publishOpenCodeAddress, receiveOpenCodePending, type OpenCodeContextMessage } from '../opencode-shared/delivery.js';
7
+
8
+ const routeKind = 'opencode-server' as const;
9
+
10
+ export function createOpenCodeWakeTransport(target: OpenCodeNativeSession, lifetime: AbortSignal): DeliveryTransport {
11
+ return {
12
+ kind: routeKind,
13
+ async send(card, address, context) {
14
+ const signal = AbortSignal.any([lifetime, context.signal]);
15
+ if (address.sessionId !== target.sessionId || signal.aborted) return 'not-capable';
16
+ const messageId = createNativeInputId();
17
+ const text = renderDeliveryReminder([card.text], { tagged: true });
18
+ const payloadHash = createHash('sha256').update(text).digest('hex');
19
+ if (!await context.prepare({ token: context.token, messageId, payloadHash }) || signal.aborted
20
+ || !await context.beforeSend() || signal.aborted || Date.now() >= context.deadline) return 'not-capable';
21
+ const result = await sendNativeText(target, text, {
22
+ inputId: messageId, delivery: 'steer', timeoutMs: Math.max(1, context.deadline - Date.now()), signal,
23
+ });
24
+ if (result.state === 'accepted') return 'accepted';
25
+ if (result.state === 'unavailable') return 'failed';
26
+ return 'unknown';
27
+ },
28
+ };
29
+ }
5
30
 
6
31
  interface OpenCodePluginContext {
7
32
  readonly app: { readonly version: string };
@@ -12,17 +37,33 @@ interface OpenCodePluginContext {
12
37
  };
13
38
  readonly event: { subscribe(options: { signal: AbortSignal }): AsyncIterable<{ type: string; data: { sessionID?: string; reason?: string } }> };
14
39
  }
15
- interface Receiver { readonly abort: AbortController; readonly running: Promise<void>; settled: boolean }
40
+ interface Receiver { readonly abort: AbortController; readonly running: Promise<void>; readonly generation: string; settled: boolean }
16
41
 
17
- /** Actual OpenCode 2.0.20 server plugin. Harness identity belongs to ctx, never process.env. */
42
+ /** OpenCode v2 server plugin. Harness identity belongs to ctx, never process.env. */
18
43
  export default {
19
44
  id: 'square',
20
45
  async setup(ctx: OpenCodePluginContext): Promise<() => Promise<void>> {
21
- if (ctx.app.version !== '2.0.20') return async () => {};
46
+ // OpenCode publishes semantic versions; unknown version forms are not evidence of a v2 host.
47
+ const version = ctx?.app?.version;
48
+ const reason = typeof version !== 'string' || !/^2\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)
49
+ ? 'expected an OpenCode 2.x semantic version'
50
+ : typeof ctx.location?.directory !== 'string' || !ctx.location.directory ||
51
+ typeof ctx.location.project?.id !== 'string' || !ctx.location.project.id ||
52
+ (ctx.location.workspaceID !== undefined && typeof ctx.location.workspaceID !== 'string')
53
+ ? 'missing location directory or project identity'
54
+ : typeof ctx.session?.hook !== 'function' || typeof ctx.session.get !== 'function' || typeof ctx.session.prompt !== 'function'
55
+ ? 'missing session hook, get, or prompt capability'
56
+ : typeof ctx.event?.subscribe !== 'function' ? 'missing event subscription capability' : undefined;
57
+ if (reason) {
58
+ console.warn(`Square OpenCode plugin inactive: ${reason}.`);
59
+ return async () => {};
60
+ }
22
61
  const env = { ...process.env };
23
- const index = await observeSquareIndex(env);
62
+ const discovery = await observeOpenCodeSessions(env);
24
63
  const lifetime = new AbortController();
25
64
  const receivers = new Map<string, Receiver>(); // Operational handles only; presence owns membership.
65
+ const interrupting = new Set<string>();
66
+ const cancellationFailed = new Set<string>();
26
67
  const directory = await canonicalPath(ctx.location.directory);
27
68
  let coordinating = Promise.resolve();
28
69
  const coordinate = (operation: () => Promise<void>) => {
@@ -49,31 +90,49 @@ export default {
49
90
  return target;
50
91
  };
51
92
  const reconcile = async (activeSession?: string) => {
52
- const candidates = new Set(await indexedOpenCodeSessions(env));
93
+ const candidates = new Set(await listOpenCodeSessions(env));
53
94
  if (activeSession) candidates.add(activeSession);
54
95
  for (const sessionId of candidates) {
55
96
  if (lifetime.signal.aborted) return;
97
+ if (interrupting.has(sessionId) || cancellationFailed.has(sessionId)) continue;
56
98
  const target = await validate(sessionId);
57
- if (!target) { stop(sessionId); continue; }
58
- if (!await publishOpenCodeAddress(sessionId, env, lifetime.signal)) { stop(sessionId); continue; }
59
- if (receivers.get(sessionId)?.settled === false) continue;
99
+ if (!target) {
100
+ const receiver = receivers.get(sessionId);
101
+ stop(sessionId);
102
+ if (receiver) await detachOpenCodeSession(sessionId, receiver.generation, env);
103
+ continue;
104
+ }
105
+ const existing = receivers.get(sessionId);
106
+ if (existing?.settled === false || interrupting.has(sessionId)) continue;
107
+ const generation = existing?.generation ?? await publishOpenCodeAddress(sessionId, env, lifetime.signal);
108
+ if (!generation) { stop(sessionId); continue; }
109
+ if (lifetime.signal.aborted || interrupting.has(sessionId)) { await detachOpenCodeSession(sessionId, generation, env); continue; }
60
110
  const abort = new AbortController();
61
111
  const signal = AbortSignal.any([lifetime.signal, abort.signal]);
62
- const receiver: Receiver = { abort, settled: false, running: receiveOpenCodePending(target, env, signal).catch(() => {}).finally(() => { receiver.settled = true; }) };
112
+ const receiver: Receiver = { abort, generation, settled: false, running: receiveOpenCodePending(sessionId, generation, createOpenCodeWakeTransport(target, signal), env, signal).catch(() => {}).finally(() => { receiver.settled = true; }) };
63
113
  receivers.set(sessionId, receiver);
64
114
  }
65
115
  for (const sessionId of receivers.keys()) if (!candidates.has(sessionId)) stop(sessionId);
66
116
  };
67
117
  const cancel = async (sessionId: string) => {
68
- if (!receivers.has(sessionId)) return;
69
- // Stop synchronously; persist the cutoff before allowing reconciliation to start fresh work.
118
+ const receiver = receivers.get(sessionId);
119
+ if (!receiver) return;
70
120
  stop(sessionId);
71
- await cancelOpenCodeDeliveries(sessionId, env, lifetime.signal);
72
- await reconcile();
121
+ await receiver.running;
122
+ try {
123
+ if (!await cancelOpenCodeDeliveries(sessionId, receiver.generation, env)) throw new Error('OpenCode cancellation fence lost');
124
+ } catch (error) {
125
+ cancellationFailed.add(sessionId);
126
+ await detachOpenCodeSession(sessionId, receiver.generation, env).catch(() => {});
127
+ throw error;
128
+ } finally { interrupting.delete(sessionId); }
129
+ await reconcile(sessionId);
73
130
  };
74
131
  const end = async (sessionId: string) => {
132
+ const receiver = receivers.get(sessionId);
75
133
  stop(sessionId);
76
- await endOpenCodeSession(sessionId, env, lifetime.signal);
134
+ const generation = receiver?.generation ?? await currentOpenCodeGeneration(sessionId, env);
135
+ if (generation) await endOpenCodeSession(sessionId, generation, env);
77
136
  };
78
137
 
79
138
  await ctx.session.hook('prompt', (event) => coordinate(() => reconcile(event.sessionID)));
@@ -81,14 +140,14 @@ export default {
81
140
  await coordinate(() => reconcile(event.sessionID));
82
141
  const receiver = receivers.get(event.sessionID);
83
142
  if (!receiver || lifetime.signal.aborted) return;
84
- await observeOpenCodeContext(event.sessionID, event.messages, env,
143
+ await observeOpenCodeContext(event.sessionID, receiver.generation, event.messages, env,
85
144
  AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => {});
86
145
  });
87
146
  const presenceLoop = (async () => {
88
147
  while (!lifetime.signal.aborted) {
89
- const baseline = await index.read(lifetime.signal);
148
+ const baseline = discovery.observer.generation;
90
149
  await coordinate(() => reconcile());
91
- await index.changed(baseline, Infinity, lifetime.signal);
150
+ await discovery.observer.hinted(baseline, 2000, lifetime.signal);
92
151
  }
93
152
  })().catch(() => {});
94
153
  const eventLoop = (async () => {
@@ -96,8 +155,15 @@ export default {
96
155
  const sessionId = event.data.sessionID;
97
156
  if (!sessionId) continue;
98
157
  if (event.type === 'session.deleted') await coordinate(() => end(sessionId));
99
- else if (event.type === 'session.moved') await coordinate(async () => { stop(sessionId); await reconcile(); });
158
+ else if (event.type === 'session.moved') await coordinate(async () => {
159
+ const receiver = receivers.get(sessionId);
160
+ stop(sessionId);
161
+ if (receiver) await detachOpenCodeSession(sessionId, receiver.generation, env);
162
+ await reconcile();
163
+ });
100
164
  else if (event.type === 'session.execution.interrupted' && event.data.reason === 'user') {
165
+ if (interrupting.has(sessionId) || !receivers.has(sessionId)) continue;
166
+ interrupting.add(sessionId);
101
167
  receivers.get(sessionId)?.abort.abort();
102
168
  await coordinate(() => cancel(sessionId));
103
169
  }
@@ -106,11 +172,13 @@ export default {
106
172
  // Do not await any native prompt in setup: plugin activation itself is lazy.
107
173
  return async () => {
108
174
  lifetime.abort();
109
- index.close();
110
- const tasks = [presenceLoop, eventLoop, coordinating, ...[...receivers.keys()].map(stop)];
175
+ const captured = [...receivers.entries()];
176
+ const tasks = [presenceLoop, eventLoop, coordinating, ...captured.map(([id]) => stop(id))];
111
177
  let timer: ReturnType<typeof setTimeout> | undefined;
112
178
  try { await Promise.race([Promise.allSettled(tasks), new Promise<void>((resolve) => { timer = setTimeout(resolve, 3000); })]); }
113
179
  finally { if (timer !== undefined) clearTimeout(timer); }
180
+ await Promise.allSettled(captured.map(([id, receiver]) => detachOpenCodeSession(id, receiver.generation, env)));
181
+ await discovery.close();
114
182
  // Instance unload is not session deletion: retain memberships and uncertain correlations.
115
183
  };
116
184
  },
@@ -0,0 +1,91 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { DeliveryLedger, type DeliveryTransport } from '../../delivery-ledger.js';
3
+ import { retainDeliveryLedger } from '../../delivery-lifetime.js';
4
+ import { harnessOwner, decodeHarnessOwner } from '../../owner.js';
5
+ import { attachDeliverySessionRoutes, detachDeliverySession, markDeliverySessionDeleted } from '../../session-lifecycle.js';
6
+
7
+ const routeKind = 'opencode-server';
8
+ const ownerFor = (sessionId: string) => harnessOwner({ channel: 'opencode', session: sessionId });
9
+ const open = (env: NodeJS.ProcessEnv) => DeliveryLedger.open(env.SQUARE_DELIVERY ? { location: env.SQUARE_DELIVERY } : {});
10
+ const payloadHash = (payload: string) => createHash('sha256').update(payload).digest('hex');
11
+ export interface OpenCodeContextMessage { readonly id?: string; readonly role: string; readonly content: readonly { readonly type: string; readonly text?: string }[] }
12
+
13
+ export async function listOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<readonly string[]> {
14
+ const ledger = await open(env);
15
+ try {
16
+ return (await ledger.sessions()).flatMap(session => {
17
+ const coordinate = decodeHarnessOwner(session.owner);
18
+ return session.state !== 'deleted' && coordinate?.channel === 'opencode' ? [coordinate.session] : [];
19
+ });
20
+ } finally { await ledger.close(); }
21
+ }
22
+
23
+ /** The file observer is a hint; callers must always reread the ledger after an edge. */
24
+ export async function observeOpenCodeSessions(env: NodeJS.ProcessEnv) {
25
+ const ledger = await open(env);
26
+ const observer = await ledger.observe(ownerFor('discovery'));
27
+ return { observer, close: async () => { observer.close(); await ledger.close(); } };
28
+ }
29
+
30
+ export async function publishOpenCodeAddress(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<string | undefined> {
31
+ const session = await attachDeliverySessionRoutes(ownerFor(sessionId), env, [{ kind: routeKind, address: { sessionId } }], signal);
32
+ return session ? session.generation : undefined;
33
+ }
34
+
35
+ export async function currentOpenCodeGeneration(sessionId: string, env: NodeJS.ProcessEnv): Promise<string | undefined> {
36
+ const ledger = await open(env);
37
+ try { return (await ledger.session(ownerFor(sessionId)))?.generation; } finally { await ledger.close(); }
38
+ }
39
+
40
+ export async function detachOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void> {
41
+ await detachDeliverySession(ownerFor(sessionId), generation, env);
42
+ }
43
+ export async function endOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void> {
44
+ await markDeliverySessionDeleted(ownerFor(sessionId), generation, env);
45
+ }
46
+ export async function cancelOpenCodeDeliveries(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<boolean> {
47
+ const ledger = await open(env);
48
+ try { return await ledger.cancel({ owner: ownerFor(sessionId), generation }); } finally { await ledger.close(); }
49
+ }
50
+
51
+ function textMessage(message: OpenCodeContextMessage): string | undefined {
52
+ if (message.role !== 'user' || message.id === undefined || !message.content.every(part => part.type === 'text' && typeof part.text === 'string')) return undefined;
53
+ return message.content.map(part => part.text!).join('');
54
+ }
55
+
56
+ /** Confirmation is observer-level: another host plugin may subsequently remove the message. */
57
+ export async function observeOpenCodeContext(sessionId: string, generation: string, messages: readonly OpenCodeContextMessage[], env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
58
+ const observed = new Map(messages.flatMap(message => {
59
+ const text = textMessage(message);
60
+ return text === undefined ? [] : [[message.id!, payloadHash(text)] as const];
61
+ }));
62
+ if (!observed.size || signal.aborted) return;
63
+ const ledger = await open(env);
64
+ try {
65
+ for (const attempt of await ledger.handoffs(ownerFor(sessionId), { generation })) {
66
+ if (signal.aborted) return;
67
+ const { correlation } = attempt;
68
+ if (attempt.routeKind === routeKind && correlation.token === attempt.token && correlation.messageId !== undefined
69
+ && correlation.payloadHash !== undefined && observed.get(correlation.messageId) === correlation.payloadHash)
70
+ await ledger.observePresentation({ token: attempt.token, owner: ownerFor(sessionId), generation, routeKind, payloadHash: correlation.payloadHash }, { requireCurrent: true });
71
+ }
72
+ } finally { await ledger.close(); }
73
+ }
74
+
75
+ export async function receiveOpenCodePending(sessionId: string, generation: string, transport: DeliveryTransport, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
76
+ const owner = ownerFor(sessionId);
77
+ const ledger = await open(env);
78
+ const lifetime = retainDeliveryLedger(ledger);
79
+ const observer = await ledger.observe(owner);
80
+ try {
81
+ while (!signal.aborted) {
82
+ const baseline = observer.generation;
83
+ for (const card of await ledger.pending(owner, { generation })) {
84
+ if (signal.aborted) return;
85
+ lifetime.track(await ledger.dispatchHandoff(owner, card.key, transport, { generation, timeoutMs: 5000, signal }));
86
+ }
87
+ if (!signal.aborted) await observer.hinted(baseline, 2000, signal);
88
+ }
89
+ } catch (error) { if (!signal.aborted) throw error; }
90
+ finally { observer.close(); await lifetime.close(); }
91
+ }
@@ -0,0 +1,36 @@
1
+ interface LegacyContext {
2
+ readonly serverUrl: string | URL;
3
+ readonly directory: string;
4
+ readonly project: { readonly id: string };
5
+ readonly client: { readonly session: {
6
+ get(input: unknown): Promise<{ data?: unknown; error?: unknown }>;
7
+ promptAsync(input: unknown): Promise<{ error?: unknown; response?: Response }>;
8
+ } };
9
+ }
10
+
11
+ /** Legacy loader: no storage imports until the actual host and runtime qualify. */
12
+ export default async function squareOpenCodeV1(ctx: LegacyContext) {
13
+ const shell = async (input: { sessionID?: string }, output: { env: Record<string, string> }) => {
14
+ if (typeof input.sessionID === 'string' && input.sessionID) output.env.OPENCODE_SESSION_ID = input.sessionID;
15
+ };
16
+ const reduced = { 'shell.env': shell };
17
+ try {
18
+ let response: Response;
19
+ try { response = await fetch(new URL('/global/health', ctx.serverUrl), { signal: AbortSignal.timeout(1500) }); }
20
+ catch { throw new Error('health endpoint unreachable (embedded TUI may not expose it)'); }
21
+ if (!response.ok) throw new Error(`health endpoint returned HTTP ${response.status} (embedded TUI may not expose it)`);
22
+ const health = await response.json() as { version?: unknown };
23
+ const match = typeof health.version === 'string' && /^(\d+)\.(\d+)\.(\d+)$/.exec(health.version);
24
+ if (!match || Number(match[1]) !== 1 || Number(match[2]) < 15 || (Number(match[2]) === 15 && Number(match[3]) < 11)) throw new Error('native version below resident floor or unknown');
25
+ if (typeof ctx.client?.session?.get !== 'function' || typeof ctx.client.session.promptAsync !== 'function') throw new Error('native session API unavailable');
26
+ const { DatabaseSync } = await import('node:sqlite');
27
+ const db = new DatabaseSync(':memory:');
28
+ try { db.exec('CREATE TABLE capability (value TEXT)'); db.prepare('INSERT INTO capability VALUES (?)').run('ok'); db.prepare('SELECT value FROM capability').get(); }
29
+ finally { db.close(); }
30
+ const { startReceiver } = await import('./runtime.js');
31
+ return { ...reduced, ...await startReceiver(ctx) };
32
+ } catch (error) {
33
+ console.warn(`[square] OpenCode 1.x reduced mode (no proactive delivery): ${error instanceof Error ? error.message : String(error)}`);
34
+ return reduced;
35
+ }
36
+ }
@@ -0,0 +1,134 @@
1
+ import { canonicalPath } from '../../canonical-path.js';
2
+ import { cancelOpenCodeDeliveries, currentOpenCodeGeneration, detachOpenCodeSession, endOpenCodeSession, listOpenCodeSessions, observeOpenCodeContext, observeOpenCodeSessions, publishOpenCodeAddress, receiveOpenCodePending } from '../opencode-shared/delivery.js';
3
+ import { legacyTransport } from './transport.js';
4
+
5
+ interface Context {
6
+ readonly directory: string;
7
+ readonly project: { readonly id: string };
8
+ readonly client: { readonly session: {
9
+ get(input: unknown): Promise<{ data?: unknown; error?: unknown }>;
10
+ promptAsync(input: unknown): Promise<{ error?: unknown; response?: Response }>;
11
+ } };
12
+ }
13
+ interface NativeMessage { info?: { id?: string; sessionID?: string; role?: string }; parts?: { type?: string; text?: string; sessionID?: string; messageID?: string; synthetic?: boolean; ignored?: boolean }[] }
14
+ interface Receiver { abort: AbortController; running: Promise<void>; generation: string; settled: boolean }
15
+
16
+ export async function startReceiver(ctx: Context) {
17
+ const env = { ...process.env };
18
+ const directory = await canonicalPath(ctx.directory);
19
+ const discovery = await observeOpenCodeSessions(env);
20
+ const lifetime = new AbortController();
21
+ const receivers = new Map<string, Receiver>();
22
+ const terminal = new Set<string>();
23
+ const interrupting = new Set<string>();
24
+ const cancellationFailed = new Set<string>();
25
+ let coordinating = Promise.resolve();
26
+ const coordinate = (operation: () => Promise<void>) => {
27
+ coordinating = coordinating.then(() => lifetime.signal.aborted ? undefined : operation()).catch(() => {});
28
+ return coordinating;
29
+ };
30
+ const stop = (id: string) => {
31
+ const receiver = receivers.get(id);
32
+ receiver?.abort.abort();
33
+ receivers.delete(id);
34
+ return receiver?.running;
35
+ };
36
+ const validate = async (id: string) => {
37
+ const result = await ctx.client.session.get({ path: { id }, signal: lifetime.signal }).catch(() => undefined);
38
+ const info = result?.data as { id?: string; projectID?: string; directory?: string } | undefined;
39
+ return !result?.error && info?.id === id && info.projectID === ctx.project.id && typeof info.directory === 'string'
40
+ && await canonicalPath(info.directory) === directory;
41
+ };
42
+ const reconcile = async (active?: string) => {
43
+ const candidates = new Set(await listOpenCodeSessions(env));
44
+ if (active) candidates.add(active);
45
+ for (const id of candidates) {
46
+ if (lifetime.signal.aborted) return;
47
+ if (interrupting.has(id) || cancellationFailed.has(id)) continue;
48
+ if (terminal.has(id) || !await validate(id) || terminal.has(id)) {
49
+ const receiver = receivers.get(id);
50
+ stop(id);
51
+ if (receiver) await detachOpenCodeSession(id, receiver.generation, env);
52
+ continue;
53
+ }
54
+ const existing = receivers.get(id);
55
+ if (existing?.settled === false || interrupting.has(id)) continue;
56
+ const generation = existing?.generation ?? await publishOpenCodeAddress(id, env, lifetime.signal);
57
+ if (!generation) { stop(id); continue; }
58
+ if (terminal.has(id) || interrupting.has(id) || lifetime.signal.aborted) { await detachOpenCodeSession(id, generation, env); continue; }
59
+ const abort = new AbortController();
60
+ const signal = AbortSignal.any([lifetime.signal, abort.signal]);
61
+ const receiver: Receiver = { abort, generation, settled: false, running: receiveOpenCodePending(id, generation, legacyTransport(ctx.client, id, signal), env, signal).catch(() => {}).finally(() => { receiver.settled = true; }) };
62
+ receivers.set(id, receiver);
63
+ }
64
+ for (const id of receivers.keys()) if (!candidates.has(id)) stop(id);
65
+ };
66
+ const loop = (async () => {
67
+ while (!lifetime.signal.aborted) {
68
+ const baseline = discovery.observer.generation;
69
+ await coordinate(() => reconcile());
70
+ await discovery.observer.hinted(baseline, 2000, lifetime.signal);
71
+ }
72
+ })().catch(() => {});
73
+ let disposal: Promise<void> | undefined;
74
+ return {
75
+ 'experimental.chat.messages.transform': async (_input: unknown, output: { messages: NativeMessage[] }) => {
76
+ const ids = new Set(output.messages.map(message => message.info?.sessionID));
77
+ if (ids.size !== 1 || typeof [...ids][0] !== 'string') return;
78
+ const id = [...ids][0]!;
79
+ if (terminal.has(id)) return;
80
+ await coordinate(() => reconcile(id));
81
+ const receiver = receivers.get(id);
82
+ if (!receiver || terminal.has(id) || lifetime.signal.aborted) return;
83
+ const messages = output.messages.flatMap(({ info, parts }) => {
84
+ if (info?.role !== 'user' || !info.id || !parts?.length || !parts.every(part => part.type === 'text'
85
+ && typeof part.text === 'string' && part.sessionID === id && part.messageID === info.id && !part.synthetic && !part.ignored)) return [];
86
+ return [{ id: info.id, role: 'user', content: parts.map(part => ({ type: 'text', text: part.text })) }];
87
+ });
88
+ await observeOpenCodeContext(id, receiver.generation, messages, env, AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => {});
89
+ },
90
+ event: async ({ event }: { event: { type: string; properties?: { sessionID?: string; error?: { name?: string }; info?: { id?: string; projectID?: string; directory?: string } } } }) => {
91
+ const interrupted = event.type === 'session.error' && event.properties?.error?.name === 'MessageAbortedError'
92
+ ? event.properties.sessionID : undefined;
93
+ if (interrupted) {
94
+ const receiver = receivers.get(interrupted);
95
+ if (!receiver || interrupting.has(interrupted)) return;
96
+ interrupting.add(interrupted);
97
+ stop(interrupted);
98
+ await coordinate(async () => {
99
+ try {
100
+ await receiver.running;
101
+ if (!await cancelOpenCodeDeliveries(interrupted, receiver.generation, env)) throw new Error('OpenCode cancellation fence lost');
102
+ } catch (error) {
103
+ cancellationFailed.add(interrupted);
104
+ await detachOpenCodeSession(interrupted, receiver.generation, env).catch(() => {});
105
+ throw error;
106
+ } finally { interrupting.delete(interrupted); }
107
+ await reconcile(interrupted);
108
+ });
109
+ return;
110
+ }
111
+ const info = event.properties?.info;
112
+ if (event.type !== 'session.deleted' || !info?.id || info.projectID !== ctx.project.id || typeof info.directory !== 'string'
113
+ || await canonicalPath(info.directory).catch(() => undefined) !== directory) return;
114
+ const id = info.id;
115
+ terminal.add(id);
116
+ const receiver = receivers.get(id);
117
+ stop(id);
118
+ await coordinate(async () => {
119
+ const generation = receiver?.generation ?? await currentOpenCodeGeneration(id, env);
120
+ if (generation) await endOpenCodeSession(id, generation, env);
121
+ });
122
+ },
123
+ dispose: () => disposal ??= (async () => {
124
+ lifetime.abort();
125
+ const captured = [...receivers.entries()];
126
+ const tasks = [loop, coordinating, ...captured.map(([id]) => stop(id))];
127
+ let timer: ReturnType<typeof setTimeout> | undefined;
128
+ try { await Promise.race([Promise.allSettled(tasks), new Promise<void>(resolve => { timer = setTimeout(resolve, 3000); })]); }
129
+ finally { if (timer !== undefined) clearTimeout(timer); }
130
+ await Promise.allSettled(captured.map(([id, receiver]) => detachOpenCodeSession(id, receiver.generation, env)));
131
+ await discovery.close();
132
+ })(),
133
+ };
134
+ }
@@ -0,0 +1,41 @@
1
+ import { randomInt, createHash } from 'node:crypto';
2
+ import { renderDeliveryReminder } from '../../delivery-frame.js';
3
+ import type { DeliveryTransport } from '../../delivery-ledger.js';
4
+
5
+ type Client = { session: { promptAsync(input: unknown): Promise<{ error?: unknown; response?: Response }> } };
6
+ let lastTime = 0, counter = 0;
7
+ const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
8
+
9
+ /** OpenCode's 48-bit timestamp/counter prefix followed by 14 random base62 digits. */
10
+ export function nativeMessageId(): string {
11
+ const now = Math.max(Date.now(), lastTime);
12
+ counter = now === lastTime ? counter + 1 : 0;
13
+ if (counter >= 0x1000) { lastTime = now + 1; counter = 0; } else lastTime = now;
14
+ const prefix = (BigInt(lastTime) * 0x1000n + BigInt(counter)).toString(16).slice(-12).padStart(12, '0');
15
+ return `msg_${prefix}${Array.from({ length: 14 }, () => alphabet[randomInt(62)]).join('')}`;
16
+ }
17
+
18
+ export function legacyTransport(client: Client, sessionId: string, lifetime: AbortSignal): DeliveryTransport {
19
+ return {
20
+ kind: 'opencode-server',
21
+ async send(card, address, context) {
22
+ const signal = AbortSignal.any([lifetime, context.signal]);
23
+ if (address.sessionId !== sessionId || signal.aborted) return 'not-capable';
24
+ const messageId = nativeMessageId();
25
+ const text = renderDeliveryReminder([card.text], { tagged: true });
26
+ const payloadHash = createHash('sha256').update(text).digest('hex');
27
+ if (!await context.prepare({ token: context.token, messageId, payloadHash }) || signal.aborted
28
+ || !await context.beforeSend() || signal.aborted || Date.now() >= context.deadline) return 'not-capable';
29
+ let onAbort: (() => void) | undefined;
30
+ try {
31
+ await Promise.race([
32
+ client.session.promptAsync({ path: { id: sessionId }, body: { messageID: messageId, parts: [{ type: 'text', text }] }, signal }),
33
+ new Promise<never>((_, reject) => { onAbort = () => reject(signal.reason); signal.addEventListener('abort', onAbort, { once: true }); if (signal.aborted) onAbort(); }),
34
+ ]);
35
+ // promptAsync acknowledges only submission, not native acceptance or presentation.
36
+ return 'unknown';
37
+ } catch { return 'unknown'; }
38
+ finally { if (onAbort) signal.removeEventListener('abort', onAbort); }
39
+ },
40
+ };
41
+ }
@@ -5,10 +5,10 @@ who reached you and where. Expand it to see the original custom-message content,
5
5
  select text, or copy it in full. The card follows the active Paseo theme on
6
6
  desktop and mobile.
7
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.
8
+ Requires Paseo 0.11.2 or newer with custom-message tool rows (#6090). Structured messages
9
+ carry producer-neutral origin, author, reference, badge and full body details. Plain messages and
10
+ joining context use a neutral notification heading.
11
11
 
12
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.
13
+ context. Expansion shows the delivered full display body, within a bounded preview,
14
+ while model-facing text stays clipped as before. No producer storage fetch is needed.