@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,20 +1,8 @@
1
- import { operationEnv } from './participant-identity.js';
2
- import { channelArtifact, createArtifactPort } from './artifact.js';
3
- import { recordClaudeDeliveryStatus } from './claude-delivery-status.js';
4
- import { Square } from './square-api.js';
5
1
  import { observeClaudeDelivery } from './claude-delivery.js';
6
- import { processActNotificationsOnce } from './notifications.js';
7
- import { sweepPending } from './delivery-operations.js';
8
2
  import { beginClaudeMcpCall, endClaudeMcpCall } from './claude-mcp-context.js';
9
- import { asSeatRef } from './seats.js';
10
- import { projectSessionSeats } from './inbox.js';
11
- import { restoreSessionSeats } from './session-lifecycle.js';
3
+ import { DeliveryLedger } from './delivery-ledger.js';
4
+ import { attachDeliverySessionRoutes, detachDeliverySession } from './session-lifecycle.js';
12
5
  import { harnessOwner } from './owner.js';
13
- import { InternalSquareError, SquareError } from './model.js';
14
- /** Persisted hook state keeps its wire coordinate; internal consumers keep the located seat. */
15
- function claudeBinding({ location, seat }, sessionId) {
16
- return { location, participant: seat.participant, session: sessionId, generation: seat.generation };
17
- }
18
6
  /** JSON stdin bridge. Identity is explicit per operation; process.env is never rewritten. */
19
7
  export async function runClaudeMod(inputText, env = process.env) {
20
8
  if (inputText.length > 512_000)
@@ -22,10 +10,6 @@ export async function runClaudeMod(inputText, env = process.env) {
22
10
  const input = JSON.parse(inputText);
23
11
  if (!input || typeof input.sessionId !== 'string' || !input.sessionId || typeof input.cwd !== 'string')
24
12
  throw new Error('Claude bridge coordinate missing.');
25
- if (input.operation === 'status') {
26
- const recorded = input.status?.sessionId === input.sessionId && await recordClaudeDeliveryStatus(input.status, env);
27
- return `${JSON.stringify({ recorded })}\n`;
28
- }
29
13
  if (input.operation === 'tool-begin') {
30
14
  return `${JSON.stringify(await beginClaudeMcpCall({ toolUseId: input.toolUseId, sessionId: input.sessionId, cwd: input.cwd, agentId: input.agentId }, env))}\n`;
31
15
  }
@@ -33,140 +17,44 @@ export async function runClaudeMod(inputText, env = process.env) {
33
17
  await endClaudeMcpCall(input.toolUseId, input.token, env);
34
18
  return '{}\n';
35
19
  }
36
- const scoped = operationEnv('claude', input.sessionId, env);
37
- const signal = AbortSignal.timeout(2500);
38
20
  const owner = harnessOwner({ channel: 'claude-code', session: input.sessionId, ...(input.agentId === undefined ? {} : { agent: input.agentId }) });
39
21
  const supported = process.platform === 'darwin' && typeof input.endpoint === 'string' && input.endpoint.startsWith('/');
40
- const sessionSeats = async () => (await projectSessionSeats({ owner, env }, signal)).filter(member => member.seat.attachment === 'attached');
41
- if (input.operation === 'end') {
42
- const retired = [];
43
- const captured = [...input.bindings ?? []];
44
- // Discover committed late joins at the pinned end boundary, never later owners.
45
- if (input.endedAt !== undefined)
46
- for (const member of await projectSessionSeats({ owner, env }, signal)) {
47
- if (member.joinedAt > input.endedAt || captured.some((item) => item.location === member.location
48
- && item.participant === member.seat.participant && item.generation === member.seat.generation))
49
- continue;
50
- captured.push(claudeBinding(member, input.sessionId));
51
- }
52
- for (const binding of captured) {
53
- if (binding.session !== input.sessionId)
54
- continue;
55
- const square = await Square.at({ path: binding.location, env: scoped });
22
+ const routes = supported ? [{ kind: 'claude-native', address: { sessionId: input.sessionId, endpoint: input.endpoint, version: input.version, platform: 'darwin', loaded: 'true' } }] : [];
23
+ const captured = input.bindings?.find(binding => binding.session === input.sessionId)?.generation;
24
+ if (input.operation === 'start') {
25
+ const session = await attachDeliverySessionRoutes(owner, env, routes, AbortSignal.timeout(2500));
26
+ if (session && !supported && session.routes.some(route => route.kind === 'claude-native')) {
27
+ const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
56
28
  try {
57
- const detached = binding.generation === undefined ? null
58
- : await square.detach(binding.participant, { owner, generation: binding.generation });
59
- if (detached)
60
- retired.push(binding);
29
+ await ledger.updateRoutes({ owner, generation: session.generation, routes: session.routes.filter(route => route.kind !== 'claude-native') });
61
30
  }
62
31
  finally {
63
- await square.close();
32
+ await ledger.close();
64
33
  }
65
- if (signal.aborted)
66
- break;
67
34
  }
68
- return `${JSON.stringify({ retired })}\n`;
35
+ return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude with an interactive loaded mod and its real endpoint.', bindings: session ? [{ session: input.sessionId, generation: session.generation }] : [] })}\n`;
69
36
  }
70
- const recovery = new Set();
71
- const cancelledBindings = [...input.cancelledBindings ?? []];
72
- if (input.operation === 'start') {
73
- await restoreSessionSeats(owner, scoped, supported ? {
74
- kind: 'claude-native', address: { sessionId: input.sessionId, endpoint: input.endpoint, version: input.version, platform: 'darwin', loaded: 'true' },
75
- } : undefined, signal);
37
+ if (input.operation === 'end') {
38
+ const retired = captured && await detachDeliverySession(owner, captured, env);
39
+ return `${JSON.stringify({ retired: retired ? input.bindings : [] })}\n`;
76
40
  }
77
- if (input.operation === 'start' || input.operation === 'reconcile' || input.operation === 'cancel' || input.cancelAt !== undefined) {
78
- for (const member of await sessionSeats()) {
79
- if (signal.aborted)
80
- break;
81
- const { location, seat: capturedSeat, joinedAt } = member;
82
- const matches = (item) => item !== undefined
83
- && item.location === location && item.participant === capturedSeat.participant && item.session === input.sessionId
84
- && (item.generation === undefined || item.generation === capturedSeat.generation);
85
- const expected = input.bindings?.find(matches);
86
- const cancelled = input.cancelledBindings?.find(matches) ?? (input.operation === 'cancel' ? expected : undefined);
87
- const lateCancelledJoin = input.operation === 'cancel' && input.cancelAt !== undefined
88
- && joinedAt <= input.cancelAt && expected === undefined;
89
- if (lateCancelledJoin && !cancelledBindings.some(matches))
90
- cancelledBindings.push(claudeBinding(member, input.sessionId));
91
- if (input.operation !== 'start' && input.operation !== 'reconcile' && !lateCancelledJoin && expected === undefined)
92
- continue;
93
- const artifact = createArtifactPort(location, undefined, signal);
94
- try {
95
- const { value: currentSeat } = await artifact.read((facts) => facts.seat(capturedSeat.participant), signal);
96
- if (currentSeat === undefined || currentSeat.attachment !== 'attached' || currentSeat.generation !== capturedSeat.generation
97
- || currentSeat.owner !== capturedSeat.owner)
98
- continue;
99
- const seat = asSeatRef(currentSeat);
100
- const existing = currentSeat.wakeAddress?.kind === 'claude-native' ? currentSeat.wakeAddress : undefined;
101
- const cutoff = input.cancelAt !== undefined && (cancelled !== undefined || lateCancelledJoin) ? input.cancelAt : undefined;
102
- if (cutoff !== undefined) {
103
- const { value: through } = await artifact.read((facts) => {
104
- let beforeIndex;
105
- for (;;) {
106
- const page = facts.activityHeads({ beforeIndex }, { edge: 'newest', limit: 100 });
107
- const last = page.items.find((activity) => activity.at <= cutoff && activity.messageIndex !== undefined);
108
- if (last !== undefined)
109
- return last.messageIndex;
110
- if (!page.hasMore)
111
- return -1;
112
- beforeIndex = page.items[0].index;
113
- }
114
- }, signal);
115
- if (through >= 0)
116
- await artifact.cancelDeliveries(seat, through, signal);
117
- }
118
- if (!supported) {
119
- if (existing !== undefined)
120
- await artifact.transactAs({ kind: 'participant', seat }, (facts) => facts.updateWakeAddress(seat, undefined), signal);
121
- continue;
122
- }
123
- if (existing === undefined || existing.address.endpoint !== input.endpoint)
124
- recovery.add(location);
125
- await artifact.transactAs({ kind: 'participant', seat }, (facts) => facts.updateWakeAddress(seat, {
126
- kind: 'claude-native', address: { sessionId: input.sessionId, endpoint: input.endpoint, version: input.version, platform: 'darwin', loaded: 'true' },
127
- }), signal);
128
- }
129
- catch (error) {
130
- if (error instanceof InternalSquareError && error.code === 'not_found')
131
- throw new SquareError('unavailable', `Square is unavailable at ${location}`);
132
- throw error;
133
- }
134
- finally {
135
- await artifact.close();
41
+ if (input.operation === 'reconcile' || input.operation === 'cancel') {
42
+ const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
43
+ try {
44
+ const session = await ledger.session(owner);
45
+ if (captured && session?.generation === captured && session.state === 'attached') {
46
+ if (input.operation === 'cancel')
47
+ await ledger.cancel({ owner, generation: captured });
48
+ else
49
+ await ledger.updateRoutes({ owner, generation: captured, routes: [...session.routes.filter(route => route.kind !== 'claude-native'), ...routes] });
136
50
  }
51
+ return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude with an interactive loaded mod and its real endpoint.', bindings: input.bindings ?? [], cancelledBindings: input.bindings ?? [] })}\n`;
52
+ }
53
+ finally {
54
+ await ledger.close();
137
55
  }
138
- }
139
- if (input.operation === 'start' || input.operation === 'reconcile' || input.operation === 'cancel') {
140
- const members = await sessionSeats();
141
- // One startup recovery pass for known unsent work. Unknown/admitted sends never replay.
142
- if (supported && input.operation !== 'cancel')
143
- for (const { location } of members) {
144
- if (signal.aborted)
145
- break;
146
- if (input.operation !== 'start' && !recovery.has(location))
147
- continue;
148
- const artifact = createArtifactPort(location, undefined, signal);
149
- let indices;
150
- try {
151
- indices = await sweepPending({ artifact: channelArtifact(artifact), location, now: Date.now(), graceMs: 0, limit: 8 });
152
- }
153
- catch (error) {
154
- if (error instanceof InternalSquareError && error.code === 'not_found')
155
- throw new SquareError('unavailable', `Square is unavailable at ${location}`);
156
- throw error;
157
- }
158
- finally {
159
- await artifact.close();
160
- }
161
- for (const index of indices) {
162
- if (signal.aborted)
163
- break;
164
- await processActNotificationsOnce(location, index, { env: { ...env, SQUARE_NOTIFY_DELIVERY_WAIT_MS: '200' } });
165
- }
166
- }
167
- return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude with an interactive loaded mod and its real endpoint.', bindings: members.map(member => claudeBinding(member, input.sessionId)), cancelledBindings })}\n`;
168
56
  }
169
57
  if (!['guard', 'admitted', 'stored'].includes(input.operation) || typeof input.text !== 'string')
170
58
  throw new Error('Unknown Claude bridge operation.');
171
- return `${JSON.stringify(await observeClaudeDelivery(input.sessionId, input.text, input.operation, env))}\n`;
59
+ return `${JSON.stringify(await observeClaudeDelivery(input.sessionId, input.text, input.operation, env, input.refs))}\n`;
172
60
  }
@@ -1,5 +1,5 @@
1
- import type { WakeTransportPort } from './channel-transport.js';
1
+ import type { DeliveryTransport } from './delivery-ledger.js';
2
2
  export declare function nativeTokens(text: string): string[];
3
- export declare function nativePayload(token: string, preview: string): string;
3
+ export declare function nativePayload(token: string, text: string): string;
4
4
  export declare function hashPayload(payload: string): string;
5
- export declare function createClaudeWakeTransport(env?: NodeJS.ProcessEnv): WakeTransportPort;
5
+ export declare function createClaudeWakeTransport(env?: NodeJS.ProcessEnv): DeliveryTransport;
@@ -1,58 +1,34 @@
1
1
  import { stat } from 'node:fs/promises';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
+ import { renderDeliveryReminder } from './delivery-frame.js';
3
4
  import { writeClaudeNative } from './packages/agent-delivery/src/claude-native.js';
4
5
  const marker = /(?:^|\n)\[square-inbox:([a-zA-Z0-9-]+)\](?:\n|$)/g;
5
- export function nativeTokens(text) { return [...text.matchAll(marker)].map((match) => match[1]); }
6
- export function nativePayload(token, preview) { return `[square-inbox:${token}]\n${preview}\n[/square-inbox:${token}]`; }
7
- function nativeSupported(route) { return route.address.loaded === 'true' && !!route.address.endpoint; }
8
- export function hashPayload(payload) { return createHash('sha256').update(payload).digest('hex'); }
9
- async function preview(request) {
10
- return request.presentation;
11
- }
12
- async function writeClaudeWake(sessionId, endpoint, messageId, payload, timeoutMs, signal) {
13
- const result = await writeClaudeNative({ sessionId, endpoint, messageId }, payload, { deadline: Date.now() + timeoutMs, signal });
14
- if (result.outcome === 'unavailable')
15
- return { outcome: 'failed', signature: 'claude_endpoint_unavailable', message: result.message };
16
- return { outcome: 'unknown', signature: 'native_admission_unconfirmed', message: result.outcome === 'written' ? 'Claude accepted the local write; native admission is unconfirmed.' : result.message };
6
+ export function nativeTokens(text) { return [...text.matchAll(marker)].map(match => match[1]); }
7
+ export function nativePayload(token, text) {
8
+ return renderDeliveryReminder([`[square-inbox:${token}]\n${text}\n[/square-inbox:${token}]`], { tagged: false });
17
9
  }
10
+ export function hashPayload(payload) { return createHash('sha256').update(payload).digest('hex'); }
18
11
  export function createClaudeWakeTransport(env = process.env) {
19
12
  return {
20
- async probe(route) {
21
- if (!nativeSupported(route))
22
- return { outcome: 'not-capable', diagnostic: 'Claude native inbox unavailable: a loaded interactive Square mod is required.' };
13
+ kind: 'claude-native',
14
+ async send(card, address, context) {
15
+ if (address.loaded !== 'true' || !address.endpoint || !address.sessionId)
16
+ return 'not-capable';
23
17
  try {
24
- return (await stat(route.address.endpoint)).isSocket() || { outcome: 'not-capable', diagnostic: 'Claude native inbox endpoint is not a socket.' };
18
+ if (!(await stat(address.endpoint)).isSocket())
19
+ return 'not-capable';
25
20
  }
26
21
  catch {
27
- return { outcome: 'not-capable', diagnostic: 'Claude native inbox endpoint unavailable.' };
22
+ return 'not-capable';
28
23
  }
24
+ const messageId = randomUUID();
25
+ const payload = nativePayload(context.token, card.text);
26
+ if (!await context.prepare({ token: context.token, messageId, payloadHash: hashPayload(payload) }))
27
+ return 'not-capable';
28
+ if (context.signal.aborted || Date.now() >= context.deadline)
29
+ return 'not-capable';
30
+ const result = await writeClaudeNative({ sessionId: address.sessionId, endpoint: address.endpoint, messageId, env }, payload, { deadline: context.deadline, signal: context.signal, beforeSend: context.beforeSend });
31
+ return result.outcome === 'unavailable' ? 'failed' : 'unknown';
29
32
  },
30
- attempt: (request, timeoutMs, beforeSend) => dispatchClaude(request, timeoutMs, beforeSend, env),
31
33
  };
32
34
  }
33
- async function dispatchClaude(request, timeoutMs, beforeSend, env = process.env) {
34
- void env;
35
- const signal = AbortSignal.timeout(timeoutMs);
36
- if (!nativeSupported(request.route) || request.claimToken === undefined)
37
- return { outcome: 'not-capable', diagnostic: 'Claude native delivery needs a current artifact claim and loaded inbox.' };
38
- if (signal.aborted || !await beforeSend())
39
- return { outcome: 'unknown', signature: 'native_send_suppressed' };
40
- const prepared = await preview(request);
41
- if (prepared === undefined)
42
- return { outcome: 'not-capable', diagnostic: 'Claude activity is no longer current.' };
43
- const messageId = randomUUID();
44
- const payload = nativePayload(request.claimToken, prepared.payload);
45
- const correlation = { harness: 'claude', token: request.claimToken, messageId, payloadHash: hashPayload(payload) };
46
- if (!await request.channel.prepare({ correlation, coverage: prepared.coverage,
47
- ...(prepared.description === undefined ? {} : { description: prepared.description }) }))
48
- return { outcome: 'not-capable', diagnostic: 'Claude delivery claim is no longer current.' };
49
- const fresh = await preview(request);
50
- if (fresh === undefined || nativePayload(request.claimToken, fresh.payload) !== payload)
51
- return { outcome: 'not-capable', diagnostic: 'Claude activity changed before native write.' };
52
- if (signal.aborted || !await beforeSend())
53
- return { outcome: 'not-capable', diagnostic: 'Claude activity was consumed before native write.' };
54
- const sessionId = request.route.address.sessionId;
55
- if (sessionId === undefined)
56
- return { outcome: 'not-capable', diagnostic: 'Claude native route has no session address.' };
57
- return writeClaudeWake(sessionId, request.route.address.endpoint, messageId, payload, timeoutMs, signal);
58
- }
@@ -150,22 +150,20 @@ export function squareInput(context) {
150
150
  export function commandIdentity(env, explicit) {
151
151
  if (explicit !== undefined)
152
152
  return { owner: explicit };
153
- const configured = env.SQUARE_OWNER;
154
- if (configured !== undefined)
155
- return { owner: configured };
156
- const selected = harnessSessionSources.flatMap(({ variable, channel }) => env[variable]?.trim() ? [{ channel, session: env[variable].trim() }] : []);
157
- const native = selected.filter(source => source.channel !== 'paseo');
153
+ if (env.SQUARE_OWNER !== undefined)
154
+ return { owner: env.SQUARE_OWNER };
158
155
  if (env.CLAUDE_CODE_CHILD_SESSION === '1')
159
156
  throw new Error('Harness identity is incomplete; provide --owner explicitly.');
157
+ const selected = harnessSessionSources.flatMap(({ variable, channel }) => env[variable]?.trim() ? [{ channel, session: env[variable].trim() }] : []);
158
+ const native = selected.filter(source => source.channel !== 'paseo');
160
159
  if (native.length > 1)
161
160
  throw new Error('Multiple harness execution coordinates are present; select one explicit owner.');
162
- const paseo = selected.find(source => source.channel === 'paseo');
163
- const coordinate = native[0] ?? paseo;
161
+ const coordinate = native[0] ?? selected[0];
164
162
  if (coordinate === undefined)
165
163
  return { owner: localUserOwner() };
166
164
  const owner = harnessOwner({ channel: coordinate.channel, session: coordinate.session });
167
- const wakeAddress = coordinate.channel === 'codex' ? codexWakeAddress(coordinate.session, env)
168
- : paseo === undefined ? undefined : { kind: 'paseo', address: { agentId: paseo.session } };
165
+ const wakeAddress = coordinate.channel === 'codex' ? { ...codexWakeAddress(coordinate.session, env), kind: 'codex-queue' }
166
+ : coordinate.channel === 'paseo' ? { kind: 'paseo', address: { agentId: coordinate.session } } : undefined;
169
167
  return { owner, ...(wakeAddress === undefined ? {} : { wakeAddress }) };
170
168
  }
171
169
  /** Adapter lifetime only; each callback uses the actual public resource API. */
@@ -27,4 +27,6 @@ interface InboxIntent {
27
27
  export declare const inboxCommand: CommandSpec<InboxIntent, string>;
28
28
  export declare const claudeModCommand: CommandSpec<undefined, string>;
29
29
  export declare const codexHookCommand: CommandSpec<undefined, string>;
30
+ export declare const codexWatchCommand: CommandSpec<undefined, string>;
31
+ export declare const codexInterruptCommand: CommandSpec<undefined, string>;
30
32
  export {};
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { once } from 'node:events';
3
3
  import { runClaudeMod } from '../claude-mod.js';
4
- import { runCodexHook } from '../codex-hook.js';
4
+ import { runCodexHook, runCodexInterrupt } from '../codex-hook.js';
5
+ import { runCodexWatch } from '../codex-watch.js';
5
6
  import { isSquareError } from '../model.js';
6
7
  import { commandPrefix, participantIdentity, truncateChars, withPathOutput, quoteShell, } from '../presentation.js';
7
8
  import { PARTICIPANTS_DEFAULT_LIMIT, PARTICIPANTS_MAX_LIMIT, renderHistoryResult, renderParticipantsResult, renderStatusResult, } from '../operation-presentation.js';
@@ -526,4 +527,13 @@ function hookCommand(runHook) {
526
527
  };
527
528
  }
528
529
  export const claudeModCommand = hookCommand(runClaudeMod);
529
- export const codexHookCommand = hookCommand(runCodexHook);
530
+ export const codexHookCommand = {
531
+ parse(argv, context) { if (argv.length > 0)
532
+ usage(context.command); return undefined; },
533
+ execute: async () => runCodexHook(await readStdin(), process.env, text => new Promise((resolve, reject) => {
534
+ process.stdout.write(text, error => error ? reject(error) : resolve());
535
+ })),
536
+ present: () => { },
537
+ };
538
+ export const codexWatchCommand = hookCommand(input => runCodexWatch(input, process.env));
539
+ export const codexInterruptCommand = hookCommand(input => runCodexInterrupt(input, process.env));
@@ -4,7 +4,7 @@ import { harnessCommand, installCommand, uninstallCommand } from './harness-comm
4
4
  import { helpCommand, versionCommand } from './meta-commands.js';
5
5
  import { mcpServerCommand } from './mcp-server-command.js';
6
6
  import { JSON_COMMANDS } from './json.js';
7
- import { catchCommand, claudeModCommand, codexHookCommand, historyCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, } from './observation-commands.js';
7
+ import { catchCommand, claudeModCommand, codexHookCommand, codexWatchCommand, codexInterruptCommand, historyCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, } from './observation-commands.js';
8
8
  /** Every public command is an executable adapter, including aliases and utility commands. */
9
9
  export const commandRegistry = [
10
10
  { names: ['build'], spec: buildCommand },
@@ -14,6 +14,8 @@ export const commandRegistry = [
14
14
  { names: ['inbox'], spec: inboxCommand },
15
15
  { names: ['claude-mod'], spec: claudeModCommand },
16
16
  { names: ['codex-hook'], spec: codexHookCommand },
17
+ { names: ['codex-watch'], spec: codexWatchCommand },
18
+ { names: ['codex-interrupt'], spec: codexInterruptCommand },
17
19
  { names: ['catch'], spec: catchCommand },
18
20
  { names: ['express'], spec: expressCommand },
19
21
  { names: ['listen'], spec: listenCommand },
@@ -1,3 +1,3 @@
1
- import type { WakeAddress } from './channel-values.js';
1
+ import type { DeliveryRoute } from './delivery-ledger.js';
2
2
  /** Capture the receiver's CLI configuration when its seat is established, not at send time. */
3
- export declare function codexWakeAddress(threadId: string, env: NodeJS.ProcessEnv): WakeAddress;
3
+ export declare function codexWakeAddress(threadId: string, env: NodeJS.ProcessEnv): DeliveryRoute;
@@ -1,6 +1,2 @@
1
- export type { CodexBoundary } from './packages/agent-delivery/src/codex-boundary.js';
1
+ export type { CodexBoundary, CodexStopEpoch } from './packages/agent-delivery/src/codex-boundary.js';
2
2
  export declare function codexBoundaryStore(env?: NodeJS.ProcessEnv): import("./packages/agent-delivery/src/codex-boundary.js").CodexBoundaryStore;
3
- export declare const readCodexBoundary: (id: string, env?: NodeJS.ProcessEnv) => Promise<import("./packages/agent-delivery/src/codex-boundary.js").CodexBoundary | undefined>;
4
- export declare const codexQueueEligible: (id: string, env?: NodeJS.ProcessEnv) => Promise<boolean>;
5
- export declare const recordCodexBoundary: (id: string, event: "Stop" | "non-stop" | "start" | "end", env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<void>;
6
- export declare const clearCodexBoundary: (id: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<void>;
@@ -4,7 +4,3 @@ import { createCodexBoundaryStore } from './packages/agent-delivery/src/codex-bo
4
4
  export function codexBoundaryStore(env = process.env) {
5
5
  return createCodexBoundaryStore(env.SQUARE_CODEX_BOUNDARIES || path.join(os.homedir(), '.square', 'codex-boundaries.json'));
6
6
  }
7
- export const readCodexBoundary = (id, env) => codexBoundaryStore(env).read(id);
8
- export const codexQueueEligible = (id, env) => codexBoundaryStore(env).queueEligible(id);
9
- export const recordCodexBoundary = (id, event, env, signal) => codexBoundaryStore(env).record(id, event, signal);
10
- export const clearCodexBoundary = (id, env, signal) => codexBoundaryStore(env).clear(id, signal);
@@ -1,12 +1,9 @@
1
- import { type Owner } from './owner.js';
2
- import type { PendingMembership } from './model.js';
3
- import type { WakeTransports } from './notifications.js';
4
- export interface CodexHookInput {
5
- session_id?: unknown;
6
- hook_event_name?: unknown;
7
- cwd?: unknown;
8
- }
1
+ import { type CodexHookInput } from './packages/agent-delivery/src/codex-hook.js';
9
2
  export type HookTrace = (line: string) => void;
10
3
  export declare const CODEX_HOOK_BUDGET_MS = 4500;
11
- export declare function codexHookResponse(input: CodexHookInput, lookup?: (identity: Owner, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<PendingMembership[]> | PendingMembership[], env?: NodeJS.ProcessEnv, deliveryTransports?: WakeTransports, trace?: HookTrace): Promise<object | undefined>;
12
- export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
4
+ /** sink resolves only after stdout has accepted the complete hook response. */
5
+ export declare function codexHookResponse(value: unknown, sink: ((text: string) => void | Promise<void>) | undefined, env?: NodeJS.ProcessEnv, trace?: HookTrace): Promise<object | undefined>;
6
+ export declare function parseCodexHookText(text: string): CodexHookInput | undefined;
7
+ /** The caller owns stdout; no receipt is written when no output sink is supplied. */
8
+ export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv, sink?: (text: string) => void | Promise<void>): Promise<string>;
9
+ export declare function runCodexInterrupt(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
@@ -1,49 +1,85 @@
1
- import { handleCodexHook } from './packages/agent-delivery/src/codex-hook.js';
1
+ import { handleCodexHook, parseCodexHook } from './packages/agent-delivery/src/codex-hook.js';
2
2
  import { presentPendingAtBoundary } from './boundary-presentation.js';
3
- import { sessionInbox } from './inbox.js';
3
+ import { DeliveryLedger } from './delivery-ledger.js';
4
4
  import { harnessOwner } from './owner.js';
5
- import { detachSessionSeats, restoreSessionSeats } from './session-lifecycle.js';
5
+ import { attachDeliverySessionRoutes, detachDeliverySession } from './session-lifecycle.js';
6
6
  import { codexBoundaryStore } from './codex-boundary-state.js';
7
7
  import { codexWakeAddress } from './codex-address.js';
8
- import { PRIVILEGED_HOOK_BUDGET_MS, sweepPrivilegedPending } from './notifications.js';
9
8
  export const CODEX_HOOK_BUDGET_MS = 4500;
10
- const defaultTrace = (line) => { process.stderr.write(`${line}\n`); };
11
- export async function codexHookResponse(input, lookup = sessionInbox, env = process.env, deliveryTransports, trace = defaultTrace) {
9
+ const defaultTrace = line => { process.stderr.write(`${line}\n`); };
10
+ /** sink resolves only after stdout has accepted the complete hook response. */
11
+ export async function codexHookResponse(value, sink, env = process.env, trace = defaultTrace) {
12
+ const input = parseCodexHook(value);
13
+ if (!input)
14
+ return;
12
15
  const configured = Number(env.SQUARE_CODEX_HOOK_BUDGET_MS ?? CODEX_HOOK_BUDGET_MS);
13
- const startedAt = Date.now();
14
- let mark = startedAt;
15
- const timeoutMs = Number.isFinite(configured) && configured > 0 ? configured : CODEX_HOOK_BUDGET_MS;
16
+ const timeoutMs = Math.min(input.hook_event_name === 'Interrupt' || input.hook_event_name === 'SessionEnd' ? 2500 : CODEX_HOOK_BUDGET_MS, Number.isFinite(configured) && configured > 0 ? configured : CODEX_HOOK_BUDGET_MS);
16
17
  const signal = AbortSignal.timeout(timeoutMs);
17
- if ((input.hook_event_name === 'SessionStart' || input.hook_event_name === 'SessionResume')
18
- && typeof input.session_id === 'string' && input.session_id.trim()) {
19
- await restoreSessionSeats(harnessOwner({ channel: 'codex', session: input.session_id }), env, codexWakeAddress(input.session_id, env), signal);
18
+ const owner = harnessOwner({ channel: 'codex', session: input.session_id });
19
+ // Capture before any asynchronous boundary update; a replacement attachment
20
+ // must never inherit this callback's cancellation or detachment.
21
+ let generation;
22
+ if (input.hook_event_name === 'SessionEnd' || input.hook_event_name === 'Interrupt') {
23
+ const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
24
+ try {
25
+ generation = (await ledger.session(owner))?.generation;
26
+ }
27
+ finally {
28
+ await ledger.close();
29
+ }
20
30
  }
21
- return handleCodexHook(input, {
22
- boundaries: codexBoundaryStore(env),
23
- timeoutMs,
24
- signal,
25
- sweepTimeoutMs: PRIVILEGED_HOOK_BUDGET_MS,
26
- pull: (id, signal) => presentPendingAtBoundary(harnessOwner({ channel: 'codex', session: id }), (context) => context, lookup, env, signal),
27
- sweep: (cwd, deadline, signal) => sweepPrivilegedPending(cwd ?? process.cwd(), env, deliveryTransports, deadline, signal),
28
- end: async (id) => { await detachSessionSeats('codex', id, env); },
29
- trace: (name, detail = '') => {
30
- const now = Date.now();
31
- const durationMs = now - mark;
32
- mark = now;
33
- trace(`square-codex-hook: stage=${name} durationMs=${durationMs} totalMs=${now - startedAt}${detail ? ` ${detail}` : ''}`);
31
+ const result = await handleCodexHook(input, {
32
+ boundaries: codexBoundaryStore(env), timeoutMs, signal,
33
+ pull: async (_sessionId, pullSignal, event) => {
34
+ if (!sink)
35
+ return;
36
+ return presentPendingAtBoundary(owner, async (context) => {
37
+ await sink(`${JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: context } })}\n`);
38
+ return context;
39
+ }, env, pullSignal);
34
40
  },
41
+ end: async () => { if (generation)
42
+ await detachDeliverySession(owner, generation, env); },
43
+ interrupt: async () => {
44
+ if (!generation || signal.aborted)
45
+ return;
46
+ const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
47
+ try {
48
+ await ledger.cancel({ owner, generation });
49
+ }
50
+ finally {
51
+ await ledger.close();
52
+ }
53
+ },
54
+ trace: name => trace(`square-codex-hook: stage=${name}`),
35
55
  });
56
+ // Disarm before rotating the native attachment, even if there is no old session.
57
+ if (input.hook_event_name === 'SessionStart' && !signal.aborted)
58
+ await attachDeliverySessionRoutes(owner, env, [codexWakeAddress(input.session_id, env)], signal);
59
+ return result;
36
60
  }
37
- export async function runCodexHook(inputText, env = process.env) {
38
- let input;
61
+ export function parseCodexHookText(text) {
39
62
  try {
40
- input = JSON.parse(inputText);
63
+ return parseCodexHook(JSON.parse(text));
41
64
  }
42
65
  catch {
43
- return '';
66
+ return;
67
+ }
68
+ }
69
+ /** The caller owns stdout; no receipt is written when no output sink is supplied. */
70
+ export async function runCodexHook(inputText, env = process.env, sink) {
71
+ await codexHookResponse(parseCodexHookText(inputText), sink, env);
72
+ return '';
73
+ }
74
+ export async function runCodexInterrupt(inputText, env = process.env) {
75
+ const input = parseCodexHookText(inputText);
76
+ if (input?.hook_event_name === 'Interrupt') {
77
+ try {
78
+ await codexHookResponse(input, undefined, env);
79
+ }
80
+ catch {
81
+ throw new Error('Codex interrupt unavailable.');
82
+ }
44
83
  }
45
- if (input === null || typeof input !== 'object')
46
- return '';
47
- const result = await codexHookResponse(input, sessionInbox, env);
48
- return result === undefined ? '' : `${JSON.stringify(result)}\n`;
84
+ return '';
49
85
  }
@@ -1,15 +1,14 @@
1
- import { type CodexCliAgent, type CodexCliConnectOptions } from './packages/agent-delivery/src/index.js';
2
- export { sendCodexQueue, CodexQueueSendError } from './packages/agent-delivery/src/codex-queue.js';
3
- export type { CodexQueueRequest, CodexQueueFailureKind } from './packages/agent-delivery/src/codex-queue.js';
4
- import type { WakeRequest, WakeOutcome, WakeTransportPort } from './channel-transport.js';
5
- import type { WakeRoute } from './channel-values.js';
1
+ import { type CodexStopEpoch } from './codex-boundary-state.js';
2
+ import type { DeliveryCard, DeliveryTransport, DeliveryTransportContext, TransportOutcome } from './delivery-ledger.js';
6
3
  export interface CodexQueueTransportOptions {
7
- env?: NodeJS.ProcessEnv;
8
- connect?: (options: CodexCliConnectOptions) => Promise<CodexCliAgent>;
4
+ readonly epoch: CodexStopEpoch;
5
+ readonly address: Readonly<Record<string, string>>;
6
+ readonly env?: NodeJS.ProcessEnv;
9
7
  }
10
- export declare class CodexQueueTransport implements WakeTransportPort {
8
+ /** Instantiated only by the receiver, for one captured Stop epoch. */
9
+ export declare class CodexQueueTransport implements DeliveryTransport {
11
10
  private readonly opts;
12
- constructor(opts?: CodexQueueTransportOptions);
13
- attempt(request: WakeRequest, timeoutMs: number, beforeSend: () => Promise<boolean>): Promise<WakeOutcome>;
11
+ readonly kind = "codex-queue";
12
+ constructor(opts: CodexQueueTransportOptions);
13
+ send(card: DeliveryCard, address: Readonly<Record<string, string>>, context: DeliveryTransportContext): Promise<TransportOutcome>;
14
14
  }
15
- export type { WakeRoute };