@astrosheep/square 0.4.0 → 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 (187) 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-delivery.d.ts +13 -8
  11. package/dist/claude-delivery.js +40 -93
  12. package/dist/claude-mcp-context.js +2 -2
  13. package/dist/claude-mod.js +28 -140
  14. package/dist/claude-transport.d.ts +3 -3
  15. package/dist/claude-transport.js +20 -44
  16. package/dist/cli/context.js +7 -9
  17. package/dist/cli/observation-commands.d.ts +2 -0
  18. package/dist/cli/observation-commands.js +12 -2
  19. package/dist/cli/registry.js +3 -1
  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-links.js +61 -7
  41. package/dist/harness.js +12 -17
  42. package/dist/help.js +2 -0
  43. package/dist/http-delivery.d.ts +5 -9
  44. package/dist/http-delivery.js +20 -77
  45. package/dist/inbox.d.ts +6 -49
  46. package/dist/inbox.js +24 -371
  47. package/dist/index.d.ts +6 -3
  48. package/dist/index.js +4 -1
  49. package/dist/mcp.js +1 -1
  50. package/dist/model.d.ts +7 -47
  51. package/dist/model.js +1 -7
  52. package/dist/notifications.d.ts +24 -20
  53. package/dist/notifications.js +39 -108
  54. package/dist/operation-control.d.ts +4 -0
  55. package/dist/operation-control.js +20 -0
  56. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  57. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  58. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  59. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  60. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  61. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  62. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  63. package/dist/packages/agent-delivery/src/codex.js +41 -4
  64. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  65. package/dist/packages/agent-delivery/src/index.js +1 -1
  66. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  67. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  68. package/dist/paseo-delivery.d.ts +5 -5
  69. package/dist/paseo-delivery.js +19 -97
  70. package/dist/paseo.d.ts +1 -1
  71. package/dist/paseo.js +1 -1
  72. package/dist/plugins/opencode/index.d.ts +5 -3
  73. package/dist/plugins/opencode/index.js +99 -21
  74. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  75. package/dist/plugins/opencode-shared/delivery.js +109 -0
  76. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  77. package/dist/plugins/opencode-v1/index.js +41 -0
  78. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  79. package/dist/plugins/opencode-v1/runtime.js +152 -0
  80. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  81. package/dist/plugins/opencode-v1/transport.js +50 -0
  82. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  83. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  84. package/dist/ports.d.ts +31 -90
  85. package/dist/session-lifecycle.d.ts +8 -7
  86. package/dist/session-lifecycle.js +40 -42
  87. package/dist/sqlite-square.d.ts +14 -23
  88. package/dist/sqlite-square.js +132 -139
  89. package/dist/square-api.js +4 -7
  90. package/dist/square-core.d.ts +0 -1
  91. package/dist/square-domain.d.ts +5 -17
  92. package/dist/square-domain.js +104 -221
  93. package/dist/square-facade.d.ts +18 -15
  94. package/dist/square-post.d.ts +53 -0
  95. package/dist/square-post.js +4 -0
  96. package/dist/square-posting.d.ts +13 -0
  97. package/dist/square-posting.js +174 -0
  98. package/dist/square-producer-sqlite.d.ts +11 -0
  99. package/dist/square-producer-sqlite.js +210 -0
  100. package/dist/square-sqlite.d.ts +15 -25
  101. package/dist/square-sqlite.js +150 -226
  102. package/dist/views.d.ts +2 -2
  103. package/dist/views.js +0 -1
  104. package/dist/wake-sink.d.ts +4 -11
  105. package/dist/wake-sink.js +8 -32
  106. package/dist/wake-transports.d.ts +4 -7
  107. package/dist/wake-transports.js +8 -39
  108. package/examples/symmetric-domains.mjs +29 -249
  109. package/guides/change-waits.md +13 -53
  110. package/guides/claude-native.md +19 -175
  111. package/guides/codex-delivery.md +55 -80
  112. package/guides/delivery-primitives.md +48 -207
  113. package/guides/operation-cancellation.md +2 -5
  114. package/guides/participant-ownership.md +25 -89
  115. package/guides/pi-native-delivery.md +19 -56
  116. package/guides/pi-tools.md +6 -8
  117. package/guides/square-api.md +113 -615
  118. package/package.json +8 -8
  119. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  120. package/plugins/claude/hooks/hooks.json +1 -1
  121. package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
  122. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  123. package/plugins/codex/hooks/hooks.json +11 -23
  124. package/plugins/opencode/index.ts +91 -23
  125. package/plugins/opencode-shared/delivery.ts +91 -0
  126. package/plugins/opencode-v1/index.ts +36 -0
  127. package/plugins/opencode-v1/runtime.ts +134 -0
  128. package/plugins/opencode-v1/transport.ts +41 -0
  129. package/plugins/paseo/OVERVIEW.md +5 -5
  130. package/plugins/paseo/README.md +7 -6
  131. package/plugins/paseo/client/markdown.tsx +5 -5
  132. package/plugins/paseo/client/square-message.tsx +28 -24
  133. package/plugins/paseo/client/transform.ts +8 -5
  134. package/plugins/paseo/shared/card-presentation.ts +16 -0
  135. package/plugins/paseo/shared/message.ts +6 -11
  136. package/plugins/pi/square-pi.js +156 -417
  137. package/dist/channel-artifact.d.ts +0 -10
  138. package/dist/channel-artifact.js +0 -55
  139. package/dist/channel-delivery.d.ts +0 -11
  140. package/dist/channel-delivery.js +0 -51
  141. package/dist/channel-domain.d.ts +0 -3
  142. package/dist/channel-domain.js +0 -83
  143. package/dist/channel-facade.d.ts +0 -76
  144. package/dist/channel-facade.js +0 -1
  145. package/dist/channel-internal.d.ts +0 -126
  146. package/dist/channel-internal.js +0 -440
  147. package/dist/channel-metadata.d.ts +0 -11
  148. package/dist/channel-metadata.js +0 -1
  149. package/dist/channel-ports.d.ts +0 -241
  150. package/dist/channel-ports.js +0 -1
  151. package/dist/channel-presentation.d.ts +0 -17
  152. package/dist/channel-presentation.js +0 -22
  153. package/dist/channel-sqlite-schema.d.ts +0 -13
  154. package/dist/channel-sqlite-schema.js +0 -81
  155. package/dist/channel-transport.d.ts +0 -62
  156. package/dist/channel-transport.js +0 -1
  157. package/dist/channel.d.ts +0 -84
  158. package/dist/channel.js +0 -60
  159. package/dist/claude-delivery-status.d.ts +0 -13
  160. package/dist/claude-delivery-status.js +0 -46
  161. package/dist/cmd/notify-once.d.ts +0 -2
  162. package/dist/cmd/notify-once.js +0 -29
  163. package/dist/delivery-attempts.d.ts +0 -52
  164. package/dist/delivery-attempts.js +0 -73
  165. package/dist/delivery-effects.d.ts +0 -139
  166. package/dist/delivery-effects.js +0 -163
  167. package/dist/delivery-health.d.ts +0 -51
  168. package/dist/delivery-health.js +0 -173
  169. package/dist/delivery-operations.d.ts +0 -22
  170. package/dist/delivery-operations.js +0 -198
  171. package/dist/delivery-primitives.d.ts +0 -5
  172. package/dist/delivery-primitives.js +0 -3
  173. package/dist/delivery.d.ts +0 -56
  174. package/dist/delivery.js +0 -80
  175. package/dist/plugins/opencode/delivery.d.ts +0 -19
  176. package/dist/plugins/opencode/delivery.js +0 -223
  177. package/dist/presentation-operations.d.ts +0 -5
  178. package/dist/presentation-operations.js +0 -86
  179. package/dist/sqlite-channel.d.ts +0 -15
  180. package/dist/sqlite-channel.js +0 -600
  181. package/dist/square-index.d.ts +0 -16
  182. package/dist/square-index.js +0 -106
  183. package/dist/wake-preview.d.ts +0 -7
  184. package/dist/wake-preview.js +0 -15
  185. package/plugins/opencode/delivery.ts +0 -198
  186. /package/dist/{channel-values.d.ts → square-values.d.ts} +0 -0
  187. /package/dist/{channel-values.js → square-values.js} +0 -0
@@ -1,16 +1,16 @@
1
- import type { WakeRequest, WakeOutcome, WakeTransportPort } from './channel-transport.js';
1
+ import type { DeliveryCard, DeliveryTransport, DeliveryTransportContext, TransportOutcome } from './delivery-ledger.js';
2
2
  import { discoverPaseoAgents, waitForPaseoWakeBoundary } from './paseo-state.js';
3
3
  import { sendPaseoWake } from './wake-sink.js';
4
4
  export interface PaseoWakeTransportOptions {
5
- discover?: (timeoutMs: number) => ReturnType<typeof discoverPaseoAgents> | Awaited<ReturnType<typeof discoverPaseoAgents>>;
5
+ discover?: typeof discoverPaseoAgents;
6
6
  waitForBoundary?: typeof waitForPaseoWakeBoundary;
7
7
  sendWake?: typeof sendPaseoWake;
8
8
  }
9
- export declare class PaseoWakeTransport implements WakeTransportPort {
9
+ export declare class PaseoWakeTransport implements DeliveryTransport {
10
10
  private readonly opts;
11
+ readonly kind = "paseo";
11
12
  private discovering?;
12
13
  constructor(opts?: PaseoWakeTransportOptions);
13
- /** Coalesce overlapping inventory reads only; a later send must rediscover current status. */
14
14
  private discover;
15
- attempt(request: WakeRequest, timeoutMs: number, beforeSend: () => Promise<boolean>): Promise<WakeOutcome>;
15
+ send(card: DeliveryCard, address: Readonly<Record<string, string>>, context: DeliveryTransportContext): Promise<TransportOutcome>;
16
16
  }
@@ -1,117 +1,39 @@
1
+ import { renderDeliveryReminder } from './delivery-frame.js';
1
2
  import { DeliveryError } from './packages/agent-delivery/src/index.js';
2
- import { resolvePaseoDaemon } from './packages/agent-delivery/src/paseo-native.js';
3
3
  import { discoverPaseoAgents, waitForPaseoWakeBoundary } from './paseo-state.js';
4
4
  import { sendPaseoWake } from './wake-sink.js';
5
- /** Every send failure is a DeliveryError; only its code and certainty decide retry. */
6
- function diagnostic(phase, address, code) {
7
- return {
8
- phase,
9
- code,
10
- // Only discovery still runs a Paseo CLI; a wake is a capability call on the shared entry.
11
- ...(phase === 'discovery' ? { command: 'paseo ls --global --json' } : {}),
12
- endpoint: resolvePaseoDaemon(undefined)?.url ?? 'unresolved',
13
- paseoAgentIds: [address.agentId].filter(Boolean),
14
- passwordPresent: Boolean(process.env.PASEO_PASSWORD),
15
- };
16
- }
17
5
  export class PaseoWakeTransport {
18
6
  opts;
7
+ kind = 'paseo';
19
8
  discovering;
20
9
  constructor(opts = {}) {
21
10
  this.opts = opts;
22
11
  }
23
- /** Coalesce overlapping inventory reads only; a later send must rediscover current status. */
24
12
  discover(timeoutMs) {
25
13
  return this.discovering ??= Promise.resolve().then(() => (this.opts.discover ?? discoverPaseoAgents)(timeoutMs))
26
14
  .finally(() => { this.discovering = undefined; });
27
15
  }
28
- async attempt(request, timeoutMs, beforeSend) {
29
- const address = request.route.address;
30
- const deadline = Date.now() + timeoutMs;
31
- const remainingMs = () => Math.max(0, deadline - Date.now());
32
- const budgetUnavailable = () => ({
33
- outcome: 'failed',
34
- signature: 'dispatch_budget_exhausted',
35
- message: 'The wake dispatch budget elapsed before Paseo accepted the wake.',
36
- });
16
+ async send(card, address, context) {
37
17
  const agentId = address.agentId?.trim();
38
- if (!agentId) {
39
- return {
40
- outcome: 'failed',
41
- signature: 'invalid_address',
42
- message: 'Paseo route has no agent id.',
43
- diagnostic: diagnostic('selection', address, 'invalid_address'),
44
- };
45
- }
46
- const payload = request.presentation?.payload;
47
- if (payload === undefined)
48
- return { outcome: 'not-capable', diagnostic: 'No pending Square activity to preview.' };
49
- let remaining = remainingMs();
50
- if (remaining === 0)
51
- return budgetUnavailable();
52
- const discovery = await this.discover(remaining);
53
- if (discovery.error && discovery.agents.length === 0) {
54
- return {
55
- outcome: 'failed',
56
- // A credential or authorization refusal is proven; anything else may be transient.
57
- signature: /password|auth|unauthori[sz]ed/i.test(discovery.error) ? 'discovery_rejected'
58
- : /DAEMON_NOT_RUNNING|ECONNREFUSED|ENOENT|not found.*executable|ETIMEDOUT|timed out|timeout/i.test(discovery.error)
59
- ? 'discovery_transient' : 'discovery_rejected',
60
- message: `Paseo unavailable: ${discovery.error}`,
61
- diagnostic: diagnostic('discovery', address, 'unavailable'),
62
- };
63
- }
64
- const agent = discovery.agents.find((candidate) => candidate.id === agentId);
65
- if (agent === undefined || agent.status !== 'idle') {
66
- return {
67
- outcome: 'failed',
68
- signature: agent === undefined ? 'address_not_found' : 'agent_not_idle',
69
- message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle.',
70
- diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : 'not_idle'),
71
- };
72
- }
73
- remaining = remainingMs();
74
- if (remaining === 0)
75
- return budgetUnavailable();
76
- if (!(await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent, remaining))) {
77
- return {
78
- outcome: 'failed',
79
- signature: 'boundary_unavailable',
80
- message: 'Paseo did not reach the current tool boundary before the wake timeout.',
81
- diagnostic: diagnostic('boundary', address, 'unavailable'),
82
- };
83
- }
84
- if (!(await beforeSend()))
85
- return { outcome: 'not-capable' };
86
- remaining = remainingMs();
87
- if (remaining === 0)
88
- return budgetUnavailable();
18
+ if (!agentId)
19
+ return 'not-capable';
20
+ const remaining = () => Math.max(0, context.deadline - Date.now());
21
+ if (context.signal.aborted || !remaining())
22
+ return 'not-capable';
89
23
  try {
90
- await (this.opts.sendWake ?? sendPaseoWake)({
91
- agentId,
92
- prompt: payload,
93
- location: request.location,
94
- participant: request.participant,
95
- message: request.message,
96
- ...(request.attemptN === undefined ? {} : { attemptN: request.attemptN }),
97
- }, { timeoutMs: remaining });
98
- return { outcome: 'accepted' };
24
+ const discovery = await this.discover(remaining());
25
+ const agent = discovery.agents.find(candidate => candidate.id === agentId);
26
+ if (!agent || agent.status !== 'idle' || !remaining())
27
+ return 'failed';
28
+ if (!await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent, remaining()))
29
+ return 'failed';
30
+ if (!await context.prepare({ messageId: context.token }) || context.signal.aborted || !remaining())
31
+ return 'not-capable';
32
+ await (this.opts.sendWake ?? sendPaseoWake)({ agentId, prompt: renderDeliveryReminder([card.text], { tagged: true }), messageId: context.token }, { timeoutMs: remaining(), beforeSend: context.beforeSend, signal: context.signal });
33
+ return 'accepted';
99
34
  }
100
35
  catch (error) {
101
- const message = error instanceof Error ? error.message : String(error);
102
- const kind = !(error instanceof DeliveryError) || error.maybeDelivered ? 'unknown'
103
- : error.code === 'unavailable' ? 'transient'
104
- : ['authentication_failed', 'session_not_found', 'rejected', 'invalid_arguments'].includes(error.code) ? 'rejected'
105
- : 'unknown';
106
- const details = { ...diagnostic('send', address, 'failed'), outcome: kind };
107
- if (kind === 'unknown')
108
- return { outcome: 'unknown', signature: 'send_unknown', message, diagnostic: details };
109
- return {
110
- outcome: 'failed',
111
- signature: kind === 'transient' ? 'send_pre_accept_transient' : 'send_pre_accept_rejected',
112
- message,
113
- diagnostic: details,
114
- };
36
+ return error instanceof DeliveryError && !error.maybeDelivered ? 'failed' : 'unknown';
115
37
  }
116
38
  }
117
39
  }
package/dist/paseo.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Optional Paseo integration. Importing this entry requires the Paseo peer dependencies. */
2
2
  export { PaseoWakeTransport } from './paseo-delivery.js';
3
- export { paseoWakeMessageId, sendPaseoWake } from './wake-sink.js';
3
+ export { sendPaseoWake } from './wake-sink.js';
4
4
  export type { PaseoWakeRequest } from './wake-sink.js';
5
5
  export { discoverPaseoAgents, waitForPaseoWakeBoundary } from './paseo-state.js';
6
6
  export type { PaseoAgent } from './paseo-state.js';
package/dist/paseo.js CHANGED
@@ -1,4 +1,4 @@
1
1
  /** Optional Paseo integration. Importing this entry requires the Paseo peer dependencies. */
2
2
  export { PaseoWakeTransport } from './paseo-delivery.js';
3
- export { paseoWakeMessageId, sendPaseoWake } from './wake-sink.js';
3
+ export { sendPaseoWake } from './wake-sink.js';
4
4
  export { discoverPaseoAgents, waitForPaseoWakeBoundary } from './paseo-state.js';
@@ -1,5 +1,7 @@
1
- import { type OpenCodeSessionCapability } from '../../packages/agent-delivery/src/opencode-native.js';
2
- import { type OpenCodeContextMessage } from './delivery.js';
1
+ import type { DeliveryTransport } from '../../delivery-ledger.js';
2
+ import { type OpenCodeNativeSession, type OpenCodeSessionCapability } from '../../packages/agent-delivery/src/opencode-native.js';
3
+ import { type OpenCodeContextMessage } from '../opencode-shared/delivery.js';
4
+ export declare function createOpenCodeWakeTransport(target: OpenCodeNativeSession, lifetime: AbortSignal): DeliveryTransport;
3
5
  interface OpenCodePluginContext {
4
6
  readonly app: {
5
7
  readonly version: string;
@@ -32,7 +34,7 @@ interface OpenCodePluginContext {
32
34
  }>;
33
35
  };
34
36
  }
35
- /** Actual OpenCode 2.0.20 server plugin. Harness identity belongs to ctx, never process.env. */
37
+ /** OpenCode v2 server plugin. Harness identity belongs to ctx, never process.env. */
36
38
  declare const _default: {
37
39
  id: string;
38
40
  setup(ctx: OpenCodePluginContext): Promise<() => Promise<void>>;
@@ -1,17 +1,58 @@
1
- import { observeSquareIndex } from '../../square-index.js';
1
+ import { createHash } from 'node:crypto';
2
+ import { renderDeliveryReminder } from '../../delivery-frame.js';
2
3
  import { canonicalPath } from '../../canonical-path.js';
3
- import { connectNative } from '../../packages/agent-delivery/src/opencode-native.js';
4
- import { cancelOpenCodeDeliveries, endOpenCodeSession, indexedOpenCodeSessions, observeOpenCodeContext, publishOpenCodeAddress, receiveOpenCodePending } from './delivery.js';
5
- /** Actual OpenCode 2.0.20 server plugin. Harness identity belongs to ctx, never process.env. */
4
+ import { connectNative, createNativeInputId, sendNativeText } from '../../packages/agent-delivery/src/opencode-native.js';
5
+ import { cancelOpenCodeDeliveries, currentOpenCodeGeneration, detachOpenCodeSession, endOpenCodeSession, listOpenCodeSessions, observeOpenCodeContext, observeOpenCodeSessions, publishOpenCodeAddress, receiveOpenCodePending } from '../opencode-shared/delivery.js';
6
+ const routeKind = 'opencode-server';
7
+ export function createOpenCodeWakeTransport(target, lifetime) {
8
+ return {
9
+ kind: routeKind,
10
+ async send(card, address, context) {
11
+ const signal = AbortSignal.any([lifetime, context.signal]);
12
+ if (address.sessionId !== target.sessionId || signal.aborted)
13
+ return 'not-capable';
14
+ const messageId = createNativeInputId();
15
+ const text = renderDeliveryReminder([card.text], { tagged: true });
16
+ const payloadHash = createHash('sha256').update(text).digest('hex');
17
+ if (!await context.prepare({ token: context.token, messageId, payloadHash }) || signal.aborted
18
+ || !await context.beforeSend() || signal.aborted || Date.now() >= context.deadline)
19
+ return 'not-capable';
20
+ const result = await sendNativeText(target, text, {
21
+ inputId: messageId, delivery: 'steer', timeoutMs: Math.max(1, context.deadline - Date.now()), signal,
22
+ });
23
+ if (result.state === 'accepted')
24
+ return 'accepted';
25
+ if (result.state === 'unavailable')
26
+ return 'failed';
27
+ return 'unknown';
28
+ },
29
+ };
30
+ }
31
+ /** OpenCode v2 server plugin. Harness identity belongs to ctx, never process.env. */
6
32
  export default {
7
33
  id: 'square',
8
34
  async setup(ctx) {
9
- if (ctx.app.version !== '2.0.20')
35
+ // OpenCode publishes semantic versions; unknown version forms are not evidence of a v2 host.
36
+ const version = ctx?.app?.version;
37
+ const reason = typeof version !== 'string' || !/^2\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)
38
+ ? 'expected an OpenCode 2.x semantic version'
39
+ : typeof ctx.location?.directory !== 'string' || !ctx.location.directory ||
40
+ typeof ctx.location.project?.id !== 'string' || !ctx.location.project.id ||
41
+ (ctx.location.workspaceID !== undefined && typeof ctx.location.workspaceID !== 'string')
42
+ ? 'missing location directory or project identity'
43
+ : typeof ctx.session?.hook !== 'function' || typeof ctx.session.get !== 'function' || typeof ctx.session.prompt !== 'function'
44
+ ? 'missing session hook, get, or prompt capability'
45
+ : typeof ctx.event?.subscribe !== 'function' ? 'missing event subscription capability' : undefined;
46
+ if (reason) {
47
+ console.warn(`Square OpenCode plugin inactive: ${reason}.`);
10
48
  return async () => { };
49
+ }
11
50
  const env = { ...process.env };
12
- const index = await observeSquareIndex(env);
51
+ const discovery = await observeOpenCodeSessions(env);
13
52
  const lifetime = new AbortController();
14
53
  const receivers = new Map(); // Operational handles only; presence owns membership.
54
+ const interrupting = new Set();
55
+ const cancellationFailed = new Set();
15
56
  const directory = await canonicalPath(ctx.location.directory);
16
57
  let coordinating = Promise.resolve();
17
58
  const coordinate = (operation) => {
@@ -39,26 +80,37 @@ export default {
39
80
  return target;
40
81
  };
41
82
  const reconcile = async (activeSession) => {
42
- const candidates = new Set(await indexedOpenCodeSessions(env));
83
+ const candidates = new Set(await listOpenCodeSessions(env));
43
84
  if (activeSession)
44
85
  candidates.add(activeSession);
45
86
  for (const sessionId of candidates) {
46
87
  if (lifetime.signal.aborted)
47
88
  return;
89
+ if (interrupting.has(sessionId) || cancellationFailed.has(sessionId))
90
+ continue;
48
91
  const target = await validate(sessionId);
49
92
  if (!target) {
93
+ const receiver = receivers.get(sessionId);
50
94
  stop(sessionId);
95
+ if (receiver)
96
+ await detachOpenCodeSession(sessionId, receiver.generation, env);
51
97
  continue;
52
98
  }
53
- if (!await publishOpenCodeAddress(sessionId, env, lifetime.signal)) {
99
+ const existing = receivers.get(sessionId);
100
+ if (existing?.settled === false || interrupting.has(sessionId))
101
+ continue;
102
+ const generation = existing?.generation ?? await publishOpenCodeAddress(sessionId, env, lifetime.signal);
103
+ if (!generation) {
54
104
  stop(sessionId);
55
105
  continue;
56
106
  }
57
- if (receivers.get(sessionId)?.settled === false)
107
+ if (lifetime.signal.aborted || interrupting.has(sessionId)) {
108
+ await detachOpenCodeSession(sessionId, generation, env);
58
109
  continue;
110
+ }
59
111
  const abort = new AbortController();
60
112
  const signal = AbortSignal.any([lifetime.signal, abort.signal]);
61
- const receiver = { abort, settled: false, running: receiveOpenCodePending(target, env, signal).catch(() => { }).finally(() => { receiver.settled = true; }) };
113
+ const receiver = { abort, generation, settled: false, running: receiveOpenCodePending(sessionId, generation, createOpenCodeWakeTransport(target, signal), env, signal).catch(() => { }).finally(() => { receiver.settled = true; }) };
62
114
  receivers.set(sessionId, receiver);
63
115
  }
64
116
  for (const sessionId of receivers.keys())
@@ -66,16 +118,31 @@ export default {
66
118
  stop(sessionId);
67
119
  };
68
120
  const cancel = async (sessionId) => {
69
- if (!receivers.has(sessionId))
121
+ const receiver = receivers.get(sessionId);
122
+ if (!receiver)
70
123
  return;
71
- // Stop synchronously; persist the cutoff before allowing reconciliation to start fresh work.
72
124
  stop(sessionId);
73
- await cancelOpenCodeDeliveries(sessionId, env, lifetime.signal);
74
- await reconcile();
125
+ await receiver.running;
126
+ try {
127
+ if (!await cancelOpenCodeDeliveries(sessionId, receiver.generation, env))
128
+ throw new Error('OpenCode cancellation fence lost');
129
+ }
130
+ catch (error) {
131
+ cancellationFailed.add(sessionId);
132
+ await detachOpenCodeSession(sessionId, receiver.generation, env).catch(() => { });
133
+ throw error;
134
+ }
135
+ finally {
136
+ interrupting.delete(sessionId);
137
+ }
138
+ await reconcile(sessionId);
75
139
  };
76
140
  const end = async (sessionId) => {
141
+ const receiver = receivers.get(sessionId);
77
142
  stop(sessionId);
78
- await endOpenCodeSession(sessionId, env, lifetime.signal);
143
+ const generation = receiver?.generation ?? await currentOpenCodeGeneration(sessionId, env);
144
+ if (generation)
145
+ await endOpenCodeSession(sessionId, generation, env);
79
146
  };
80
147
  await ctx.session.hook('prompt', (event) => coordinate(() => reconcile(event.sessionID)));
81
148
  await ctx.session.hook('context', async (event) => {
@@ -83,13 +150,13 @@ export default {
83
150
  const receiver = receivers.get(event.sessionID);
84
151
  if (!receiver || lifetime.signal.aborted)
85
152
  return;
86
- await observeOpenCodeContext(event.sessionID, event.messages, env, AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => { });
153
+ await observeOpenCodeContext(event.sessionID, receiver.generation, event.messages, env, AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => { });
87
154
  });
88
155
  const presenceLoop = (async () => {
89
156
  while (!lifetime.signal.aborted) {
90
- const baseline = await index.read(lifetime.signal);
157
+ const baseline = discovery.observer.generation;
91
158
  await coordinate(() => reconcile());
92
- await index.changed(baseline, Infinity, lifetime.signal);
159
+ await discovery.observer.hinted(baseline, 2000, lifetime.signal);
93
160
  }
94
161
  })().catch(() => { });
95
162
  const eventLoop = (async () => {
@@ -100,8 +167,17 @@ export default {
100
167
  if (event.type === 'session.deleted')
101
168
  await coordinate(() => end(sessionId));
102
169
  else if (event.type === 'session.moved')
103
- await coordinate(async () => { stop(sessionId); await reconcile(); });
170
+ await coordinate(async () => {
171
+ const receiver = receivers.get(sessionId);
172
+ stop(sessionId);
173
+ if (receiver)
174
+ await detachOpenCodeSession(sessionId, receiver.generation, env);
175
+ await reconcile();
176
+ });
104
177
  else if (event.type === 'session.execution.interrupted' && event.data.reason === 'user') {
178
+ if (interrupting.has(sessionId) || !receivers.has(sessionId))
179
+ continue;
180
+ interrupting.add(sessionId);
105
181
  receivers.get(sessionId)?.abort.abort();
106
182
  await coordinate(() => cancel(sessionId));
107
183
  }
@@ -110,8 +186,8 @@ export default {
110
186
  // Do not await any native prompt in setup: plugin activation itself is lazy.
111
187
  return async () => {
112
188
  lifetime.abort();
113
- index.close();
114
- const tasks = [presenceLoop, eventLoop, coordinating, ...[...receivers.keys()].map(stop)];
189
+ const captured = [...receivers.entries()];
190
+ const tasks = [presenceLoop, eventLoop, coordinating, ...captured.map(([id]) => stop(id))];
115
191
  let timer;
116
192
  try {
117
193
  await Promise.race([Promise.allSettled(tasks), new Promise((resolve) => { timer = setTimeout(resolve, 3000); })]);
@@ -120,6 +196,8 @@ export default {
120
196
  if (timer !== undefined)
121
197
  clearTimeout(timer);
122
198
  }
199
+ await Promise.allSettled(captured.map(([id, receiver]) => detachOpenCodeSession(id, receiver.generation, env)));
200
+ await discovery.close();
123
201
  // Instance unload is not session deletion: retain memberships and uncertain correlations.
124
202
  };
125
203
  },
@@ -0,0 +1,23 @@
1
+ import { type DeliveryTransport } from '../../delivery-ledger.js';
2
+ export interface OpenCodeContextMessage {
3
+ readonly id?: string;
4
+ readonly role: string;
5
+ readonly content: readonly {
6
+ readonly type: string;
7
+ readonly text?: string;
8
+ }[];
9
+ }
10
+ export declare function listOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<readonly string[]>;
11
+ /** The file observer is a hint; callers must always reread the ledger after an edge. */
12
+ export declare function observeOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<{
13
+ observer: import("../../file-changes.js").VersionObserver<string>;
14
+ close: () => Promise<void>;
15
+ }>;
16
+ export declare function publishOpenCodeAddress(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<string | undefined>;
17
+ export declare function currentOpenCodeGeneration(sessionId: string, env: NodeJS.ProcessEnv): Promise<string | undefined>;
18
+ export declare function detachOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void>;
19
+ export declare function endOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void>;
20
+ export declare function cancelOpenCodeDeliveries(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<boolean>;
21
+ /** Confirmation is observer-level: another host plugin may subsequently remove the message. */
22
+ export declare function observeOpenCodeContext(sessionId: string, generation: string, messages: readonly OpenCodeContextMessage[], env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void>;
23
+ export declare function receiveOpenCodePending(sessionId: string, generation: string, transport: DeliveryTransport, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,109 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { DeliveryLedger } 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
+ const routeKind = 'opencode-server';
7
+ const ownerFor = (sessionId) => harnessOwner({ channel: 'opencode', session: sessionId });
8
+ const open = (env) => DeliveryLedger.open(env.SQUARE_DELIVERY ? { location: env.SQUARE_DELIVERY } : {});
9
+ const payloadHash = (payload) => createHash('sha256').update(payload).digest('hex');
10
+ export async function listOpenCodeSessions(env) {
11
+ const ledger = await open(env);
12
+ try {
13
+ return (await ledger.sessions()).flatMap(session => {
14
+ const coordinate = decodeHarnessOwner(session.owner);
15
+ return session.state !== 'deleted' && coordinate?.channel === 'opencode' ? [coordinate.session] : [];
16
+ });
17
+ }
18
+ finally {
19
+ await ledger.close();
20
+ }
21
+ }
22
+ /** The file observer is a hint; callers must always reread the ledger after an edge. */
23
+ export async function observeOpenCodeSessions(env) {
24
+ const ledger = await open(env);
25
+ const observer = await ledger.observe(ownerFor('discovery'));
26
+ return { observer, close: async () => { observer.close(); await ledger.close(); } };
27
+ }
28
+ export async function publishOpenCodeAddress(sessionId, env, signal) {
29
+ const session = await attachDeliverySessionRoutes(ownerFor(sessionId), env, [{ kind: routeKind, address: { sessionId } }], signal);
30
+ return session ? session.generation : undefined;
31
+ }
32
+ export async function currentOpenCodeGeneration(sessionId, env) {
33
+ const ledger = await open(env);
34
+ try {
35
+ return (await ledger.session(ownerFor(sessionId)))?.generation;
36
+ }
37
+ finally {
38
+ await ledger.close();
39
+ }
40
+ }
41
+ export async function detachOpenCodeSession(sessionId, generation, env) {
42
+ await detachDeliverySession(ownerFor(sessionId), generation, env);
43
+ }
44
+ export async function endOpenCodeSession(sessionId, generation, env) {
45
+ await markDeliverySessionDeleted(ownerFor(sessionId), generation, env);
46
+ }
47
+ export async function cancelOpenCodeDeliveries(sessionId, generation, env) {
48
+ const ledger = await open(env);
49
+ try {
50
+ return await ledger.cancel({ owner: ownerFor(sessionId), generation });
51
+ }
52
+ finally {
53
+ await ledger.close();
54
+ }
55
+ }
56
+ function textMessage(message) {
57
+ if (message.role !== 'user' || message.id === undefined || !message.content.every(part => part.type === 'text' && typeof part.text === 'string'))
58
+ return undefined;
59
+ return message.content.map(part => part.text).join('');
60
+ }
61
+ /** Confirmation is observer-level: another host plugin may subsequently remove the message. */
62
+ export async function observeOpenCodeContext(sessionId, generation, messages, env, signal) {
63
+ const observed = new Map(messages.flatMap(message => {
64
+ const text = textMessage(message);
65
+ return text === undefined ? [] : [[message.id, payloadHash(text)]];
66
+ }));
67
+ if (!observed.size || signal.aborted)
68
+ return;
69
+ const ledger = await open(env);
70
+ try {
71
+ for (const attempt of await ledger.handoffs(ownerFor(sessionId), { generation })) {
72
+ if (signal.aborted)
73
+ return;
74
+ const { correlation } = attempt;
75
+ if (attempt.routeKind === routeKind && correlation.token === attempt.token && correlation.messageId !== undefined
76
+ && correlation.payloadHash !== undefined && observed.get(correlation.messageId) === correlation.payloadHash)
77
+ await ledger.observePresentation({ token: attempt.token, owner: ownerFor(sessionId), generation, routeKind, payloadHash: correlation.payloadHash }, { requireCurrent: true });
78
+ }
79
+ }
80
+ finally {
81
+ await ledger.close();
82
+ }
83
+ }
84
+ export async function receiveOpenCodePending(sessionId, generation, transport, env, signal) {
85
+ const owner = ownerFor(sessionId);
86
+ const ledger = await open(env);
87
+ const lifetime = retainDeliveryLedger(ledger);
88
+ const observer = await ledger.observe(owner);
89
+ try {
90
+ while (!signal.aborted) {
91
+ const baseline = observer.generation;
92
+ for (const card of await ledger.pending(owner, { generation })) {
93
+ if (signal.aborted)
94
+ return;
95
+ lifetime.track(await ledger.dispatchHandoff(owner, card.key, transport, { generation, timeoutMs: 5000, signal }));
96
+ }
97
+ if (!signal.aborted)
98
+ await observer.hinted(baseline, 2000, signal);
99
+ }
100
+ }
101
+ catch (error) {
102
+ if (!signal.aborted)
103
+ throw error;
104
+ }
105
+ finally {
106
+ observer.close();
107
+ await lifetime.close();
108
+ }
109
+ }
@@ -0,0 +1,28 @@
1
+ interface LegacyContext {
2
+ readonly serverUrl: string | URL;
3
+ readonly directory: string;
4
+ readonly project: {
5
+ readonly id: string;
6
+ };
7
+ readonly client: {
8
+ readonly session: {
9
+ get(input: unknown): Promise<{
10
+ data?: unknown;
11
+ error?: unknown;
12
+ }>;
13
+ promptAsync(input: unknown): Promise<{
14
+ error?: unknown;
15
+ response?: Response;
16
+ }>;
17
+ };
18
+ };
19
+ }
20
+ /** Legacy loader: no storage imports until the actual host and runtime qualify. */
21
+ export default function squareOpenCodeV1(ctx: LegacyContext): Promise<{
22
+ 'shell.env': (input: {
23
+ sessionID?: string;
24
+ }, output: {
25
+ env: Record<string, string>;
26
+ }) => Promise<void>;
27
+ }>;
28
+ export {};
@@ -0,0 +1,41 @@
1
+ /** Legacy loader: no storage imports until the actual host and runtime qualify. */
2
+ export default async function squareOpenCodeV1(ctx) {
3
+ const shell = async (input, output) => {
4
+ if (typeof input.sessionID === 'string' && input.sessionID)
5
+ output.env.OPENCODE_SESSION_ID = input.sessionID;
6
+ };
7
+ const reduced = { 'shell.env': shell };
8
+ try {
9
+ let response;
10
+ try {
11
+ response = await fetch(new URL('/global/health', ctx.serverUrl), { signal: AbortSignal.timeout(1500) });
12
+ }
13
+ catch {
14
+ throw new Error('health endpoint unreachable (embedded TUI may not expose it)');
15
+ }
16
+ if (!response.ok)
17
+ throw new Error(`health endpoint returned HTTP ${response.status} (embedded TUI may not expose it)`);
18
+ const health = await response.json();
19
+ const match = typeof health.version === 'string' && /^(\d+)\.(\d+)\.(\d+)$/.exec(health.version);
20
+ if (!match || Number(match[1]) !== 1 || Number(match[2]) < 15 || (Number(match[2]) === 15 && Number(match[3]) < 11))
21
+ throw new Error('native version below resident floor or unknown');
22
+ if (typeof ctx.client?.session?.get !== 'function' || typeof ctx.client.session.promptAsync !== 'function')
23
+ throw new Error('native session API unavailable');
24
+ const { DatabaseSync } = await import('node:sqlite');
25
+ const db = new DatabaseSync(':memory:');
26
+ try {
27
+ db.exec('CREATE TABLE capability (value TEXT)');
28
+ db.prepare('INSERT INTO capability VALUES (?)').run('ok');
29
+ db.prepare('SELECT value FROM capability').get();
30
+ }
31
+ finally {
32
+ db.close();
33
+ }
34
+ const { startReceiver } = await import('./runtime.js');
35
+ return { ...reduced, ...await startReceiver(ctx) };
36
+ }
37
+ catch (error) {
38
+ console.warn(`[square] OpenCode 1.x reduced mode (no proactive delivery): ${error instanceof Error ? error.message : String(error)}`);
39
+ return reduced;
40
+ }
41
+ }
@@ -0,0 +1,56 @@
1
+ interface Context {
2
+ readonly directory: string;
3
+ readonly project: {
4
+ readonly id: string;
5
+ };
6
+ readonly client: {
7
+ readonly session: {
8
+ get(input: unknown): Promise<{
9
+ data?: unknown;
10
+ error?: unknown;
11
+ }>;
12
+ promptAsync(input: unknown): Promise<{
13
+ error?: unknown;
14
+ response?: Response;
15
+ }>;
16
+ };
17
+ };
18
+ }
19
+ interface NativeMessage {
20
+ info?: {
21
+ id?: string;
22
+ sessionID?: string;
23
+ role?: string;
24
+ };
25
+ parts?: {
26
+ type?: string;
27
+ text?: string;
28
+ sessionID?: string;
29
+ messageID?: string;
30
+ synthetic?: boolean;
31
+ ignored?: boolean;
32
+ }[];
33
+ }
34
+ export declare function startReceiver(ctx: Context): Promise<{
35
+ 'experimental.chat.messages.transform': (_input: unknown, output: {
36
+ messages: NativeMessage[];
37
+ }) => Promise<void>;
38
+ event: ({ event }: {
39
+ event: {
40
+ type: string;
41
+ properties?: {
42
+ sessionID?: string;
43
+ error?: {
44
+ name?: string;
45
+ };
46
+ info?: {
47
+ id?: string;
48
+ projectID?: string;
49
+ directory?: string;
50
+ };
51
+ };
52
+ };
53
+ }) => Promise<void>;
54
+ dispose: () => Promise<void>;
55
+ }>;
56
+ export {};