@astrosheep/square 0.3.106 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/activity-feed.js +1 -1
  2. package/dist/artifact-store.d.ts +41 -0
  3. package/dist/artifact-store.js +272 -0
  4. package/dist/artifact.d.ts +6 -12
  5. package/dist/artifact.js +43 -344
  6. package/dist/attention-presentation.d.ts +13 -7
  7. package/dist/attention-presentation.js +29 -68
  8. package/dist/boundary-presentation.d.ts +3 -16
  9. package/dist/boundary-presentation.js +29 -109
  10. package/dist/card-presentation.d.ts +1 -0
  11. package/dist/card-presentation.js +2 -0
  12. package/dist/catch-decisions.d.ts +2 -2
  13. package/dist/catch-decisions.js +7 -3
  14. package/dist/catch-selection.d.ts +8 -0
  15. package/dist/catch-selection.js +5 -0
  16. package/dist/claude-delivery.d.ts +14 -12
  17. package/dist/claude-delivery.js +42 -142
  18. package/dist/claude-mcp-context.js +2 -2
  19. package/dist/claude-mod.js +28 -140
  20. package/dist/claude-transport.d.ts +5 -0
  21. package/dist/claude-transport.js +34 -0
  22. package/dist/cli/context.d.ts +2 -2
  23. package/dist/cli/context.js +7 -10
  24. package/dist/cli/maintenance-commands.d.ts +0 -4
  25. package/dist/cli/maintenance-commands.js +2 -29
  26. package/dist/cli/observation-commands.d.ts +2 -0
  27. package/dist/cli/observation-commands.js +12 -2
  28. package/dist/cli/program.js +1 -1
  29. package/dist/cli/registry.js +4 -3
  30. package/dist/codex-address.d.ts +2 -2
  31. package/dist/codex-boundary-state.d.ts +1 -5
  32. package/dist/codex-boundary-state.js +0 -4
  33. package/dist/codex-hook.d.ts +7 -10
  34. package/dist/codex-hook.js +69 -33
  35. package/dist/codex-queue.d.ts +10 -11
  36. package/dist/codex-queue.js +32 -34
  37. package/dist/codex-watch.d.ts +2 -0
  38. package/dist/codex-watch.js +100 -0
  39. package/dist/decisions.js +3 -3
  40. package/dist/delivery-deferral.d.ts +1 -19
  41. package/dist/delivery-deferral.js +1 -29
  42. package/dist/delivery-frame.d.ts +7 -0
  43. package/dist/delivery-frame.js +11 -0
  44. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  45. package/dist/delivery-ledger-sqlite.js +496 -0
  46. package/dist/delivery-ledger.d.ts +196 -0
  47. package/dist/delivery-ledger.js +268 -0
  48. package/dist/delivery-lifetime.d.ts +6 -0
  49. package/dist/delivery-lifetime.js +45 -0
  50. package/dist/harness-links.js +61 -7
  51. package/dist/harness.js +12 -17
  52. package/dist/help.js +3 -6
  53. package/dist/http-delivery.d.ts +5 -9
  54. package/dist/http-delivery.js +20 -79
  55. package/dist/inbox.d.ts +6 -57
  56. package/dist/inbox.js +24 -380
  57. package/dist/index.d.ts +7 -4
  58. package/dist/index.js +5 -2
  59. package/dist/mcp.d.ts +2 -1
  60. package/dist/mcp.js +40 -70
  61. package/dist/message.d.ts +7 -0
  62. package/dist/message.js +23 -0
  63. package/dist/model.d.ts +15 -91
  64. package/dist/model.js +2 -140
  65. package/dist/neutral.d.ts +72 -0
  66. package/dist/neutral.js +27 -0
  67. package/dist/notifications.d.ts +24 -26
  68. package/dist/notifications.js +39 -143
  69. package/dist/operation-control.d.ts +4 -0
  70. package/dist/operation-control.js +20 -0
  71. package/dist/operation-presentation.d.ts +1 -2
  72. package/dist/operation-presentation.js +1 -7
  73. package/dist/owner.d.ts +2 -2
  74. package/dist/owner.js +1 -1
  75. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  76. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  77. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  78. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  79. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  80. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  81. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  82. package/dist/packages/agent-delivery/src/codex.js +41 -4
  83. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  84. package/dist/packages/agent-delivery/src/index.js +1 -1
  85. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  86. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  87. package/dist/paseo-delivery.d.ts +5 -5
  88. package/dist/paseo-delivery.js +19 -99
  89. package/dist/paseo.d.ts +1 -1
  90. package/dist/paseo.js +1 -1
  91. package/dist/plugins/opencode/index.d.ts +5 -3
  92. package/dist/plugins/opencode/index.js +99 -21
  93. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  94. package/dist/plugins/opencode-shared/delivery.js +109 -0
  95. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  96. package/dist/plugins/opencode-v1/index.js +41 -0
  97. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  98. package/dist/plugins/opencode-v1/runtime.js +152 -0
  99. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  100. package/dist/plugins/opencode-v1/transport.js +50 -0
  101. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  102. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  103. package/dist/ports.d.ts +33 -214
  104. package/dist/presentation.d.ts +5 -5
  105. package/dist/presentation.js +1 -0
  106. package/dist/runtime.js +2 -2
  107. package/dist/search.js +1 -1
  108. package/dist/session-lifecycle.d.ts +8 -7
  109. package/dist/session-lifecycle.js +40 -42
  110. package/dist/sqlite-framing.d.ts +8 -0
  111. package/dist/sqlite-framing.js +31 -0
  112. package/dist/sqlite-square.d.ts +16 -26
  113. package/dist/sqlite-square.js +199 -262
  114. package/dist/sqlite-values.d.ts +21 -0
  115. package/dist/sqlite-values.js +78 -0
  116. package/dist/square-api.d.ts +1 -1
  117. package/dist/square-api.js +5 -8
  118. package/dist/square-core.d.ts +0 -1
  119. package/dist/square-domain.d.ts +12 -10
  120. package/dist/square-domain.js +105 -223
  121. package/dist/square-facade.d.ts +25 -76
  122. package/dist/square-location.d.ts +6 -1
  123. package/dist/square-location.js +1 -1
  124. package/dist/square-post.d.ts +53 -0
  125. package/dist/square-post.js +4 -0
  126. package/dist/square-posting.d.ts +13 -0
  127. package/dist/square-posting.js +174 -0
  128. package/dist/square-producer-sqlite.d.ts +11 -0
  129. package/dist/square-producer-sqlite.js +210 -0
  130. package/dist/square-receive.d.ts +2 -3
  131. package/dist/square-sqlite.d.ts +33 -42
  132. package/dist/square-sqlite.js +236 -768
  133. package/dist/square-tools.d.ts +8 -0
  134. package/dist/square-tools.js +39 -0
  135. package/dist/square-values.d.ts +27 -0
  136. package/dist/square-values.js +90 -0
  137. package/dist/views.d.ts +6 -6
  138. package/dist/views.js +14 -9
  139. package/dist/wake-sink.d.ts +4 -11
  140. package/dist/wake-sink.js +8 -32
  141. package/dist/wake-transports.d.ts +4 -0
  142. package/dist/wake-transports.js +11 -0
  143. package/examples/symmetric-domains.mjs +29 -249
  144. package/guides/change-waits.md +13 -53
  145. package/guides/claude-native.md +19 -172
  146. package/guides/codex-delivery.md +55 -78
  147. package/guides/delivery-primitives.md +48 -207
  148. package/guides/operation-cancellation.md +2 -5
  149. package/guides/participant-ownership.md +25 -89
  150. package/guides/pi-native-delivery.md +19 -56
  151. package/guides/pi-tools.md +6 -8
  152. package/guides/row-runtime-delivery-evidence.md +2 -0
  153. package/guides/row-storage-design.md +19 -44
  154. package/guides/square-api.md +113 -538
  155. package/package.json +8 -8
  156. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  157. package/plugins/claude/hooks/hooks.json +1 -1
  158. package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
  159. package/plugins/claude/types.d.ts +1 -1
  160. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  161. package/plugins/codex/hooks/hooks.json +11 -23
  162. package/plugins/opencode/index.ts +91 -23
  163. package/plugins/opencode-shared/delivery.ts +91 -0
  164. package/plugins/opencode-v1/index.ts +36 -0
  165. package/plugins/opencode-v1/runtime.ts +134 -0
  166. package/plugins/opencode-v1/transport.ts +41 -0
  167. package/plugins/paseo/OVERVIEW.md +5 -5
  168. package/plugins/paseo/README.md +7 -6
  169. package/plugins/paseo/client/markdown.tsx +5 -5
  170. package/plugins/paseo/client/square-message.tsx +28 -24
  171. package/plugins/paseo/client/transform.ts +8 -5
  172. package/plugins/paseo/shared/card-presentation.ts +16 -0
  173. package/plugins/paseo/shared/message.ts +6 -11
  174. package/plugins/pi/square-pi.js +167 -451
  175. package/dist/channel.d.ts +0 -74
  176. package/dist/channel.js +0 -92
  177. package/dist/claude-delivery-status.d.ts +0 -13
  178. package/dist/claude-delivery-status.js +0 -46
  179. package/dist/cmd/notify-once.d.ts +0 -2
  180. package/dist/cmd/notify-once.js +0 -29
  181. package/dist/delivery-attempts.d.ts +0 -52
  182. package/dist/delivery-attempts.js +0 -73
  183. package/dist/delivery-effects.d.ts +0 -139
  184. package/dist/delivery-effects.js +0 -163
  185. package/dist/delivery-health.d.ts +0 -51
  186. package/dist/delivery-health.js +0 -173
  187. package/dist/delivery-operations.d.ts +0 -9
  188. package/dist/delivery-operations.js +0 -168
  189. package/dist/delivery-primitives.d.ts +0 -5
  190. package/dist/delivery-primitives.js +0 -3
  191. package/dist/delivery.d.ts +0 -45
  192. package/dist/delivery.js +0 -106
  193. package/dist/plugins/opencode/delivery.d.ts +0 -19
  194. package/dist/plugins/opencode/delivery.js +0 -238
  195. package/dist/presentation-operations.d.ts +0 -18
  196. package/dist/presentation-operations.js +0 -86
  197. package/dist/square-index.d.ts +0 -16
  198. package/dist/square-index.js +0 -106
  199. package/dist/square-legacy-framing.d.ts +0 -11
  200. package/dist/square-legacy-framing.js +0 -64
  201. package/dist/square-migration-host-ledger.d.ts +0 -26
  202. package/dist/square-migration-host-ledger.js +0 -169
  203. package/dist/square-migration.d.ts +0 -30
  204. package/dist/square-migration.js +0 -599
  205. package/guides/migration.md +0 -156
  206. package/plugins/opencode/delivery.ts +0 -211
@@ -0,0 +1,20 @@
1
+ import { SquareError } from './neutral.js';
2
+ export function checkControl(control) {
3
+ if (control?.signal?.aborted)
4
+ throw control.signal.reason ?? new Error('Square operation was cancelled');
5
+ }
6
+ export function squareClock(env, clock) {
7
+ if (clock !== undefined)
8
+ return clock;
9
+ if (env.SQUARE_NOW_MS === undefined)
10
+ return Date.now;
11
+ const value = Number.parseInt(env.SQUARE_NOW_MS, 10);
12
+ if (!Number.isFinite(value))
13
+ throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
14
+ return () => value;
15
+ }
16
+ export function failure(error, detail) {
17
+ const normalized = error instanceof Error ? error : new Error(String(error), { cause: error });
18
+ Object.assign(normalized, { operation: detail });
19
+ return normalized;
20
+ }
@@ -30,9 +30,8 @@ export interface FailureReport {
30
30
  readonly code: string;
31
31
  readonly message: string;
32
32
  readonly facts?: {
33
- readonly reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'migration_required' | 'invalid_reply' | 'invalid_done_body' | 'needs_seat';
33
+ readonly reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'invalid_reply' | 'invalid_done_body' | 'needs_seat';
34
34
  readonly activityId?: string;
35
- readonly ledgerRoot?: string;
36
35
  readonly activityExists?: boolean;
37
36
  };
38
37
  readonly operation?: OperationFailureDetail;
@@ -335,13 +335,7 @@ export function renderFailureLines(failure, options = {}) {
335
335
  const reason = facts?.reason;
336
336
  const marked = reason === 'never_joined' || reason === 'not_standing';
337
337
  const lines = marked ? failure.message.split('\n').map((line) => `✕ ${line}`) : [failure.message];
338
- if (reason === 'migration_required') {
339
- if (facts?.ledgerRoot !== undefined)
340
- lines.push('square ledger migrate', 'square ledger verify');
341
- else if (squarePath !== undefined)
342
- lines.push(`${commandPrefix(squarePath)} migrate`);
343
- }
344
- else if (squarePath !== undefined && name !== undefined && reason === 'needs_seat') {
338
+ if (reason === 'needs_seat' && squarePath !== undefined && name !== undefined) {
345
339
  lines.splice(0, 1, `✕ ${failure.message}`);
346
340
  lines.push(joinRecoveryCommand(squarePath, name, owner));
347
341
  }
package/dist/owner.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type Owner } from './model.js';
2
- export type { Owner } from './model.js';
1
+ import { type Owner } from './neutral.js';
2
+ export type { Owner } from './neutral.js';
3
3
  export interface HarnessCoordinate {
4
4
  readonly channel: string;
5
5
  readonly session: string;
package/dist/owner.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import os from 'node:os';
2
- import { validateOwner } from './model.js';
2
+ import { validateOwner } from './neutral.js';
3
3
  export function harnessOwner(coordinate) {
4
4
  const channel = nonempty(coordinate.channel, 'channel');
5
5
  const session = nonempty(coordinate.session, 'session');
@@ -11,6 +11,8 @@ export interface ClaudeNativeTarget {
11
11
  export interface ClaudeNativeControl {
12
12
  readonly deadline: number;
13
13
  readonly signal?: AbortSignal;
14
+ /** Final asynchronous eligibility check after connecting, before writing bytes. */
15
+ readonly beforeSend?: () => Promise<boolean>;
14
16
  }
15
17
  export type NativeWriteResult = {
16
18
  readonly outcome: 'written';
@@ -43,6 +43,11 @@ export async function writeClaudeNative(target, text, control) {
43
43
  throw new LocalEndpointError('timeout');
44
44
  socket = await connectLocal(endpoint, { signal: scope.signal });
45
45
  scope.check();
46
+ if (Date.now() >= control.deadline)
47
+ throw new LocalEndpointError('timeout');
48
+ if (control.beforeSend && !await scope.wait(control.beforeSend()))
49
+ return { outcome: 'unavailable', code: 'aborted', message: 'Native inbox write suppressed.' };
50
+ scope.check();
46
51
  if (Date.now() >= control.deadline)
47
52
  throw new LocalEndpointError('timeout');
48
53
  // A receiver that never drains must not hold the caller past its deadline.
@@ -1,13 +1,18 @@
1
+ export interface CodexStopEpoch {
2
+ readonly token: string;
3
+ readonly turnId: string;
4
+ }
1
5
  export interface CodexBoundary {
2
- readonly lastStop: number;
3
- readonly lastNonStop: number;
6
+ readonly stop?: CodexStopEpoch;
4
7
  readonly ended?: true;
5
8
  }
6
9
  export interface CodexBoundaryStore {
7
10
  read(sessionId: string): Promise<CodexBoundary | undefined>;
8
- queueEligible(sessionId: string): Promise<boolean>;
9
- record(sessionId: string, event: 'Stop' | 'non-stop' | 'start' | 'end', signal?: AbortSignal): Promise<void>;
10
- clear(sessionId: string, signal?: AbortSignal): Promise<void>;
11
+ queueEligible(sessionId: string, epoch?: CodexStopEpoch): Promise<boolean>;
12
+ record(sessionId: string, event: 'Stop' | 'non-stop' | 'start' | 'end', options?: {
13
+ readonly signal?: AbortSignal;
14
+ readonly turnId?: string;
15
+ }): Promise<void>;
11
16
  }
12
- /** Caller-owned observation file, shared by independent hook and sender processes. */
17
+ /** Receiver-owned idle authority. Old or unverifiable records never arm a send. */
13
18
  export declare function createCodexBoundaryStore(file: string): CodexBoundaryStore;
@@ -1,8 +1,9 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { withFileLock } from './file-lock.js';
4
- const empty = () => ({ v: 1, nextSequence: 0, threads: Object.create(null) });
5
- /** Caller-owned observation file, shared by independent hook and sender processes. */
4
+ const empty = () => ({ v: 2, threads: Object.create(null) });
5
+ const identity = (value) => typeof value === 'string' && value.trim().length > 0 && !value.includes('\0');
6
+ /** Receiver-owned idle authority. Old or unverifiable records never arm a send. */
6
7
  export function createCodexBoundaryStore(file) {
7
8
  const load = async () => {
8
9
  let raw;
@@ -16,16 +17,14 @@ export function createCodexBoundaryStore(file) {
16
17
  }
17
18
  try {
18
19
  const value = JSON.parse(raw);
19
- if (value?.v !== 1 || !Number.isSafeInteger(value.nextSequence) || value.nextSequence < 0 ||
20
- !value.threads || typeof value.threads !== 'object')
20
+ if (value?.v !== 2 || !value.threads || typeof value.threads !== 'object' || Array.isArray(value.threads))
21
21
  return empty();
22
22
  const result = empty();
23
- result.nextSequence = value.nextSequence;
24
- for (const [id, boundary] of Object.entries(value.threads)) {
25
- const item = boundary;
26
- if (item && Number.isSafeInteger(item.lastStop) && item.lastStop >= 0 &&
27
- Number.isSafeInteger(item.lastNonStop) && item.lastNonStop >= 0)
28
- result.threads[id] = item;
23
+ for (const [id, item] of Object.entries(value.threads)) {
24
+ if (!item || typeof item !== 'object')
25
+ continue;
26
+ result.threads[id] = item.ended === true ? { ended: true }
27
+ : identity(item.stop?.token) && identity(item.stop?.turnId) ? { stop: { token: item.stop.token, turnId: item.stop.turnId } } : {};
29
28
  }
30
29
  return result;
31
30
  }
@@ -33,43 +32,35 @@ export function createCodexBoundaryStore(file) {
33
32
  return empty();
34
33
  }
35
34
  };
36
- const update = (change, signal) => withFileLock(`${file}.lock`, { retryMs: 10, signal }, async () => {
37
- const state = await load();
38
- change(state);
39
- signal?.throwIfAborted();
40
- const temporary = `${file}.${randomUUID()}.tmp`;
41
- try {
42
- await fs.writeFile(temporary, JSON.stringify(state), { mode: 0o600 });
43
- await fs.rename(temporary, file);
44
- }
45
- finally {
46
- await fs.unlink(temporary).catch(() => { });
47
- }
48
- });
49
35
  const read = async (id) => id ? (await load()).threads[id] : undefined;
50
36
  return {
51
37
  read,
52
- async queueEligible(id) {
38
+ async queueEligible(id, epoch) {
53
39
  const boundary = await read(id);
54
- return boundary !== undefined && boundary.ended !== true && boundary.lastStop > boundary.lastNonStop;
40
+ return boundary?.ended !== true && boundary?.stop !== undefined &&
41
+ (epoch === undefined || boundary.stop.token === epoch.token && boundary.stop.turnId === epoch.turnId);
55
42
  },
56
- async record(id, event, signal) {
57
- if (!id)
43
+ async record(id, event, options = {}) {
44
+ if (!identity(id))
58
45
  return;
59
- await update((state) => {
60
- const previous = state.threads[id] ?? { lastStop: 0, lastNonStop: 0 };
61
- // A late Stop/tool callback must not reopen an ended session.
62
- if (previous.ended && event !== 'start')
46
+ await withFileLock(`${file}.lock`, { retryMs: 10, signal: options.signal }, async () => {
47
+ const state = await load();
48
+ // A late callback cannot reopen an ended native session.
49
+ if (state.threads[id]?.ended && event !== 'start')
63
50
  return;
64
- state.nextSequence += 1;
65
- state.threads[id] = event === 'start'
66
- ? { lastStop: 0, lastNonStop: state.nextSequence }
67
- : event === 'end' ? { ...previous, lastNonStop: state.nextSequence, ended: true }
68
- : event === 'Stop' ? { ...previous, lastStop: state.nextSequence }
69
- : { ...previous, lastNonStop: state.nextSequence };
70
- }, signal);
51
+ state.threads[id] = event === 'end' ? { ended: true }
52
+ : event === 'Stop' && identity(options.turnId) ? { stop: { token: randomUUID(), turnId: options.turnId } } : {};
53
+ options.signal?.throwIfAborted();
54
+ const temporary = `${file}.${randomUUID()}.tmp`;
55
+ try {
56
+ await fs.writeFile(temporary, JSON.stringify(state), { mode: 0o600 });
57
+ options.signal?.throwIfAborted();
58
+ await fs.rename(temporary, file);
59
+ }
60
+ finally {
61
+ await fs.unlink(temporary).catch(() => { });
62
+ }
63
+ });
71
64
  },
72
- async clear(id, signal) { if (id)
73
- await update((state) => { delete state.threads[id]; }, signal); },
74
65
  };
75
66
  }
@@ -1,26 +1,31 @@
1
1
  import type { CodexBoundaryStore } from './codex-boundary.js';
2
2
  export { createCodexBoundaryStore } from './codex-boundary.js';
3
- export type { CodexBoundary, CodexBoundaryStore } from './codex-boundary.js';
3
+ export type { CodexBoundary, CodexBoundaryStore, CodexStopEpoch } from './codex-boundary.js';
4
+ export { codexIdleAtStop } from './codex.js';
5
+ export type CodexActiveEvent = 'UserPromptSubmit' | 'PreToolUse' | 'PostToolUse';
6
+ export type CodexHookEvent = CodexActiveEvent | 'SessionStart' | 'SessionEnd' | 'Stop' | 'Interrupt';
7
+ export interface CodexHookInput {
8
+ readonly session_id: string;
9
+ readonly hook_event_name: CodexHookEvent;
10
+ readonly turn_id?: string;
11
+ }
4
12
  export interface CodexHookResponse {
5
13
  readonly hookSpecificOutput: {
6
- readonly hookEventName: 'PostToolUse';
14
+ readonly hookEventName: CodexActiveEvent;
7
15
  readonly additionalContext: string;
8
16
  };
9
17
  }
10
18
  export interface CodexHookOptions {
11
19
  readonly boundaries: CodexBoundaryStore;
12
- /** The host owns pending text, claims and cancellation. Honor signal before presenting;
13
- * if cancelled before an effect, release custody and resolve undefined or reject.
14
- * After presenting, finish any cancellation-independent settlement and return its text,
15
- * even if signal aborts meanwhile. This callback is awaited, never raced or abandoned. */
16
- readonly pull?: (sessionId: string, signal: AbortSignal) => Promise<string | undefined>;
17
- /** Dispatch the host's pending work; Stop merely makes queue eligible. */
18
- readonly sweep?: (cwd: string | undefined, deadline: number, signal: AbortSignal) => Promise<void>;
20
+ /** The host owns claims and cancellation. After an external effect, finish its
21
+ * cancellation-independent settlement and retain the output. Never abandon it. */
22
+ readonly pull?: (sessionId: string, signal: AbortSignal, event: CodexActiveEvent) => Promise<string | undefined>;
19
23
  readonly end?: (sessionId: string, signal: AbortSignal) => Promise<void>;
24
+ readonly interrupt?: (sessionId: string, signal: AbortSignal) => Promise<void>;
20
25
  readonly trace?: (stage: string, detail?: string) => void;
21
26
  readonly timeoutMs?: number;
22
- readonly sweepTimeoutMs?: number;
23
27
  readonly signal?: AbortSignal;
24
28
  }
25
- /** Stateless hook protocol. No message source, session identity or service is invented here. */
26
- export declare function handleCodexHook(input: unknown, options: CodexHookOptions): Promise<CodexHookResponse | undefined>;
29
+ export declare function parseCodexHook(input: unknown): CodexHookInput | undefined;
30
+ /** Native synchronous boundaries; Stop only records authority for its async receiver. */
31
+ export declare function handleCodexHook(value: unknown, options: CodexHookOptions): Promise<CodexHookResponse | undefined>;
@@ -1,57 +1,44 @@
1
1
  import { deadline, validSignal, validTimeout } from './deadline.js';
2
2
  import { isRecord } from './guards.js';
3
3
  export { createCodexBoundaryStore } from './codex-boundary.js';
4
- /** Stateless hook protocol. No message source, session identity or service is invented here. */
5
- export async function handleCodexHook(input, options) {
6
- if (!isRecord(input) || typeof input.session_id !== 'string' || !input.session_id.trim())
4
+ export { codexIdleAtStop } from './codex.js';
5
+ export function parseCodexHook(input) {
6
+ const identity = (value) => typeof value === 'string' && value.trim().length > 0 && !value.includes('\0');
7
+ if (!isRecord(input) || !identity(input.session_id) ||
8
+ !['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'SessionEnd', 'Stop', 'Interrupt'].includes(input.hook_event_name) ||
9
+ (['Stop', 'Interrupt'].includes(input.hook_event_name) && !identity(input.turn_id)))
7
10
  return;
8
- const event = input.hook_event_name;
9
- if (!['SessionStart', 'SessionResume', 'UserPromptSubmit', 'SessionEnd', 'PostToolUse', 'Stop'].includes(event))
11
+ return input;
12
+ }
13
+ /** Native synchronous boundaries; Stop only records authority for its async receiver. */
14
+ export async function handleCodexHook(value, options) {
15
+ const input = parseCodexHook(value);
16
+ if (!input)
10
17
  return;
11
- if (!validTimeout(options.timeoutMs) || !validTimeout(options.sweepTimeoutMs) || !validSignal(options.signal)) {
18
+ if (!validTimeout(options.timeoutMs) || !validSignal(options.signal))
12
19
  throw new TypeError('Invalid hook control.');
13
- }
14
20
  const scope = deadline(options.timeoutMs ?? 4500, options.signal);
15
- const sweepDeadline = Date.now() + Math.min(options.sweepTimeoutMs ?? 3000, options.timeoutMs ?? 4500);
16
- const id = input.session_id;
21
+ const event = input.hook_event_name, id = input.session_id;
17
22
  try {
18
23
  scope.check();
19
- if (event === 'SessionEnd') {
20
- await scope.wait(options.boundaries.record(id, 'end', scope.signal));
21
- await scope.wait(options.end?.(id, scope.signal) ?? Promise.resolve()).catch(() => { });
22
- return;
23
- }
24
- await scope.wait(options.boundaries.record(id, event === 'SessionStart' || event === 'SessionResume' ? 'start' : event === 'Stop' ? 'Stop' : 'non-stop', scope.signal));
24
+ await scope.wait(options.boundaries.record(id, event === 'SessionStart' ? 'start' : event === 'SessionEnd' ? 'end' : event === 'Stop' ? 'Stop' : 'non-stop', { signal: scope.signal, turnId: input.turn_id }));
25
25
  options.trace?.('boundary-record');
26
- if (event !== 'Stop' && event !== 'PostToolUse')
26
+ if (event === 'SessionEnd' || event === 'Interrupt') {
27
+ await (event === 'SessionEnd' ? options.end : options.interrupt)?.(id, scope.signal);
27
28
  return;
28
- let context;
29
- if (event === 'PostToolUse') {
30
- try {
31
- // A host's successful presentation settlement may outlive this signal. Retain
32
- // its output instead of leaving background work to consume it without a response.
33
- context = await options.pull?.(id, scope.signal);
34
- options.trace?.('presentation');
35
- }
36
- catch (error) {
37
- options.trace?.('presentation', `error=${error instanceof Error ? error.name : 'Error'}`);
38
- }
39
29
  }
40
- if (!scope.signal.aborted && Date.now() < sweepDeadline) {
41
- await scope.wait(options.sweep?.(typeof input.cwd === 'string' ? input.cwd : undefined, sweepDeadline, scope.signal) ?? Promise.resolve()).catch(() => { });
30
+ if (event !== 'UserPromptSubmit' && event !== 'PreToolUse' && event !== 'PostToolUse')
31
+ return;
32
+ try {
33
+ const context = await options.pull?.(id, scope.signal, event);
34
+ options.trace?.('presentation');
35
+ if (context !== undefined)
36
+ return { hookSpecificOutput: { hookEventName: event, additionalContext: context } };
42
37
  }
43
- options.trace?.('sweep', scope.signal.aborted ? 'aborted=true' : '');
44
- // A successful pull may already have committed host presentation evidence.
45
- // A later, unrelated sweep timeout must not discard its completed output.
46
- if (context !== undefined) {
47
- return { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } };
38
+ catch (error) {
39
+ options.trace?.('presentation', `error=${error instanceof Error ? error.name : 'Error'}`);
48
40
  }
49
41
  }
50
- catch (error) {
51
- if (scope.signal.aborted)
52
- throw scope.signal.reason;
53
- throw error;
54
- }
55
42
  finally {
56
43
  scope.close();
57
44
  }
@@ -1,4 +1,7 @@
1
1
  import type { CodexAppServerConnectOptions, CodexDeliveryResult, CodexSendTextOptions, CodexTarget } from './types.js';
2
+ /** Newest-last live history is verified for Codex 0.166.0 only. A sampled read
3
+ * cannot make a later external queue atomic with native turn starts. */
4
+ export declare function codexIdleAtStop(options: CodexAppServerConnectOptions, turnId: string): Promise<boolean>;
2
5
  /** Address an existing thread on an already running app-server. Never starts a
3
6
  * service, resumes a thread, or claims ownership of a standalone CLI session. */
4
7
  export declare function connectCodex(options: CodexAppServerConnectOptions): Promise<CodexTarget>;
@@ -143,6 +143,7 @@ class Peer {
143
143
  throw new Fault('invalid_response');
144
144
  scope.check();
145
145
  this.socket.send(JSON.stringify({ method: 'initialized' }));
146
+ return result.userAgent;
146
147
  }
147
148
  call(method, params, scope, dispatch) {
148
149
  scope.check();
@@ -239,9 +240,7 @@ async function discoverTurn(peer, metadata, sessionId, scope) {
239
240
  cursors.add(cursor);
240
241
  }
241
242
  }
242
- /** Address an existing thread on an already running app-server. Never starts a
243
- * service, resumes a thread, or claims ownership of a standalone CLI session. */
244
- export async function connectCodex(options) {
243
+ function validatedConnection(options) {
245
244
  if (!isRecord(options) || options.harness !== 'codex' ||
246
245
  (options.transport !== undefined && options.transport !== 'app-server') || !isIdentity(options.sessionId) ||
247
246
  !validTimeout(options.timeoutMs) || !validSignal(options.signal) ||
@@ -250,7 +249,45 @@ export async function connectCodex(options) {
250
249
  (options.token !== undefined && (!isIdentity(options.token) || /[\r\n]/.test(options.token)))) {
251
250
  throw new ConnectionError('invalid_arguments');
252
251
  }
253
- const connection = resolveConnection(options);
252
+ return resolveConnection(options);
253
+ }
254
+ /** Newest-last live history is verified for Codex 0.166.0 only. A sampled read
255
+ * cannot make a later external queue atomic with native turn starts. */
256
+ export async function codexIdleAtStop(options, turnId) {
257
+ const connection = validatedConnection(options);
258
+ if (!isIdentity(turnId))
259
+ return false;
260
+ const scope = deadline(options.timeoutMs, options.signal);
261
+ let peer;
262
+ try {
263
+ peer = await open(connection, scope);
264
+ const version = await peer.initialize(scope);
265
+ // The app-server's originator prefix belongs to the hosting client, which
266
+ // may have initialized before this connection. Fence the native version.
267
+ if (!/^[A-Za-z0-9._-]+\/0\.166\.0(?:\s|$)/.test(version))
268
+ return false;
269
+ const thread = await readThread(peer, options.sessionId, scope, true);
270
+ if (!isRecord(thread.status) || thread.status.type !== 'idle' || !Array.isArray(thread.turns) || !thread.turns.length)
271
+ return false;
272
+ // Validate the whole live list, including absence of an active turn; never
273
+ // fall back to persisted pages or a boolean boundary for missing history.
274
+ if (activeTurn(thread.turns) !== undefined || new Set(thread.turns.map(turn => turn.id)).size !== thread.turns.length)
275
+ return false;
276
+ const newest = thread.turns.at(-1);
277
+ return newest.id === turnId && newest.status === 'completed';
278
+ }
279
+ catch {
280
+ return false;
281
+ }
282
+ finally {
283
+ scope.close();
284
+ await peer?.close();
285
+ }
286
+ }
287
+ /** Address an existing thread on an already running app-server. Never starts a
288
+ * service, resumes a thread, or claims ownership of a standalone CLI session. */
289
+ export async function connectCodex(options) {
290
+ const connection = validatedConnection(options);
254
291
  const scope = deadline(options.timeoutMs, options.signal);
255
292
  let peer;
256
293
  try {
@@ -30,7 +30,9 @@ export type PiAgent = Steerable & Queueable & {
30
30
  readonly harness: 'pi';
31
31
  readonly sessionId: string;
32
32
  };
33
- export type PaseoAgent = Steerable<KeyedSendOptions> & {
33
+ export type PaseoAgent = Steerable<KeyedSendOptions & {
34
+ readonly beforeSend?: () => Promise<boolean>;
35
+ }> & {
34
36
  readonly harness: 'paseo';
35
37
  readonly agentId: string;
36
38
  };
@@ -110,7 +110,7 @@ export async function connect(options) {
110
110
  throw new DeliveryError('invalid_arguments');
111
111
  try {
112
112
  const result = await opened.send(text, spec.keyed
113
- ? { delivery, inputId: id, timeoutMs: options.timeoutMs, signal: options.signal }
113
+ ? { delivery, inputId: id, timeoutMs: options.timeoutMs, signal: options.signal, ...(spec.harness === 'paseo' ? { beforeSend: options.beforeSend } : {}) }
114
114
  : { delivery, timeoutMs: options.timeoutMs, signal: options.signal });
115
115
  if (result.state !== spec.accepted)
116
116
  throw failure(result, 'send', id);
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { deadline, validSignal, validTimeout } from './deadline.js';
2
+ import { deadline, DEFAULT_TIMEOUT_MS, validSignal, validTimeout } from './deadline.js';
3
3
  import { ConnectionError } from './errors.js';
4
4
  import { isIdentity, isRecord } from './guards.js';
5
5
  import { daemonCredential, openPaseoClient, paseoHome, resolvePaseoDaemon, } from './paseo-native.js';
@@ -59,26 +59,35 @@ export async function sendPaseoText(target, text, options = {}) {
59
59
  if (connection === undefined || typeof text !== 'string' || text.length === 0 ||
60
60
  !validTimeout(options.timeoutMs) || !validSignal(options.signal) ||
61
61
  (options.inputId !== undefined && !isIdentity(options.inputId)) ||
62
+ (options.beforeSend !== undefined && typeof options.beforeSend !== 'function') ||
62
63
  (options.delivery !== undefined && options.delivery !== 'steer')) {
63
64
  throw new TypeError('Invalid sendText arguments.');
64
65
  }
65
66
  const inputId = options.inputId ?? randomUUID();
66
67
  const attempt = { harness: 'paseo', agentId: target.agentId, inputId };
67
68
  const scope = deadline(options.timeoutMs, options.signal);
69
+ const until = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
68
70
  const client = await openPaseoClient(connection.target, connection.credential, options.timeoutMs);
69
71
  // The connection is the uncertainty boundary: before it, nothing was written.
70
72
  let connected = false;
73
+ let sent = false;
71
74
  try {
72
75
  scope.check();
73
76
  await scope.wait(client.connect());
74
77
  connected = true;
75
78
  scope.check();
79
+ if (options.beforeSend && !await scope.wait(options.beforeSend()))
80
+ return { ...attempt, state: 'unavailable', code: 'aborted' };
81
+ scope.check();
82
+ if (Date.now() >= until)
83
+ return { ...attempt, state: 'unavailable', code: 'timeout' };
84
+ sent = true;
76
85
  await scope.wait(client.sendAgentMessage(target.agentId, text, { messageId: inputId, activeTurnBehavior: 'steer' }));
77
86
  return { ...attempt, state: 'admitted' };
78
87
  }
79
88
  catch (error) {
80
89
  if (scope.code !== undefined) {
81
- return { ...attempt, state: connected ? 'unknown' : 'unavailable', code: scope.code };
90
+ return { ...attempt, state: sent ? 'unknown' : 'unavailable', code: scope.code };
82
91
  }
83
92
  if (error instanceof Error && error.name === 'DaemonAuthenticationError') {
84
93
  return { ...attempt, state: 'unavailable', code: 'authentication_failed' };
@@ -50,6 +50,7 @@ export interface PaseoSendTextOptions {
50
50
  readonly inputId?: string;
51
51
  readonly timeoutMs?: number;
52
52
  readonly signal?: AbortSignal;
53
+ readonly beforeSend?: () => Promise<boolean>;
53
54
  }
54
55
  export interface CodexSendTextOptions {
55
56
  readonly delivery?: Delivery;
@@ -1,16 +1,16 @@
1
- import type { WakeRequest, WakeOutcome, WakeTransportPort } from './ports.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
  }