@astrosheep/square 0.3.106 → 0.4.1

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 (206) hide show
  1. package/dist/activity-feed.js +1 -1
  2. package/dist/artifact-store.d.ts +41 -0
  3. package/dist/artifact-store.js +272 -0
  4. package/dist/artifact.d.ts +6 -12
  5. package/dist/artifact.js +43 -344
  6. package/dist/attention-presentation.d.ts +13 -7
  7. package/dist/attention-presentation.js +29 -68
  8. package/dist/boundary-presentation.d.ts +3 -16
  9. package/dist/boundary-presentation.js +29 -109
  10. package/dist/card-presentation.d.ts +1 -0
  11. package/dist/card-presentation.js +2 -0
  12. package/dist/catch-decisions.d.ts +2 -2
  13. package/dist/catch-decisions.js +7 -3
  14. package/dist/catch-selection.d.ts +8 -0
  15. package/dist/catch-selection.js +5 -0
  16. package/dist/claude-delivery.d.ts +14 -12
  17. package/dist/claude-delivery.js +42 -142
  18. package/dist/claude-mcp-context.js +2 -2
  19. package/dist/claude-mod.js +28 -140
  20. package/dist/claude-transport.d.ts +5 -0
  21. package/dist/claude-transport.js +34 -0
  22. package/dist/cli/context.d.ts +2 -2
  23. package/dist/cli/context.js +7 -10
  24. package/dist/cli/maintenance-commands.d.ts +0 -4
  25. package/dist/cli/maintenance-commands.js +2 -29
  26. package/dist/cli/observation-commands.d.ts +2 -0
  27. package/dist/cli/observation-commands.js +12 -2
  28. package/dist/cli/program.js +1 -1
  29. package/dist/cli/registry.js +4 -3
  30. package/dist/codex-address.d.ts +2 -2
  31. package/dist/codex-boundary-state.d.ts +1 -5
  32. package/dist/codex-boundary-state.js +0 -4
  33. package/dist/codex-hook.d.ts +7 -10
  34. package/dist/codex-hook.js +69 -33
  35. package/dist/codex-queue.d.ts +10 -11
  36. package/dist/codex-queue.js +32 -34
  37. package/dist/codex-watch.d.ts +2 -0
  38. package/dist/codex-watch.js +100 -0
  39. package/dist/decisions.js +3 -3
  40. package/dist/delivery-deferral.d.ts +1 -19
  41. package/dist/delivery-deferral.js +1 -29
  42. package/dist/delivery-frame.d.ts +7 -0
  43. package/dist/delivery-frame.js +11 -0
  44. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  45. package/dist/delivery-ledger-sqlite.js +496 -0
  46. package/dist/delivery-ledger.d.ts +196 -0
  47. package/dist/delivery-ledger.js +268 -0
  48. package/dist/delivery-lifetime.d.ts +6 -0
  49. package/dist/delivery-lifetime.js +45 -0
  50. package/dist/harness-links.js +61 -7
  51. package/dist/harness.js +12 -17
  52. package/dist/help.js +3 -6
  53. package/dist/http-delivery.d.ts +5 -9
  54. package/dist/http-delivery.js +20 -79
  55. package/dist/inbox.d.ts +6 -57
  56. package/dist/inbox.js +24 -380
  57. package/dist/index.d.ts +7 -4
  58. package/dist/index.js +5 -2
  59. package/dist/mcp.d.ts +2 -1
  60. package/dist/mcp.js +40 -70
  61. package/dist/message.d.ts +7 -0
  62. package/dist/message.js +23 -0
  63. package/dist/model.d.ts +15 -91
  64. package/dist/model.js +2 -140
  65. package/dist/neutral.d.ts +72 -0
  66. package/dist/neutral.js +27 -0
  67. package/dist/notifications.d.ts +24 -26
  68. package/dist/notifications.js +39 -143
  69. package/dist/operation-control.d.ts +4 -0
  70. package/dist/operation-control.js +20 -0
  71. package/dist/operation-presentation.d.ts +1 -2
  72. package/dist/operation-presentation.js +1 -7
  73. package/dist/owner.d.ts +2 -2
  74. package/dist/owner.js +1 -1
  75. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  76. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  77. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  78. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  79. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  80. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  81. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  82. package/dist/packages/agent-delivery/src/codex.js +41 -4
  83. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  84. package/dist/packages/agent-delivery/src/index.js +1 -1
  85. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  86. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  87. package/dist/paseo-delivery.d.ts +5 -5
  88. package/dist/paseo-delivery.js +19 -99
  89. package/dist/paseo.d.ts +1 -1
  90. package/dist/paseo.js +1 -1
  91. package/dist/plugins/opencode/index.d.ts +5 -3
  92. package/dist/plugins/opencode/index.js +99 -21
  93. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  94. package/dist/plugins/opencode-shared/delivery.js +109 -0
  95. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  96. package/dist/plugins/opencode-v1/index.js +41 -0
  97. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  98. package/dist/plugins/opencode-v1/runtime.js +152 -0
  99. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  100. package/dist/plugins/opencode-v1/transport.js +50 -0
  101. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  102. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  103. package/dist/ports.d.ts +33 -214
  104. package/dist/presentation.d.ts +5 -5
  105. package/dist/presentation.js +1 -0
  106. package/dist/runtime.js +2 -2
  107. package/dist/search.js +1 -1
  108. package/dist/session-lifecycle.d.ts +8 -7
  109. package/dist/session-lifecycle.js +40 -42
  110. package/dist/sqlite-framing.d.ts +8 -0
  111. package/dist/sqlite-framing.js +31 -0
  112. package/dist/sqlite-square.d.ts +16 -26
  113. package/dist/sqlite-square.js +199 -262
  114. package/dist/sqlite-values.d.ts +21 -0
  115. package/dist/sqlite-values.js +78 -0
  116. package/dist/square-api.d.ts +1 -1
  117. package/dist/square-api.js +5 -8
  118. package/dist/square-core.d.ts +0 -1
  119. package/dist/square-domain.d.ts +12 -10
  120. package/dist/square-domain.js +105 -223
  121. package/dist/square-facade.d.ts +25 -76
  122. package/dist/square-location.d.ts +6 -1
  123. package/dist/square-location.js +1 -1
  124. package/dist/square-post.d.ts +53 -0
  125. package/dist/square-post.js +4 -0
  126. package/dist/square-posting.d.ts +13 -0
  127. package/dist/square-posting.js +174 -0
  128. package/dist/square-producer-sqlite.d.ts +11 -0
  129. package/dist/square-producer-sqlite.js +210 -0
  130. package/dist/square-receive.d.ts +2 -3
  131. package/dist/square-sqlite.d.ts +33 -42
  132. package/dist/square-sqlite.js +236 -768
  133. package/dist/square-tools.d.ts +8 -0
  134. package/dist/square-tools.js +39 -0
  135. package/dist/square-values.d.ts +27 -0
  136. package/dist/square-values.js +90 -0
  137. package/dist/views.d.ts +6 -6
  138. package/dist/views.js +14 -9
  139. package/dist/wake-sink.d.ts +4 -11
  140. package/dist/wake-sink.js +8 -32
  141. package/dist/wake-transports.d.ts +4 -0
  142. package/dist/wake-transports.js +11 -0
  143. package/examples/symmetric-domains.mjs +29 -249
  144. package/guides/change-waits.md +13 -53
  145. package/guides/claude-native.md +19 -172
  146. package/guides/codex-delivery.md +55 -78
  147. package/guides/delivery-primitives.md +48 -207
  148. package/guides/operation-cancellation.md +2 -5
  149. package/guides/participant-ownership.md +25 -89
  150. package/guides/pi-native-delivery.md +19 -56
  151. package/guides/pi-tools.md +6 -8
  152. package/guides/row-runtime-delivery-evidence.md +2 -0
  153. package/guides/row-storage-design.md +19 -44
  154. package/guides/square-api.md +113 -538
  155. package/package.json +8 -8
  156. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  157. package/plugins/claude/hooks/hooks.json +1 -1
  158. package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
  159. package/plugins/claude/types.d.ts +1 -1
  160. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  161. package/plugins/codex/hooks/hooks.json +11 -23
  162. package/plugins/opencode/index.ts +91 -23
  163. package/plugins/opencode-shared/delivery.ts +91 -0
  164. package/plugins/opencode-v1/index.ts +36 -0
  165. package/plugins/opencode-v1/runtime.ts +134 -0
  166. package/plugins/opencode-v1/transport.ts +41 -0
  167. package/plugins/paseo/OVERVIEW.md +5 -5
  168. package/plugins/paseo/README.md +7 -6
  169. package/plugins/paseo/client/markdown.tsx +5 -5
  170. package/plugins/paseo/client/square-message.tsx +28 -24
  171. package/plugins/paseo/client/transform.ts +8 -5
  172. package/plugins/paseo/shared/card-presentation.ts +16 -0
  173. package/plugins/paseo/shared/message.ts +6 -11
  174. package/plugins/pi/square-pi.js +167 -451
  175. package/dist/channel.d.ts +0 -74
  176. package/dist/channel.js +0 -92
  177. package/dist/claude-delivery-status.d.ts +0 -13
  178. package/dist/claude-delivery-status.js +0 -46
  179. package/dist/cmd/notify-once.d.ts +0 -2
  180. package/dist/cmd/notify-once.js +0 -29
  181. package/dist/delivery-attempts.d.ts +0 -52
  182. package/dist/delivery-attempts.js +0 -73
  183. package/dist/delivery-effects.d.ts +0 -139
  184. package/dist/delivery-effects.js +0 -163
  185. package/dist/delivery-health.d.ts +0 -51
  186. package/dist/delivery-health.js +0 -173
  187. package/dist/delivery-operations.d.ts +0 -9
  188. package/dist/delivery-operations.js +0 -168
  189. package/dist/delivery-primitives.d.ts +0 -5
  190. package/dist/delivery-primitives.js +0 -3
  191. package/dist/delivery.d.ts +0 -45
  192. package/dist/delivery.js +0 -106
  193. package/dist/plugins/opencode/delivery.d.ts +0 -19
  194. package/dist/plugins/opencode/delivery.js +0 -238
  195. package/dist/presentation-operations.d.ts +0 -18
  196. package/dist/presentation-operations.js +0 -86
  197. package/dist/square-index.d.ts +0 -16
  198. package/dist/square-index.js +0 -106
  199. package/dist/square-legacy-framing.d.ts +0 -11
  200. package/dist/square-legacy-framing.js +0 -64
  201. package/dist/square-migration-host-ledger.d.ts +0 -26
  202. package/dist/square-migration-host-ledger.js +0 -169
  203. package/dist/square-migration.d.ts +0 -30
  204. package/dist/square-migration.js +0 -599
  205. package/guides/migration.md +0 -156
  206. package/plugins/opencode/delivery.ts +0 -211
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.106",
3
+ "version": "0.4.1",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.106",
3
+ "version": "0.4.1",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -1 +1 @@
1
- {"modules":["./register.js"]}
1
+ {"modules":["../mods/square.js"]}
@@ -11,15 +11,12 @@ async function bridge($, value, budget = 2800) {
11
11
  }
12
12
  async function snapshot($, id) { return (await $.state.get({ plugin: 'square', key: 'inbox', id })).value; }
13
13
  async function cancellation($, id) { return (await $.state.get({ plugin: 'square', key: 'cancelAt', id })).value; }
14
- function confirmLater($, c, operation, text) {
14
+ function confirmLater($, c, operation, text, refs) {
15
15
  // State reads are dispatch snapshots. This ONE fresh timer dispatch is not polling.
16
16
  // Reload cancels it; lost evidence stays unknown and is never blindly replayed.
17
17
  try { $.clock.after(0, () => {
18
18
  (async () => {
19
- if (await $.session.id() !== c.sessionId) return;
20
- const old = await snapshot($, c.sessionId);
21
- const cancelled = await cancellation($, c.sessionId);
22
- await bridge($, { ...c, cancelAt: cancelled?.at, cancelledBindings: cancelled?.bindings, bindings: old?.bindings, operation, text });
19
+ await bridge($, { ...c, operation, text, refs });
23
20
  })().catch(() => undefined);
24
21
  }); } catch { /* denied scheduling leaves evidence unknown, never a second next */ }
25
22
  }
@@ -112,10 +109,10 @@ export function register(on) {
112
109
  const old = await snapshot($, c.sessionId).catch(() => undefined);
113
110
  const cancelled = await cancellation($, c.sessionId).catch(() => undefined);
114
111
  const guard = await bridge($, { ...c, cancelAt: cancelled?.at, cancelledBindings: cancelled?.bindings, bindings: old?.bindings, operation: 'guard', text: e.text }).catch(() => undefined);
115
- if (guard?.recognized && !guard.current) return { consumed: 'Square delivery is stale or cancelled' };
112
+ if (!guard || (guard.recognized && !guard.current)) return { consumed: 'Square delivery is stale, cancelled, or unavailable' };
116
113
  // Preserve native policy: hold/refuse reject this gate; never call next twice.
117
114
  const result = await next(e);
118
- if (guard?.current && result.text === e.text) confirmLater($, c, 'admitted', result.text);
115
+ if (guard?.current && result.text === e.text) confirmLater($, c, 'admitted', result.text, guard.refs);
119
116
  return result;
120
117
  });
121
118
  on('session.append', async ($, e, next) => {
@@ -1,4 +1,4 @@
1
- export type SquareInboxBinding = { location: string; participant: string; session: string; generation?: `act/${number}` };
1
+ export type SquareInboxBinding = { location: string; participant: string; session: string; generation?: string };
2
2
  export type SquareInboxCoordinate = { sessionId: string; cwd: string; version: string; endpoint?: string };
3
3
  export type SquareInboxCancellation = { at: number; bindings?: SquareInboxBinding[] };
4
4
  export type SquareInboxState = { coordinate: SquareInboxCoordinate; bindings?: SquareInboxBinding[] };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.106",
3
+ "version": "0.4.1",
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
+ }