@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
@@ -0,0 +1,196 @@
1
+ import { type Owner } from './neutral.js';
2
+ import { type VersionObserver } from './file-changes.js';
3
+ export interface DeliveryRoute {
4
+ readonly kind: string;
5
+ readonly address: Readonly<Record<string, string>>;
6
+ }
7
+ export interface DeliverySession {
8
+ readonly owner: Owner;
9
+ readonly generation: string;
10
+ readonly state: 'attached' | 'detached' | 'deleted';
11
+ readonly routes: readonly DeliveryRoute[];
12
+ readonly at: number;
13
+ }
14
+ export interface DeliveryCard {
15
+ readonly seq: number;
16
+ readonly owner: Owner;
17
+ readonly key: string;
18
+ readonly text: string;
19
+ readonly details?: Readonly<Record<string, unknown>>;
20
+ readonly postedAt: number;
21
+ }
22
+ export interface DeliveryCorrelation {
23
+ readonly token?: string;
24
+ readonly messageId?: string;
25
+ readonly payloadHash?: string;
26
+ }
27
+ export interface DeliveryHandoffRef {
28
+ readonly token: string;
29
+ readonly owner: Owner;
30
+ readonly generation: string;
31
+ readonly payloadHash: string;
32
+ readonly routeKind: string;
33
+ }
34
+ export type DeliveryOutcome = 'dispatching' | 'accepted' | 'presented' | 'failed' | 'unknown' | 'released';
35
+ export type PresentationOutcome = 'presented' | 'failed' | 'unknown' | 'released';
36
+ export interface DeliveryAttempt {
37
+ readonly token: string;
38
+ readonly seq: number;
39
+ readonly generation: string;
40
+ readonly track: 'handoff' | 'presentation';
41
+ readonly routeKind: string | null;
42
+ readonly outcome: DeliveryOutcome;
43
+ readonly correlation: DeliveryCorrelation;
44
+ readonly startedAt: number;
45
+ readonly expiresAt: number;
46
+ readonly settledAt?: number;
47
+ }
48
+ export interface DeliveryReceipt {
49
+ readonly owner: Owner;
50
+ readonly key: string;
51
+ readonly at: number;
52
+ }
53
+ export type PostResult = {
54
+ readonly status: 'posted' | 'duplicate' | 'conflict' | 'unknown-session' | 'deleted-session' | 'suppressed';
55
+ };
56
+ export type SuppressionResult = {
57
+ readonly id: string;
58
+ readonly status: 'suppressed' | 'in-custody';
59
+ };
60
+ export interface DeliveryHealth {
61
+ readonly unknownAttempts: readonly DeliveryAttempt[];
62
+ readonly repeatedFailures: readonly {
63
+ readonly owner: Owner;
64
+ readonly key: string;
65
+ readonly count: number;
66
+ }[];
67
+ readonly sessionsWithoutRoute: readonly Owner[];
68
+ }
69
+ export interface PresentationClaim {
70
+ readonly cards: readonly DeliveryCard[];
71
+ readonly attempts: readonly DeliveryAttempt[];
72
+ /** Recheck immediately before handing text to the external receiver. */
73
+ current(signal?: AbortSignal): Promise<boolean>;
74
+ /** Captured custody survives attachment rotation; errors propagate without a false receipt. */
75
+ settle(outcome: PresentationOutcome): Promise<boolean>;
76
+ }
77
+ export interface DeliveryTransportContext {
78
+ readonly token: string;
79
+ readonly deadline: number;
80
+ readonly signal: AbortSignal;
81
+ prepare(correlation: DeliveryCorrelation): Promise<boolean>;
82
+ /** Adapters with awaits before I/O recheck here, then check signal/deadline synchronously. */
83
+ beforeSend(): Promise<boolean>;
84
+ }
85
+ export type TransportOutcome = 'accepted' | 'presented' | 'failed' | 'unknown' | 'not-capable';
86
+ export interface DeliveryTransport {
87
+ readonly kind: string;
88
+ send(card: DeliveryCard, address: Readonly<Record<string, string>>, context: DeliveryTransportContext): Promise<TransportOutcome>;
89
+ }
90
+ export interface LateHandoffSettlement {
91
+ readonly outcome: TransportOutcome | 'released';
92
+ readonly persistence: {
93
+ readonly status: 'recorded' | 'not-recorded';
94
+ } | {
95
+ readonly status: 'error';
96
+ readonly error: unknown;
97
+ };
98
+ }
99
+ export interface HandoffResult {
100
+ readonly outcome: TransportOutcome | 'released' | 'not-invoked';
101
+ readonly token?: string;
102
+ readonly persistence?: 'recorded' | 'not-recorded';
103
+ /** Interrupted effects may complete later; this handle never rejects and may stay pending. */
104
+ readonly lateSettlement?: Promise<LateHandoffSettlement>;
105
+ }
106
+ /** Shared by host and producer callers; an explicit delivery override always wins. */
107
+ export declare function deliveryLedgerLocation(env?: NodeJS.ProcessEnv, homeDir?: string): string;
108
+ /** One user-owned ledger; producers supply finished text and callers choose routes. */
109
+ export declare class DeliveryLedger {
110
+ private readonly store;
111
+ private readonly leaseMs;
112
+ private constructor();
113
+ static open(options?: {
114
+ readonly location?: string;
115
+ readonly clock?: () => number;
116
+ readonly leaseMs?: number;
117
+ }): Promise<DeliveryLedger>;
118
+ close(): Promise<void>;
119
+ attach(input: {
120
+ readonly owner: Owner;
121
+ readonly expectedGeneration: string | null;
122
+ readonly routes: readonly DeliveryRoute[];
123
+ }): Promise<DeliverySession | false>;
124
+ updateRoutes(input: {
125
+ readonly owner: Owner;
126
+ readonly generation: string;
127
+ readonly routes: readonly DeliveryRoute[];
128
+ }): Promise<boolean>;
129
+ detach(input: {
130
+ readonly owner: Owner;
131
+ readonly generation: string;
132
+ }): Promise<boolean>;
133
+ markDeleted(input: {
134
+ readonly owner: Owner;
135
+ readonly generation: string;
136
+ }): Promise<boolean>;
137
+ private lifecycle;
138
+ session(owner: Owner): Promise<DeliverySession | undefined>;
139
+ sessions(): Promise<readonly DeliverySession[]>;
140
+ pending(owner: Owner, options: {
141
+ readonly generation: string;
142
+ }): Promise<readonly DeliveryCard[]>;
143
+ handoffs(owner: Owner, options: {
144
+ readonly generation: string;
145
+ }): Promise<readonly DeliveryAttempt[]>;
146
+ post(input: {
147
+ readonly owner: Owner;
148
+ readonly key: string;
149
+ readonly text: string;
150
+ readonly details?: Readonly<Record<string, unknown>>;
151
+ }): Promise<PostResult>;
152
+ suppress(input: {
153
+ readonly owner: Owner;
154
+ readonly key: string;
155
+ readonly scope: 'exact' | 'prefix';
156
+ readonly expiresAt?: number;
157
+ }): Promise<SuppressionResult>;
158
+ renewSuppression(input: {
159
+ readonly owner: Owner;
160
+ readonly id: string;
161
+ readonly expiresAt: number;
162
+ }): Promise<boolean>;
163
+ releaseSuppression(input: {
164
+ readonly owner: Owner;
165
+ readonly id: string;
166
+ }): Promise<boolean>;
167
+ cancel(input: {
168
+ readonly owner: Owner;
169
+ readonly generation: string;
170
+ }): Promise<boolean>;
171
+ receipts(owner: Owner, options?: {
172
+ readonly keys?: readonly string[];
173
+ readonly since?: number;
174
+ }): Promise<readonly DeliveryReceipt[]>;
175
+ claimPresentation(owner: Owner, options: {
176
+ readonly generation: string;
177
+ readonly budgetChars: number;
178
+ readonly signal?: AbortSignal;
179
+ }): Promise<PresentationClaim | undefined>;
180
+ /** Resolve the original prepared handoff, including its stored route kind. */
181
+ handoff(token: string): Promise<DeliveryHandoffRef | undefined>;
182
+ guardHandoff(ref: DeliveryHandoffRef): Promise<boolean>;
183
+ acceptHandoff(ref: DeliveryHandoffRef): Promise<boolean>;
184
+ /** Confirms the exact existing external effect; never sends or returns text. */
185
+ observePresentation(handoff: string | DeliveryHandoffRef, options?: {
186
+ readonly requireCurrent?: boolean;
187
+ }): Promise<boolean>;
188
+ health(owner?: Owner): Promise<DeliveryHealth>;
189
+ /** Subscribe first; read the returned version, inspect state, then await changed(version,...). */
190
+ observe(owner: Owner): Promise<VersionObserver<string>>;
191
+ dispatchHandoff(owner: Owner, key: string, transport: DeliveryTransport, options: {
192
+ readonly generation: string;
193
+ readonly timeoutMs: number;
194
+ readonly signal?: AbortSignal;
195
+ }): Promise<HandoffResult>;
196
+ }
@@ -0,0 +1,268 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { validateOwner } from './neutral.js';
4
+ import { observeFileMetadata } from './file-changes.js';
5
+ import { LedgerStore } from './delivery-ledger-sqlite.js';
6
+ function nonempty(value) {
7
+ if (typeof value !== 'string' || value.length === 0)
8
+ throw new TypeError('Expected a nonempty string');
9
+ }
10
+ function positive(value) {
11
+ if (!Number.isSafeInteger(value) || value <= 0)
12
+ throw new RangeError('Expected a positive safe integer');
13
+ }
14
+ function routes(value) {
15
+ if (!Array.isArray(value))
16
+ throw new TypeError('Expected routes');
17
+ const kinds = new Set();
18
+ for (const route of value) {
19
+ nonempty(route.kind);
20
+ if (kinds.has(route.kind))
21
+ throw new TypeError('Duplicate route kind');
22
+ kinds.add(route.kind);
23
+ if (route.address === null || typeof route.address !== 'object' || Array.isArray(route.address)
24
+ || !Object.values(route.address).every(item => typeof item === 'string'))
25
+ throw new TypeError('Expected string address fields');
26
+ if (Object.keys(route).some(key => key !== 'kind' && key !== 'address'))
27
+ throw new TypeError('Unexpected route field');
28
+ }
29
+ }
30
+ function correlation(value) {
31
+ if (!value || typeof value !== 'object' || Array.isArray(value)
32
+ || !Object.entries(value).every(([key, item]) => ['token', 'messageId', 'payloadHash'].includes(key) && typeof item === 'string'))
33
+ throw new TypeError('Invalid correlation');
34
+ }
35
+ /** Shared by host and producer callers; an explicit delivery override always wins. */
36
+ export function deliveryLedgerLocation(env = process.env, homeDir = os.homedir()) {
37
+ return env.SQUARE_DELIVERY ?? path.join(homeDir, '.square', 'delivery.v3.sqlite');
38
+ }
39
+ function encodeDetails(details) {
40
+ const ancestors = new Set();
41
+ const encode = (value) => {
42
+ if (value === null || typeof value === 'string' || typeof value === 'boolean'
43
+ || typeof value === 'number' && Number.isFinite(value))
44
+ return value;
45
+ if (typeof value !== 'object' || !value || ancestors.has(value)
46
+ || !Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
47
+ throw new TypeError('Expected JSON card details');
48
+ ancestors.add(value);
49
+ const result = Array.isArray(value) ? Array.from(value, encode)
50
+ : Object.fromEntries(Object.keys(value).sort().map(key => [key, encode(value[key])]));
51
+ ancestors.delete(value);
52
+ return result;
53
+ };
54
+ if (!details || typeof details !== 'object' || Array.isArray(details))
55
+ throw new TypeError('Expected a card details object');
56
+ return JSON.stringify(encode(details));
57
+ }
58
+ /** One user-owned ledger; producers supply finished text and callers choose routes. */
59
+ export class DeliveryLedger {
60
+ store;
61
+ leaseMs;
62
+ constructor(store, leaseMs) {
63
+ this.store = store;
64
+ this.leaseMs = leaseMs;
65
+ }
66
+ static async open(options = {}) {
67
+ const location = options.location ?? deliveryLedgerLocation();
68
+ if (!path.isAbsolute(location))
69
+ throw new TypeError('Delivery location must be absolute');
70
+ const leaseMs = options.leaseMs ?? 30_000;
71
+ positive(leaseMs);
72
+ return new DeliveryLedger(await LedgerStore.open(location, options.clock ?? Date.now), leaseMs);
73
+ }
74
+ close() { return this.store.close(); }
75
+ attach(input) {
76
+ validateOwner(input.owner);
77
+ routes(input.routes);
78
+ if (input.expectedGeneration !== null)
79
+ nonempty(input.expectedGeneration);
80
+ return this.store.attach(input);
81
+ }
82
+ updateRoutes(input) {
83
+ validateOwner(input.owner);
84
+ nonempty(input.generation);
85
+ routes(input.routes);
86
+ return this.store.updateRoutes(input);
87
+ }
88
+ detach(input) { return this.lifecycle(input, 'detached'); }
89
+ markDeleted(input) { return this.lifecycle(input, 'deleted'); }
90
+ lifecycle(input, state) {
91
+ validateOwner(input.owner);
92
+ nonempty(input.generation);
93
+ return this.store.lifecycle(input, state);
94
+ }
95
+ session(owner) { validateOwner(owner); return this.store.session(owner); }
96
+ sessions() { return this.store.sessions(); }
97
+ pending(owner, options) {
98
+ validateOwner(owner);
99
+ nonempty(options.generation);
100
+ return this.store.pendingCards(owner, options.generation);
101
+ }
102
+ handoffs(owner, options) {
103
+ validateOwner(owner);
104
+ nonempty(options.generation);
105
+ return this.store.handoffs(owner, options.generation);
106
+ }
107
+ post(input) {
108
+ validateOwner(input.owner);
109
+ nonempty(input.key);
110
+ if (typeof input.text !== 'string')
111
+ throw new TypeError('Expected card text');
112
+ return this.store.post({ ...input, details: input.details === undefined ? null : encodeDetails(input.details) });
113
+ }
114
+ suppress(input) {
115
+ validateOwner(input.owner);
116
+ nonempty(input.key);
117
+ if (!['exact', 'prefix'].includes(input.scope) || input.expiresAt !== undefined && !Number.isFinite(input.expiresAt))
118
+ throw new TypeError('Invalid suppression');
119
+ return this.store.suppress(input);
120
+ }
121
+ renewSuppression(input) {
122
+ validateOwner(input.owner);
123
+ nonempty(input.id);
124
+ if (!Number.isFinite(input.expiresAt))
125
+ throw new TypeError('Expected finite suppression expiry');
126
+ return this.store.renewSuppression(input);
127
+ }
128
+ releaseSuppression(input) {
129
+ validateOwner(input.owner);
130
+ nonempty(input.id);
131
+ return this.store.releaseSuppression(input);
132
+ }
133
+ cancel(input) {
134
+ validateOwner(input.owner);
135
+ nonempty(input.generation);
136
+ return this.store.cancel(input);
137
+ }
138
+ receipts(owner, options = {}) {
139
+ validateOwner(owner);
140
+ if (options.keys)
141
+ options.keys.forEach(nonempty);
142
+ if (options.since !== undefined && !Number.isFinite(options.since))
143
+ throw new TypeError('Expected finite receipt time');
144
+ return this.store.receipts(owner, options);
145
+ }
146
+ async claimPresentation(owner, options) {
147
+ validateOwner(owner);
148
+ nonempty(options.generation);
149
+ positive(options.budgetChars);
150
+ if (options.signal?.aborted)
151
+ return undefined;
152
+ const claim = await this.store.claimPresentation(owner, options.generation, options.budgetChars, this.leaseMs, options.signal);
153
+ if (!claim)
154
+ return undefined;
155
+ const tokens = claim.attempts.map(attempt => attempt.token);
156
+ return { ...claim,
157
+ current: signal => this.store.current(tokens, signal ?? options.signal),
158
+ settle: outcome => {
159
+ if (!['presented', 'failed', 'unknown', 'released'].includes(outcome))
160
+ throw new TypeError('Invalid presentation outcome');
161
+ return this.store.settle(tokens, outcome);
162
+ },
163
+ };
164
+ }
165
+ /** Resolve the original prepared handoff, including its stored route kind. */
166
+ handoff(token) {
167
+ nonempty(token);
168
+ return this.store.handoff(token);
169
+ }
170
+ guardHandoff(ref) { return this.store.guardHandoff(ref); }
171
+ acceptHandoff(ref) { return this.store.acceptHandoff(ref); }
172
+ /** Confirms the exact existing external effect; never sends or returns text. */
173
+ observePresentation(handoff, options) {
174
+ if (typeof handoff === 'string')
175
+ nonempty(handoff);
176
+ return this.store.observePresentation(handoff, options);
177
+ }
178
+ health(owner) {
179
+ if (owner !== undefined)
180
+ validateOwner(owner);
181
+ return this.store.health(owner);
182
+ }
183
+ /** Subscribe first; read the returned version, inspect state, then await changed(version,...). */
184
+ observe(owner) {
185
+ validateOwner(owner);
186
+ return observeFileMetadata(this.store.location);
187
+ }
188
+ async dispatchHandoff(owner, key, transport, options) {
189
+ validateOwner(owner);
190
+ nonempty(key);
191
+ nonempty(options.generation);
192
+ positive(options.timeoutMs);
193
+ if (options.signal?.aborted)
194
+ return { outcome: 'not-invoked' };
195
+ nonempty(transport.kind);
196
+ const claim = await this.store.claimHandoff(owner, key, options.generation, transport.kind, Math.max(this.leaseMs, options.timeoutMs), options.signal);
197
+ if (!claim)
198
+ return { outcome: 'not-invoked' };
199
+ const token = claim.attempt.token;
200
+ const controller = new AbortController();
201
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
202
+ const deadline = Date.now() + options.timeoutMs;
203
+ let persistenceFailure;
204
+ const persist = async (operation) => {
205
+ try {
206
+ return await operation;
207
+ }
208
+ catch (error) {
209
+ persistenceFailure = { error };
210
+ throw error;
211
+ }
212
+ };
213
+ const current = () => persist(this.store.current([token], signal, claim.route));
214
+ const finish = async (outcome) => {
215
+ if (!['accepted', 'presented', 'failed', 'unknown', 'not-capable', 'released'].includes(outcome))
216
+ throw new TypeError('Invalid transport outcome');
217
+ const recorded = await this.store.settle([token], outcome === 'not-capable' ? 'released' : outcome);
218
+ if (persistenceFailure)
219
+ throw persistenceFailure.error;
220
+ return { outcome, token, persistence: recorded ? 'recorded' : 'not-recorded' };
221
+ };
222
+ if (!await current() || signal.aborted || Date.now() >= deadline)
223
+ return finish('released');
224
+ let timer;
225
+ let onAbort;
226
+ try {
227
+ const interrupted = new Promise(resolve => {
228
+ onAbort = () => resolve('unknown');
229
+ signal.addEventListener('abort', onAbort, { once: true });
230
+ timer = setTimeout(() => { resolve('unknown'); controller.abort(new Error('Delivery deadline elapsed')); }, options.timeoutMs);
231
+ });
232
+ // Invoke synchronously after the final fence; adapters own checks after their own awaits.
233
+ const effect = (async () => {
234
+ if (signal.aborted || Date.now() >= deadline)
235
+ return 'released';
236
+ return transport.send(claim.card, claim.route.address, { token, deadline, signal,
237
+ beforeSend: current,
238
+ prepare: value => { correlation(value); return persist(this.store.prepare(token, value, signal)); },
239
+ });
240
+ })().catch(() => 'unknown');
241
+ const first = await Promise.race([
242
+ effect.then(outcome => ({ outcome, interrupted: false })),
243
+ interrupted.then(outcome => ({ outcome, interrupted: true })),
244
+ ]);
245
+ if (!first.interrupted)
246
+ return await finish(first.outcome);
247
+ const unknown = finish('unknown');
248
+ // Register before awaiting persistence; even an already completed effect waits for unknown.
249
+ const lateSettlement = unknown.then(async () => {
250
+ const outcome = await effect;
251
+ try {
252
+ const result = await finish(outcome);
253
+ return { outcome, persistence: { status: result.persistence } };
254
+ }
255
+ catch (error) {
256
+ return { outcome, persistence: { status: 'error', error } };
257
+ }
258
+ }, error => ({ outcome: 'unknown', persistence: { status: 'error', error } }));
259
+ return { ...await unknown, lateSettlement };
260
+ }
261
+ finally {
262
+ if (timer !== undefined)
263
+ clearTimeout(timer);
264
+ if (onAbort)
265
+ signal.removeEventListener('abort', onAbort);
266
+ }
267
+ }
268
+ }
@@ -0,0 +1,6 @@
1
+ import type { DeliveryLedger, HandoffResult } from './delivery-ledger.js';
2
+ /** Retire the caller's connection without abandoning an already-invoked effect. */
3
+ export declare function retainDeliveryLedger(ledger: DeliveryLedger): {
4
+ track(result: HandoffResult): void;
5
+ close(): Promise<void>;
6
+ };
@@ -0,0 +1,45 @@
1
+ import { redactCurrentDiagnostic } from './diagnostic-redaction.js';
2
+ /** Retire the caller's connection without abandoning an already-invoked effect. */
3
+ export function retainDeliveryLedger(ledger) {
4
+ const pending = new Set();
5
+ let retired = false, closing;
6
+ const diagnose = (event, result, detail) => {
7
+ try {
8
+ const diagnostic = redactCurrentDiagnostic({ event, token: result?.token, outcome: result?.outcome, ...detail });
9
+ try {
10
+ console.warn(diagnostic);
11
+ }
12
+ catch {
13
+ process.stderr.write(`${JSON.stringify(diagnostic)}\n`);
14
+ }
15
+ }
16
+ catch { /* Closed diagnostic sinks cannot prevent settlement or resource retirement. */ }
17
+ };
18
+ const closeOnce = () => closing ??= ledger.close();
19
+ const settled = (handle) => {
20
+ pending.delete(handle);
21
+ if (retired && !pending.size)
22
+ void closeOnce().catch(() => diagnose('delivery-ledger-deferred-close-error'));
23
+ };
24
+ return {
25
+ track(result) {
26
+ const handle = result.lateSettlement;
27
+ if (!handle || pending.has(handle))
28
+ return;
29
+ pending.add(handle);
30
+ void handle.then(late => {
31
+ if (late.persistence.status !== 'recorded')
32
+ diagnose('delivery-late-settlement', result, { lateOutcome: late.outcome, persistence: late.persistence.status });
33
+ settled(handle);
34
+ }, () => {
35
+ diagnose('delivery-late-settlement-rejected', result);
36
+ settled(handle);
37
+ });
38
+ },
39
+ async close() {
40
+ retired = true;
41
+ if (!pending.size)
42
+ await closeOnce();
43
+ },
44
+ };
45
+ }
@@ -71,9 +71,8 @@ export async function doctorClaudePlugin(homeDir, run = runClaude) {
71
71
  return [
72
72
  fs.existsSync(bundle) ? `✓ Square Claude plugin bundle ${root}` : `○ Square Claude plugin bundle missing ${root}`,
73
73
  installed ? `✓ ${CLAUDE_PLUGIN_ID} installed` : `○ ${CLAUDE_PLUGIN_ID} unavailable`,
74
- `Claude native inbox: requires macOS Claude Code with a trusted, approved interactive mod.`,
75
- `Installation or a socket alone does not prove receiver availability; absent/disabled mods, headless unapproved mods, other platforms are unavailable.`,
76
- `Queue admission is not model processing. Queued or held native messages cannot be retracted by Square cancellation.`,
77
- `Live routes and admission/presentation evidence: square harness doctor delivery. No legacy fallback.`,
74
+ `Claude delivery uses synchronous prompt/tool hooks and one finite 24-hour asyncRewake Stop observer; actual coverage ends at native boundaries.`,
75
+ `Idle Escape cannot disarm a watcher, and already emitted notifications cannot be recalled; busy Escape disarms at the next prompt boundary.`,
76
+ `Native stderr output is recorded unknown until its exact token/hash frame appears at a later prompt boundary; model comprehension is not observed.`,
78
77
  ];
79
78
  }
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
3
4
  import { isDeepStrictEqual } from 'node:util';
4
5
  import crossSpawn from 'cross-spawn';
5
6
  import { applyEdits, createScanner, findNodeAtLocation, modify, parse, parseTree } from 'jsonc-parser';
@@ -28,22 +29,35 @@ export function installOpenCodePlugin(homeDir, force = false, run = runOpenCode)
28
29
  // Explicit `square install` replaces owned pins/duplicates with one unpinned
29
30
  // current-field entry; updating an exact old pin otherwise succeeds unchanged.
30
31
  void force;
32
+ const version = openCodeCommand(homeDir, ['--version'], run, 'version check').trim();
33
+ const match = /^(?:opencode[ \t]+)?v?(\d+)\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
34
+ if (!match)
35
+ throw new Error(`Could not determine OpenCode version from ${JSON.stringify(version)}; check opencode --version output.`);
36
+ const major = Number(match[1]);
37
+ if (major !== 1 && major !== 2)
38
+ throw new Error(`Unsupported OpenCode version ${JSON.stringify(version)}; install OpenCode 1.x or 2.x and retry.`);
31
39
  const configs = readOpenCodeConfigs(homeDir);
32
40
  const entries = configs.flatMap((config) => config.entries);
33
41
  const options = entries.map((entry) => entry.options).filter((value) => value !== undefined);
34
42
  if (options.some((value) => !isDeepStrictEqual(value, options[0]))) {
35
43
  throw new Error('Conflicting Square plugin options in OpenCode configuration; reconcile the owned plugin/plugins entries before installing.');
36
44
  }
37
- const canonical = options.length ? { package: SQUARE_IDENTITY.packageName, options: options[0] } : SQUARE_IDENTITY.packageName;
45
+ if (major === 1 && options.length)
46
+ throw new Error('Square plugin options cannot be translated to OpenCode 1.x; reconcile the owned plugin options before installing.');
47
+ const canonical = major === 1 ? pathToFileURL(path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'plugins/opencode-v1/index.js')).href
48
+ : options.length ? { package: SQUARE_IDENTITY.packageName, options: options[0] } : SQUARE_IDENTITY.packageName;
49
+ if (major === 1 && !fs.existsSync(fileURLToPath(canonical)))
50
+ throw new Error('Square OpenCode 1.x plugin is missing from this installation; reinstall Square.');
51
+ const field = major === 1 ? 'plugin' : 'plugins';
38
52
  const targets = configs.length ? configs : [{ target: openCodeConfigPaths(homeDir)[0], source: '{}', entries: [] }];
39
- const alreadyCanonical = entries.length === 1 && configs[0]?.entries[0]?.field === 'plugins'
53
+ const alreadyCanonical = entries.length === 1 && configs[0]?.entries[0]?.field === field
40
54
  && isDeepStrictEqual(entries[0].value, canonical);
41
55
  const changes = alreadyCanonical ? [] : targets.map(({ target, source, entries }, index) => {
42
56
  let next = source;
43
57
  for (const { field, index } of entries.toReversed())
44
58
  next = removeOpenCodeEntry(next, field, index);
45
59
  if (index === 0)
46
- next = applyEdits(next, modify(next, ['plugins', -1], canonical, {}));
60
+ next = applyEdits(next, modify(next, [field, -1], canonical, {}));
47
61
  return { target, source, next };
48
62
  }).filter(({ source, next }) => source !== next);
49
63
  // Appending just our element avoids native add's whole-array rewrite, which
@@ -52,6 +66,8 @@ export function installOpenCodePlugin(homeDir, force = false, run = runOpenCode)
52
66
  fs.mkdirSync(path.dirname(target), { recursive: true });
53
67
  fs.writeFileSync(target, next);
54
68
  }
69
+ if (major === 1)
70
+ return [SQUARE_IDENTITY.packageName];
55
71
  if (!entries.some(({ spec }) => spec === SQUARE_IDENTITY.packageName)) {
56
72
  try {
57
73
  openCodeCommand(homeDir, ['plugin', 'add', SQUARE_IDENTITY.packageName], run, 'install');
@@ -100,6 +116,7 @@ function readOpenCodeConfigs(homeDir) {
100
116
  if (errors.length || typeof config !== 'object' || config === null || Array.isArray(config)) {
101
117
  throw new Error(`Invalid OpenCode configuration: ${target}`);
102
118
  }
119
+ const unproven = [];
103
120
  const entries = ['plugin', 'plugins'].flatMap((field) => {
104
121
  const plugins = config[field];
105
122
  if (plugins === undefined)
@@ -108,12 +125,15 @@ function readOpenCodeConfigs(homeDir) {
108
125
  throw new Error(`Invalid OpenCode ${field} configuration: ${target}`);
109
126
  return plugins.flatMap((entry, index) => {
110
127
  const spec = openCodePluginSpec(entry);
128
+ const raw = openCodeRawSpec(entry);
129
+ if (spec === undefined && raw !== undefined && openCodeV1Root(raw) !== undefined)
130
+ unproven.push(raw);
111
131
  const options = Array.isArray(entry) ? entry[1]
112
132
  : typeof entry === 'object' && entry !== null ? entry.options : undefined;
113
133
  return spec === undefined ? [] : [{ field, index, spec, options, value: entry }];
114
134
  });
115
135
  });
116
- return [{ target, source, entries }];
136
+ return [{ target, source, entries, unproven }];
117
137
  });
118
138
  }
119
139
  export function uninstallOpenCodePlugin(homeDir) {
@@ -148,11 +168,37 @@ function removeOpenCodeEntry(source, field, index) {
148
168
  ...(comma === undefined ? [] : [{ offset: comma, length: 1, content: '' }]),
149
169
  ]);
150
170
  }
151
- function openCodePluginSpec(entry) {
171
+ function openCodeRawSpec(entry) {
152
172
  const spec = typeof entry === 'string' ? entry : Array.isArray(entry) ? entry[0]
153
173
  : typeof entry === 'object' && entry !== null && 'package' in entry ? entry.package : undefined;
154
- return typeof spec === 'string' && (spec === SQUARE_IDENTITY.packageName || spec.startsWith(`${SQUARE_IDENTITY.packageName}@`))
155
- ? spec : undefined;
174
+ return typeof spec === 'string' ? spec : undefined;
175
+ }
176
+ function openCodeV1Root(spec) {
177
+ try {
178
+ const file = fileURLToPath(spec);
179
+ return file.endsWith(`${path.sep}dist${path.sep}plugins${path.sep}opencode-v1${path.sep}index.js`)
180
+ ? path.resolve(path.dirname(file), '../../..') : undefined;
181
+ }
182
+ catch {
183
+ return undefined;
184
+ }
185
+ }
186
+ function openCodePluginSpec(entry) {
187
+ const spec = openCodeRawSpec(entry);
188
+ if (spec === undefined)
189
+ return undefined;
190
+ if (spec === SQUARE_IDENTITY.packageName || spec.startsWith(`${SQUARE_IDENTITY.packageName}@`))
191
+ return spec;
192
+ const root = openCodeV1Root(spec);
193
+ if (root === undefined)
194
+ return undefined;
195
+ try {
196
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
197
+ return manifest.name === SQUARE_IDENTITY.packageName ? spec : undefined;
198
+ }
199
+ catch {
200
+ return undefined;
201
+ }
156
202
  }
157
203
  function parseOpenCodeConfig(stdout) {
158
204
  let sources;
@@ -171,6 +217,16 @@ function parseOpenCodeConfig(stdout) {
171
217
  /** Config sources prove configuration, not that a running session loaded the plugin. */
172
218
  export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
173
219
  try {
220
+ const configs = readOpenCodeConfigs(homeDir);
221
+ const local = configs.flatMap(config => config.entries).filter(entry => entry.spec.startsWith('file:'));
222
+ const missing = local.find(entry => !fs.existsSync(fileURLToPath(entry.spec)));
223
+ if (missing)
224
+ return `✕ OpenCode 1.x plugin file missing: ${missing.spec}; reinstall Square and run square install opencode`;
225
+ const unproven = configs.flatMap(config => config.unproven)[0];
226
+ if (unproven)
227
+ return `○ OpenCode plugin ownership unverified: ${unproven}; inspect it before removing or replacing it`;
228
+ if (local.length)
229
+ return '✓ OpenCode 1.x plugin configured (runtime loading not verified)';
174
230
  const result = run(homeDir, ['debug', 'config']);
175
231
  if (result.status !== 0) {
176
232
  const diagnostic = result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`;