@astrosheep/square 0.4.3 → 0.4.5

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 (104) hide show
  1. package/dist/artifact-store.d.ts +1 -1
  2. package/dist/artifact-store.js +32 -15
  3. package/dist/artifact.d.ts +1 -4
  4. package/dist/artifact.js +0 -28
  5. package/dist/attention-presentation.js +1 -1
  6. package/dist/boundary-presentation.js +2 -2
  7. package/dist/claude-boundary-state.d.ts +6 -6
  8. package/dist/claude-boundary-state.js +13 -13
  9. package/dist/claude-hook.js +26 -25
  10. package/dist/claude-mcp-context.d.ts +2 -2
  11. package/dist/claude-mcp-context.js +6 -6
  12. package/dist/cli/context.js +3 -6
  13. package/dist/cli/json.js +1 -1
  14. package/dist/cli/mcp-server-command.js +3 -3
  15. package/dist/cli/observation-commands.d.ts +2 -2
  16. package/dist/cli/observation-commands.js +23 -51
  17. package/dist/cli/perceive-presentation.d.ts +3 -0
  18. package/dist/cli/{catch-presentation.js → perceive-presentation.js} +3 -3
  19. package/dist/cli/registry.js +2 -2
  20. package/dist/codex-address.d.ts +3 -0
  21. package/dist/codex-address.js +7 -0
  22. package/dist/codex-hook.d.ts +2 -2
  23. package/dist/codex-hook.js +29 -39
  24. package/dist/codex-queue.d.ts +3 -3
  25. package/dist/codex-queue.js +9 -15
  26. package/dist/codex-watch.d.ts +6 -1
  27. package/dist/codex-watch.js +38 -42
  28. package/dist/delivery-ledger-sqlite.d.ts +25 -20
  29. package/dist/delivery-ledger-sqlite.js +76 -61
  30. package/dist/delivery-ledger.d.ts +24 -19
  31. package/dist/delivery-ledger.js +150 -81
  32. package/dist/delivery-lifetime.d.ts +2 -2
  33. package/dist/delivery-lifetime.js +6 -18
  34. package/dist/help.js +12 -12
  35. package/dist/hints.d.ts +2 -2
  36. package/dist/hints.js +7 -7
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/list.d.ts +2 -2
  40. package/dist/list.js +4 -3
  41. package/dist/mcp.d.ts +2 -2
  42. package/dist/mcp.js +8 -32
  43. package/dist/neutral.d.ts +5 -5
  44. package/dist/notifications.js +12 -6
  45. package/dist/operation-presentation.d.ts +8 -3
  46. package/dist/operation-presentation.js +50 -12
  47. package/dist/packages/agent-delivery/src/codex.d.ts +4 -0
  48. package/dist/packages/agent-delivery/src/codex.js +18 -13
  49. package/dist/packages/agent-delivery/src/file-lock.js +4 -1
  50. package/dist/perceive-decisions.d.ts +28 -0
  51. package/dist/{catch-decisions.js → perceive-decisions.js} +24 -24
  52. package/dist/{catch-selection.d.ts → perceive-selection.d.ts} +2 -2
  53. package/dist/{catch-selection.js → perceive-selection.js} +3 -3
  54. package/dist/plugins/opencode/index.js +4 -4
  55. package/dist/plugins/opencode-shared/delivery.d.ts +6 -6
  56. package/dist/plugins/opencode-shared/delivery.js +27 -21
  57. package/dist/plugins/opencode-v1/runtime.js +3 -3
  58. package/dist/ports.d.ts +0 -4
  59. package/dist/presentation.d.ts +1 -1
  60. package/dist/presentation.js +13 -13
  61. package/dist/session-lifecycle.d.ts +2 -2
  62. package/dist/session-lifecycle.js +11 -11
  63. package/dist/sqlite-square.d.ts +2 -4
  64. package/dist/sqlite-square.js +47 -54
  65. package/dist/sqlite-transaction.d.ts +2 -0
  66. package/dist/sqlite-transaction.js +25 -3
  67. package/dist/square-api.d.ts +1 -1
  68. package/dist/square-api.js +1 -1
  69. package/dist/square-domain.js +1 -1
  70. package/dist/square-facade.d.ts +15 -13
  71. package/dist/square-location.js +1 -1
  72. package/dist/square-posting.d.ts +5 -5
  73. package/dist/square-posting.js +39 -38
  74. package/dist/square-tools.d.ts +15 -4
  75. package/dist/square-tools.js +35 -17
  76. package/dist/views.d.ts +7 -3
  77. package/dist/views.js +6 -3
  78. package/guides/architect.md +2 -2
  79. package/guides/change-waits.md +1 -1
  80. package/guides/claude-native.md +2 -2
  81. package/guides/codex-delivery.md +91 -57
  82. package/guides/delivery-primitives.md +26 -10
  83. package/guides/mcp.md +4 -2
  84. package/guides/operation-cancellation.md +11 -3
  85. package/guides/pi-native-delivery.md +1 -1
  86. package/guides/pi-tools.md +11 -6
  87. package/guides/receive-primitive.md +3 -3
  88. package/guides/square-api.md +15 -8
  89. package/package.json +2 -2
  90. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  91. package/plugins/claude/hooks/hooks.json +2 -2
  92. package/plugins/claude/skills/square/SKILL.md +10 -10
  93. package/plugins/codex/.codex-plugin/plugin.json +2 -2
  94. package/plugins/codex/hooks/hooks.json +1 -7
  95. package/plugins/opencode/index.ts +4 -4
  96. package/plugins/opencode-shared/delivery.ts +27 -21
  97. package/plugins/opencode-v1/runtime.ts +3 -3
  98. package/plugins/pi/square-pi.js +18 -12
  99. package/skills/square/SKILL.md +10 -10
  100. package/dist/catch-decisions.d.ts +0 -28
  101. package/dist/cli/catch-presentation.d.ts +0 -3
  102. package/dist/codex-boundary-state.d.ts +0 -2
  103. package/dist/codex-boundary-state.js +0 -6
  104. package/guides/mcp-connection-lifetime.md +0 -22
@@ -4,12 +4,11 @@ import { runClaudeHook, runClaudeWatch } from '../claude-hook.js';
4
4
  import { runCodexHook, runCodexInterrupt } from '../codex-hook.js';
5
5
  import { runCodexWatch } from '../codex-watch.js';
6
6
  import { isSquareError } from '../model.js';
7
- import { commandPrefix, participantIdentity, truncateChars, withPathOutput, quoteShell, } from '../presentation.js';
8
- import { PARTICIPANTS_DEFAULT_LIMIT, PARTICIPANTS_MAX_LIMIT, renderHistoryResult, renderParticipantsResult, renderStatusResult, } from '../operation-presentation.js';
9
- import { style } from '../tty-style.js';
10
- import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
11
- import { discover, inbox, formatActivityId, parseActivityId, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from '../square-api.js';
12
- import { renderCatchOutcome, renderCatchProgress } from './catch-presentation.js';
7
+ import { commandPrefix, truncateChars, withPathOutput, quoteShell, } from '../presentation.js';
8
+ import { PARTICIPANTS_DEFAULT_LIMIT, PARTICIPANTS_MAX_LIMIT, renderHistoryResult, renderParticipantsResult, renderStatusResult, renderSquareList, } from '../operation-presentation.js';
9
+ import { formatTimestamp, parseTimeOrRelative } from '../time.js';
10
+ import { discover, inbox, formatActivityId, parseActivityId, PERCEIVE_DEFAULT_LIMIT, PERCEIVE_MAX_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from '../square-api.js';
11
+ import { renderPerceiveOutcome, renderPerceiveProgress } from './perceive-presentation.js';
13
12
  import { selectOwnSeat } from '../square-api.js';
14
13
  import { jsonResult } from './json.js';
15
14
  import { withSquare, squareInput, now, fail, parseBoundedLimit, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdin, requireSquarePath, requireValue, usage, commandIdentity, } from './context.js';
@@ -20,38 +19,6 @@ function inboxDisplay(value) {
20
19
  const truncated = truncateChars(value, INBOX_DISPLAY_CHARS - 1);
21
20
  return truncated.remaining === 0 ? truncated.text : `${truncated.text}…`;
22
21
  }
23
- function renderSquareList(data, projection) {
24
- const { items, stopped, remaining, next } = data;
25
- if (data.total === 0) {
26
- return ['(no squares found)', ...(stopped ? [style('dim', '○ the walk stopped early — deeper squares may still be hiding')] : [])].join('\n') + '\n';
27
- }
28
- const lines = ['squares'];
29
- for (const item of items) {
30
- const glyph = item.activities > 0 ? '●' : style('dim', '○');
31
- lines.push(`${glyph} ${inboxDisplay(item.path)}${style('dim', ` · ${formatRelativeTime(item.lastActiveAt, projection.now)} · ${item.participants.length} in the square · ${item.activities} ${item.activities === 1 ? 'activity' : 'activities'}`)}`);
32
- const context = item.context.slice(0, 2);
33
- if (context.length === 0)
34
- lines.push(` ${style('dim', 'context')} · (none)`);
35
- else {
36
- context.forEach((line, index) => lines.push(` ${index === 0 ? style('dim', 'context') : ' '} · ${inboxDisplay(line)}`));
37
- const hidden = item.context.length - context.length;
38
- if (hidden > 0)
39
- lines.push(style('dim', ` · … ${hidden} more ${hidden === 1 ? 'line' : 'lines'}`));
40
- }
41
- const participants = item.participants.slice(0, 3);
42
- const hidden = item.participants.length - participants.length;
43
- const names = participants.length === 0 ? 'nobody' : participants.map((name) => participantIdentity(inboxDisplay(name))).join(' · ');
44
- lines.push(` ${style('dim', 'participants')} · ${names}${hidden > 0 ? style('dim', ` · … ${hidden} more`) : ''}`);
45
- }
46
- if (remaining > 0) {
47
- lines.push(style('dim', stopped ? ' … more squares' : ` … ${remaining} more ${remaining === 1 ? 'square' : 'squares'}`));
48
- if (next !== undefined)
49
- lines.push(`square list --depth ${projection.depth} --limit ${projection.limit} --after ${quoteShell(next)}`);
50
- }
51
- if (stopped)
52
- lines.push(style('dim', '○ the walk stopped early — deeper squares may still be hiding'));
53
- return lines.join('\n') + '\n';
54
- }
55
22
  export const listCommand = {
56
23
  parse(argv, context) {
57
24
  let depth = DEFAULT_LIST_DEPTH;
@@ -81,7 +48,12 @@ export const listCommand = {
81
48
  return { depth, limit, after };
82
49
  },
83
50
  async execute(intent, context) {
84
- const result = await discover(squareInput(context), intent);
51
+ let owner;
52
+ try {
53
+ owner = commandIdentity(context.env, context.owner).owner;
54
+ }
55
+ catch { /* Discovery stays available without a complete harness identity. */ }
56
+ const result = await discover(squareInput(context), { ...intent, owner });
85
57
  return renderSquareList(result, { now: now(context), depth: intent.depth ?? DEFAULT_LIST_DEPTH, limit: intent.limit ?? DEFAULT_LIST_LIMIT });
86
58
  },
87
59
  present: (result) => process.stdout.write(result),
@@ -156,7 +128,7 @@ export const streamCommand = {
156
128
  },
157
129
  present: () => { },
158
130
  };
159
- export const catchCommand = {
131
+ export const perceiveCommand = {
160
132
  parse(argv, context) {
161
133
  let idle;
162
134
  let mention;
@@ -168,10 +140,10 @@ export const catchCommand = {
168
140
  for (let index = 0; index < argv.length; index++) {
169
141
  if (argv[index] === '--id') {
170
142
  if (id !== undefined)
171
- fail('✕ catch accepts one --id\nsquare catch --help');
143
+ fail('✕ perceive accepts one --id\nsquare perceive --help');
172
144
  const value = requireValue(argv, index, '--id');
173
145
  if (parseActivityId(value) === undefined)
174
- fail('✕ invalid --id: expected an activity id like act/12\nsquare catch --help');
146
+ fail('✕ invalid --id: expected an activity id like act/12\nsquare perceive --help');
175
147
  id = value;
176
148
  index += 1;
177
149
  }
@@ -187,7 +159,7 @@ export const catchCommand = {
187
159
  mention = true;
188
160
  }
189
161
  else if (argv[index] === '--limit') {
190
- limit = parseBoundedLimit(argv[index + 1], '--limit', CATCH_MAX_LIMIT, `${commandPrefix(requireSquarePath(context))} catch --limit ${CATCH_MAX_LIMIT}`);
162
+ limit = parseBoundedLimit(argv[index + 1], '--limit', PERCEIVE_MAX_LIMIT, `${commandPrefix(requireSquarePath(context))} perceive --limit ${PERCEIVE_MAX_LIMIT}`);
191
163
  index += 1;
192
164
  }
193
165
  else if (argv[index] === '--replace')
@@ -195,22 +167,22 @@ export const catchCommand = {
195
167
  else if (argv[index] === '--now')
196
168
  now = true;
197
169
  else if (argv[index] !== '--json')
198
- fail(`✕ catch does not know ${argv[index]}\nsquare catch --help`);
170
+ fail(`✕ perceive does not know ${argv[index]}\nsquare perceive --help`);
199
171
  }
200
172
  if (id !== undefined) {
201
173
  if (argv.some((flag) => ['--idle', '--from', '--mention', '--limit', '--replace'].includes(flag))) {
202
- fail('✕ --id cannot be combined with --idle, --from, --mention, --limit or --replace\nsquare catch --help');
174
+ fail('✕ --id cannot be combined with --idle, --from, --mention, --limit or --replace\nsquare perceive --help');
203
175
  }
204
176
  return { id };
205
177
  }
206
178
  if (idle !== undefined && now)
207
- fail('catch accepts --now or --idle, not both.');
179
+ fail('perceive accepts --now or --idle, not both.');
208
180
  if (replace && idle === undefined)
209
181
  fail('--replace can only be used with --idle.');
210
182
  return {
211
183
  ...(participants.length > 0 ? { from: participants } : {}),
212
184
  ...(mention === undefined ? {} : { mention }),
213
- limit: limit ?? CATCH_DEFAULT_LIMIT,
185
+ limit: limit ?? PERCEIVE_DEFAULT_LIMIT,
214
186
  ...(idle === undefined ? {} : { idle }),
215
187
  ...(replace ? { replace } : {}),
216
188
  };
@@ -221,10 +193,10 @@ export const catchCommand = {
221
193
  const interrupt = () => controller.abort();
222
194
  process.once('SIGINT', interrupt);
223
195
  try {
224
- const outcome = await withSquare(context, async (square) => (await selectOwnSeat(square, commandIdentity(context.env, context.owner).owner, context.name)).catch({ ...intent, ...(context.json ? {} : { observe: true }) }, {
196
+ const outcome = await withSquare(context, async (square) => (await selectOwnSeat(square, commandIdentity(context.env, context.owner).owner, context.name)).perceive({ ...intent, ...(context.json ? {} : { observe: true }) }, {
225
197
  signal: controller.signal,
226
198
  onProgress(progress) {
227
- const line = renderCatchProgress(squarePath, progress);
199
+ const line = renderPerceiveProgress(squarePath, progress);
228
200
  if (line !== undefined)
229
201
  process.stderr.write(line);
230
202
  },
@@ -234,7 +206,7 @@ export const catchCommand = {
234
206
  process.exitCode = outcome.reason === 'already-active' || outcome.reason === 'capped' ? 1 : 0;
235
207
  }
236
208
  else {
237
- const rendered = renderCatchOutcome(squarePath, outcome, intent, context.owner ?? context.env.SQUARE_OWNER);
209
+ const rendered = renderPerceiveOutcome(squarePath, outcome, intent, context.owner ?? context.env.SQUARE_OWNER);
238
210
  process.stdout.write(rendered.stdout);
239
211
  process.exitCode = rendered.exitCode;
240
212
  }
@@ -242,7 +214,7 @@ export const catchCommand = {
242
214
  catch (error) {
243
215
  if (!controller.signal.aborted)
244
216
  throw error;
245
- process.stdout.write(withPathOutput(squarePath, '○ you step back — catch stopped'));
217
+ process.stdout.write(withPathOutput(squarePath, '○ you step back — perceive stopped'));
246
218
  process.exitCode = 130;
247
219
  }
248
220
  finally {
@@ -0,0 +1,3 @@
1
+ import type { OperationProgress } from '../square-facade.js';
2
+ export { renderPerceiveOutcome } from '../operation-presentation.js';
3
+ export declare function renderPerceiveProgress(squarePath: string, progress: OperationProgress): string | undefined;
@@ -1,7 +1,7 @@
1
1
  import { renderWatchForceTakeover, renderWatchReplaceMissing } from '../presentation.js';
2
- export { renderCatchOutcome } from '../operation-presentation.js';
3
- export function renderCatchProgress(squarePath, progress) {
4
- if (progress.kind !== 'catch-started' || !progress.replacementRequested)
2
+ export { renderPerceiveOutcome } from '../operation-presentation.js';
3
+ export function renderPerceiveProgress(squarePath, progress) {
4
+ if (progress.kind !== 'perceive-started' || !progress.replacementRequested)
5
5
  return undefined;
6
6
  const options = { squarePath, name: progress.participant };
7
7
  return (progress.replaced
@@ -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, claudeHookCommand, claudeWatchCommand, codexHookCommand, codexWatchCommand, codexInterruptCommand, historyCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, } from './observation-commands.js';
7
+ import { perceiveCommand, claudeHookCommand, claudeWatchCommand, 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 },
@@ -17,7 +17,7 @@ export const commandRegistry = [
17
17
  { names: ['codex-hook'], spec: codexHookCommand },
18
18
  { names: ['codex-watch'], spec: codexWatchCommand },
19
19
  { names: ['codex-interrupt'], spec: codexInterruptCommand },
20
- { names: ['catch'], spec: catchCommand },
20
+ { names: ['perceive'], spec: perceiveCommand },
21
21
  { names: ['express'], spec: expressCommand },
22
22
  { names: ['listen'], spec: listenCommand },
23
23
  { names: ['ignore'], spec: ignoreCommand },
@@ -1,3 +1,6 @@
1
+ import { type CodexBoundaryStore } from './packages/agent-delivery/src/codex-boundary.js';
1
2
  import type { DeliveryRoute } from './delivery-ledger.js';
3
+ /** Local Stop/activity epochs shared by native hooks and async watchers. No RPC. */
4
+ export declare function codexBoundaryStore(env: NodeJS.ProcessEnv): CodexBoundaryStore;
2
5
  /** Capture the receiver's CLI configuration when its seat is established, not at send time. */
3
6
  export declare function codexWakeAddress(threadId: string, env: NodeJS.ProcessEnv): DeliveryRoute;
@@ -1,4 +1,11 @@
1
1
  import { resolveCodexHome } from './packages/agent-delivery/src/codex-endpoint.js';
2
+ import { createCodexBoundaryStore } from './packages/agent-delivery/src/codex-boundary.js';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ /** Local Stop/activity epochs shared by native hooks and async watchers. No RPC. */
6
+ export function codexBoundaryStore(env) {
7
+ return createCodexBoundaryStore(env.SQUARE_CODEX_BOUNDARIES ?? path.join(os.homedir(), '.square', 'codex-boundaries.json'));
8
+ }
2
9
  /** Capture the receiver's CLI configuration when its seat is established, not at send time. */
3
10
  export function codexWakeAddress(threadId, env) {
4
11
  const endpoint = env.SQUARE_CODEX_ENDPOINT?.trim();
@@ -1,8 +1,8 @@
1
1
  import { type CodexHookInput } from './packages/agent-delivery/src/codex-hook.js';
2
2
  export type HookTrace = (line: string) => void;
3
3
  export declare const CODEX_HOOK_BUDGET_MS = 4500;
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>;
4
+ /** Active delivery uses UserPromptSubmit and PostToolUse; each async Stop owns its own wait. */
5
+ export declare function codexHookResponse(value: unknown, sink: ((text: string) => void | Promise<void>) | undefined, env?: NodeJS.ProcessEnv, trace?: HookTrace): Promise<void>;
6
6
  export declare function parseCodexHookText(text: string): CodexHookInput | undefined;
7
7
  /** The caller owns stdout; no receipt is written when no output sink is supplied. */
8
8
  export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv, sink?: (text: string) => void | Promise<void>): Promise<string>;
@@ -1,13 +1,12 @@
1
- import { handleCodexHook, parseCodexHook } from './packages/agent-delivery/src/codex-hook.js';
1
+ import { parseCodexHook } from './packages/agent-delivery/src/codex-hook.js';
2
2
  import { presentPendingAtBoundary } from './boundary-presentation.js';
3
3
  import { DeliveryLedger } from './delivery-ledger.js';
4
4
  import { harnessOwner } from './owner.js';
5
5
  import { attachDeliverySessionRoutes, detachDeliverySession } from './session-lifecycle.js';
6
- import { codexBoundaryStore } from './codex-boundary-state.js';
7
- import { codexWakeAddress } from './codex-address.js';
6
+ import { codexWakeAddress, codexBoundaryStore } from './codex-address.js';
8
7
  export const CODEX_HOOK_BUDGET_MS = 4500;
9
8
  const defaultTrace = line => { process.stderr.write(`${line}\n`); };
10
- /** sink resolves only after stdout has accepted the complete hook response. */
9
+ /** Active delivery uses UserPromptSubmit and PostToolUse; each async Stop owns its own wait. */
11
10
  export async function codexHookResponse(value, sink, env = process.env, trace = defaultTrace) {
12
11
  const input = parseCodexHook(value);
13
12
  if (!input)
@@ -16,47 +15,38 @@ export async function codexHookResponse(value, sink, env = process.env, trace =
16
15
  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);
17
16
  const signal = AbortSignal.timeout(timeoutMs);
18
17
  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 });
18
+ // Active native events disarm idle queueing; Stop arms its own async watcher.
19
+ // This file state is the only activity gate: no app-server sampling.
20
+ await codexBoundaryStore(env).record(input.session_id, input.hook_event_name === 'SessionStart' ? 'start' : input.hook_event_name === 'SessionEnd' ? 'end' : 'non-stop', { signal, turnId: input.turn_id });
21
+ if (input.hook_event_name === 'SessionStart') {
22
+ await attachDeliverySessionRoutes(owner, env, [codexWakeAddress(input.session_id, env)], signal);
23
+ }
24
+ else if (input.hook_event_name === 'SessionEnd' || input.hook_event_name === 'Interrupt') {
25
+ const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY, signal });
24
26
  try {
25
- generation = (await ledger.session(owner))?.generation;
27
+ const captured = await ledger.session(owner, signal);
28
+ if (!captured || signal.aborted)
29
+ return;
30
+ if (input.hook_event_name === 'SessionEnd')
31
+ await detachDeliverySession(owner, captured.generation, env, signal);
32
+ else
33
+ await ledger.cancel({ owner, generation: captured.generation }, signal);
26
34
  }
27
35
  finally {
28
36
  await ledger.close();
29
37
  }
30
38
  }
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);
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}`),
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;
39
+ else if ((input.hook_event_name === 'UserPromptSubmit' || input.hook_event_name === 'PostToolUse') && sink) {
40
+ try {
41
+ await presentPendingAtBoundary(owner, async (context) => {
42
+ await sink(`${JSON.stringify({ hookSpecificOutput: { hookEventName: input.hook_event_name, additionalContext: context } })}\n`);
43
+ }, env, signal);
44
+ trace('square-codex-hook: stage=presentation');
45
+ }
46
+ catch (error) {
47
+ trace(`square-codex-hook: stage=presentation error=${error instanceof Error ? error.name : 'Error'}`);
48
+ }
49
+ }
60
50
  }
61
51
  export function parseCodexHookText(text) {
62
52
  try {
@@ -1,11 +1,11 @@
1
- import { type CodexStopEpoch } from './codex-boundary-state.js';
2
1
  import type { DeliveryCard, DeliveryTransport, DeliveryTransportContext, TransportOutcome } from './delivery-ledger.js';
3
2
  export interface CodexQueueTransportOptions {
4
- readonly epoch: CodexStopEpoch;
5
3
  readonly address: Readonly<Record<string, string>>;
6
4
  readonly env?: NodeJS.ProcessEnv;
7
5
  }
8
- /** Instantiated only by the receiver, for one captured Stop epoch. */
6
+ /** Instantiated only by the receiver. The native queue store is shared across
7
+ * processes: the hosting Codex app-server polls it and starts queued input only
8
+ * when the thread is idle, so admission needs no remote idle proof. */
9
9
  export declare class CodexQueueTransport implements DeliveryTransport {
10
10
  private readonly opts;
11
11
  readonly kind = "codex-queue";
@@ -1,9 +1,9 @@
1
1
  import { connect, DeliveryError } from './packages/agent-delivery/src/index.js';
2
- import { codexIdleAtStop } from './packages/agent-delivery/src/codex.js';
3
2
  import { codexQueueEndpoint } from './packages/agent-delivery/src/codex-endpoint.js';
4
- import { codexBoundaryStore } from './codex-boundary-state.js';
5
3
  import { renderDeliveryReminder } from './delivery-frame.js';
6
- /** Instantiated only by the receiver, for one captured Stop epoch. */
4
+ /** Instantiated only by the receiver. The native queue store is shared across
5
+ * processes: the hosting Codex app-server polls it and starts queued input only
6
+ * when the thread is idle, so admission needs no remote idle proof. */
7
7
  export class CodexQueueTransport {
8
8
  opts;
9
9
  kind = 'codex-queue';
@@ -16,21 +16,15 @@ export class CodexQueueTransport {
16
16
  !Object.entries(this.opts.address).every(([key, value]) => address[key] === value))
17
17
  return 'not-capable';
18
18
  const env = this.opts.env ?? process.env;
19
- const boundaries = codexBoundaryStore(env), epoch = this.opts.epoch;
20
19
  try {
21
- // Receiver delivery addresses the already-hosting endpoint explicitly;
22
- // disappearing native custody must not trigger CLI service startup.
20
+ // Receiver delivery addresses the shared queue store explicitly; disappearing
21
+ // native custody must not trigger CLI service startup.
23
22
  const endpoint = address.endpoint ?? codexQueueEndpoint({ CODEX_HOME: address.codexHome });
24
23
  const agent = await connect({ harness: 'codex', transport: 'cli', sessionId: threadId,
25
- boundaries: { queueEligible: async (id) => {
26
- if (!await boundaries.queueEligible(id, epoch))
27
- return false;
28
- // Repeat the native sample after claim, immediately before the final
29
- // epoch and ledger fences. These reads are not an atomic native enqueue.
30
- if (!await codexIdleAtStop({ harness: 'codex', sessionId: id, codexHome: address.codexHome,
31
- endpoint, timeoutMs: Math.max(1, context.deadline - Date.now()), signal: context.signal }, epoch.turnId))
32
- return false;
33
- return await boundaries.queueEligible(id, epoch) && await context.beforeSend()
24
+ boundaries: { queueEligible: async () => {
25
+ // Only ledger fences remain: currentness, suppression, cancellation,
26
+ // attachment generation, signal and deadline immediately before launch.
27
+ return await context.beforeSend()
34
28
  && !context.signal.aborted && Date.now() < context.deadline;
35
29
  } },
36
30
  env, bin: env.SQUARE_CODEX_BIN, codexHome: address.codexHome, endpoint });
@@ -1,2 +1,7 @@
1
- /** One native async hook owns observation; the lock itself conveys no idle authority. */
1
+ /** One async Stop waits only for its captured native turn; it never adopts another Stop.
2
+ * Queue admission writes the shared native queue store through the CLI; the hosting
3
+ * Codex process starts queued input only when idle, so no remote idle sampling.
4
+ * Local boundary epochs retire this watcher when a newer Stop or any active native
5
+ * event (UserPromptSubmit/PostToolUse/Interrupt) arrives, keeping active-turn
6
+ * presentation on the synchronous hooks. */
2
7
  export declare function runCodexWatch(inputText: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<string>;
@@ -6,12 +6,15 @@ import { retainDeliveryLedger } from './delivery-lifetime.js';
6
6
  import { harnessOwner } from './owner.js';
7
7
  import { withFileLock } from './file-lock.js';
8
8
  import { canonicalPath } from './canonical-path.js';
9
- import { codexBoundaryStore } from './codex-boundary-state.js';
10
- import { codexWakeAddress } from './codex-address.js';
9
+ import { codexWakeAddress, codexBoundaryStore } from './codex-address.js';
11
10
  import { parseCodexHookText } from './codex-hook.js';
12
11
  import { CodexQueueTransport } from './codex-queue.js';
13
- import { codexIdleAtStop } from './packages/agent-delivery/src/codex.js';
14
- /** One native async hook owns observation; the lock itself conveys no idle authority. */
12
+ /** One async Stop waits only for its captured native turn; it never adopts another Stop.
13
+ * Queue admission writes the shared native queue store through the CLI; the hosting
14
+ * Codex process starts queued input only when idle, so no remote idle sampling.
15
+ * Local boundary epochs retire this watcher when a newer Stop or any active native
16
+ * event (UserPromptSubmit/PostToolUse/Interrupt) arrives, keeping active-turn
17
+ * presentation on the synchronous hooks. */
15
18
  export async function runCodexWatch(inputText, env = process.env, signal) {
16
19
  const input = parseCodexHookText(inputText);
17
20
  if (input?.hook_event_name !== 'Stop' || signal?.aborted)
@@ -22,31 +25,39 @@ export async function runCodexWatch(inputText, env = process.env, signal) {
22
25
  const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
23
26
  process.on('SIGTERM', stop);
24
27
  process.on('SIGINT', stop);
28
+ let lifetime;
25
29
  try {
26
30
  const route = codexWakeAddress(input.session_id, env);
27
31
  const home = await canonicalPath(route.address.codexHome);
28
- const coordinate = [input.session_id, route.address.endpoint ?? home];
32
+ const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env), signal: combined });
33
+ lifetime = retainDeliveryLedger(ledger);
34
+ const owner = harnessOwner({ channel: 'codex', session: input.session_id });
35
+ const captured = await ledger.session(owner, combined);
36
+ if (captured?.state !== 'attached')
37
+ return '';
38
+ // Same-turn duplicates cannot overlap; a later Stop need not wait for an old
39
+ // watcher or a slow late settlement to release a session-wide lock.
40
+ const coordinate = [input.session_id, route.address.endpoint ?? home, captured.generation, input.turn_id];
29
41
  const root = path.join(path.dirname(deliveryLedgerLocation(env)), 'codex-watchers');
30
42
  await fs.mkdir(root, { recursive: true, mode: 0o700 });
31
43
  const lock = path.join(root, createHash('sha256').update(JSON.stringify(coordinate)).digest('hex') + '.lock');
32
44
  const file = await fs.open(lock, 'a', 0o600);
33
45
  await file.close();
34
46
  await withFileLock(lock, { retryMs: 10, skipIfBusy: true, signal: combined }, async () => {
35
- const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env) });
36
- const lifetime = retainDeliveryLedger(ledger);
37
- let observer;
47
+ const boundaries = codexBoundaryStore(env);
48
+ // Record only after winning the lock: a duplicate Stop must not replace the
49
+ // live watcher's epoch and retire both. A newer Stop or active event does.
50
+ await boundaries.record(input.session_id, 'Stop', { turnId: input.turn_id, signal: combined });
51
+ const epoch = (await boundaries.read(input.session_id))?.stop;
52
+ if (!epoch)
53
+ return;
54
+ const observer = await ledger.observe(owner);
38
55
  try {
39
- const owner = harnessOwner({ channel: 'codex', session: input.session_id });
40
- const captured = await ledger.session(owner);
41
- if (captured?.state !== 'attached')
42
- return;
43
- const boundaries = codexBoundaryStore(env);
44
- // Subscribe before inspecting pending work. Both ledger edges and the
45
- // bounded wait below cause reconciliation; boundary-only changes matter.
46
- observer = await ledger.observe(owner);
47
56
  while (!combined.aborted) {
48
- let version = await observer.read(combined);
49
- const session = await ledger.session(owner);
57
+ const version = await observer.read(combined);
58
+ if (!await boundaries.queueEligible(input.session_id, epoch))
59
+ return;
60
+ const session = await ledger.session(owner, combined);
50
61
  if (session?.state !== 'attached' || session.generation !== captured.generation)
51
62
  return;
52
63
  const registered = session.routes.find(candidate => candidate.kind === 'codex-queue');
@@ -54,36 +65,20 @@ export async function runCodexWatch(inputText, env = process.env, signal) {
54
65
  registered.address.endpoint !== route.address.endpoint ||
55
66
  !registered.address.codexHome || await canonicalPath(registered.address.codexHome) !== home)
56
67
  return;
57
- const boundary = await boundaries.read(input.session_id);
58
- if (boundary?.ended)
59
- return;
60
- const pending = await ledger.pending(owner, { generation: captured.generation });
61
- const held = new Set((await ledger.handoffs(owner, { generation: captured.generation }))
68
+ const held = new Set((await ledger.handoffs(owner, { generation: captured.generation }, combined))
62
69
  .filter(attempt => !['failed', 'released'].includes(attempt.outcome)).map(attempt => attempt.seq));
63
- for (const card of pending) {
64
- if (combined.aborted)
65
- break;
66
- if (held.has(card.seq))
67
- continue;
68
- // Follow the latest Stop, regardless of which Stop launched this process.
69
- const epoch = (await boundaries.read(input.session_id))?.stop;
70
- if (!epoch || !await codexIdleAtStop({ harness: 'codex', sessionId: input.session_id,
71
- codexHome: registered.address.codexHome, endpoint: registered.address.endpoint,
72
- timeoutMs: 2000, signal: combined }, epoch.turnId))
73
- break;
74
- lifetime.track(await ledger.dispatchHandoff(owner, card.key, new CodexQueueTransport({ epoch, env, address: registered.address }), { generation: captured.generation, timeoutMs: 5000, signal: combined }));
75
- // Drain our own claim/settlement edge. A known-unsent failure must
76
- // not turn file observation into an immediate retry loop. Any post
77
- // racing this read is still found by the bounded rescan below.
78
- version = await observer.read(combined);
70
+ const card = (await ledger.pending(owner, { generation: captured.generation }, combined)).find(card => !held.has(card.seq));
71
+ if (card) {
72
+ lifetime.track(await ledger.dispatchHandoff(owner, card.key, new CodexQueueTransport({ env, address: registered.address }), { generation: captured.generation, timeoutMs: 5000, signal: combined }));
73
+ // One handoff attempt ends this wait, including unknown/refused
74
+ // outcomes. Never replay or turn our own writes into a retry loop.
75
+ return;
79
76
  }
80
- // No claims are held while waiting, including while active or disarmed.
81
77
  await observer.changed(version, 1000, combined);
82
78
  }
83
79
  }
84
80
  finally {
85
- observer?.close();
86
- await lifetime.close();
81
+ observer.close();
87
82
  }
88
83
  });
89
84
  }
@@ -95,6 +90,7 @@ export async function runCodexWatch(inputText, env = process.env, signal) {
95
90
  clearTimeout(timer);
96
91
  process.off('SIGTERM', stop);
97
92
  process.off('SIGINT', stop);
93
+ await lifetime?.close();
98
94
  }
99
95
  return '';
100
96
  }
@@ -8,8 +8,13 @@ export declare class LedgerStore {
8
8
  private readonly clock;
9
9
  private pending;
10
10
  private closed;
11
+ private readonly cancellation;
12
+ private readonly ordinaryCancellation;
13
+ private closing?;
11
14
  private constructor();
12
- static open(location: string, clock: () => number): Promise<LedgerStore>;
15
+ static open(location: string, clock: () => number, signal?: AbortSignal): Promise<LedgerStore>;
16
+ /** Stop ordinary work while captured effects retain access to settlement. */
17
+ retire(): void;
13
18
  close(): Promise<void>;
14
19
  private transaction;
15
20
  private getSession;
@@ -20,49 +25,49 @@ export declare class LedgerStore {
20
25
  private normalize;
21
26
  private deleteCard;
22
27
  private gc;
23
- session(owner: string): Promise<DeliverySession | undefined>;
24
- sessions(): Promise<readonly DeliverySession[]>;
25
- pendingCards(owner: string, generation: string): Promise<readonly DeliveryCard[]>;
26
- handoffs(owner: string, generation: string): Promise<readonly DeliveryAttempt[]>;
28
+ session(owner: string, signal?: AbortSignal): Promise<DeliverySession | undefined>;
29
+ sessions(signal?: AbortSignal): Promise<readonly DeliverySession[]>;
30
+ pendingCards(owner: string, generation: string, signal?: AbortSignal): Promise<readonly DeliveryCard[]>;
31
+ handoffs(owner: string, generation: string, signal?: AbortSignal): Promise<readonly DeliveryAttempt[]>;
27
32
  attach(input: {
28
33
  owner: string;
29
34
  expectedGeneration: string | null;
30
35
  routes: readonly DeliveryRoute[];
31
- }): Promise<DeliverySession | false>;
36
+ }, signal?: AbortSignal): Promise<DeliverySession | false>;
32
37
  updateRoutes(input: {
33
38
  owner: string;
34
39
  generation: string;
35
40
  routes: readonly DeliveryRoute[];
36
- }): Promise<boolean>;
41
+ }, signal?: AbortSignal): Promise<boolean>;
37
42
  lifecycle(input: {
38
43
  owner: string;
39
44
  generation: string;
40
- }, state: 'detached' | 'deleted'): Promise<boolean>;
45
+ }, state: 'detached' | 'deleted', signal?: AbortSignal): Promise<boolean>;
41
46
  post(input: {
42
47
  owner: string;
43
48
  key: string;
44
49
  text: string;
45
50
  details: string | null;
46
- }): Promise<PostResult>;
51
+ }, signal?: AbortSignal): Promise<PostResult>;
47
52
  suppress(input: {
48
53
  owner: string;
49
54
  key: string;
50
55
  scope: 'exact' | 'prefix';
51
56
  expiresAt?: number;
52
- }): Promise<SuppressionResult>;
57
+ }, signal?: AbortSignal): Promise<SuppressionResult>;
53
58
  renewSuppression(input: {
54
59
  owner: string;
55
60
  id: string;
56
61
  expiresAt: number;
57
- }): Promise<boolean>;
62
+ }, signal?: AbortSignal): Promise<boolean>;
58
63
  releaseSuppression(input: {
59
64
  owner: string;
60
65
  id: string;
61
- }): Promise<boolean>;
66
+ }, signal?: AbortSignal): Promise<boolean>;
62
67
  cancel(input: {
63
68
  owner: string;
64
69
  generation: string;
65
- }): Promise<boolean>;
70
+ }, signal?: AbortSignal): Promise<boolean>;
66
71
  private isSuppressed;
67
72
  private eligible;
68
73
  private insertAttempt;
@@ -79,21 +84,21 @@ export declare class LedgerStore {
79
84
  prepare(token: string, value: DeliveryCorrelation, signal?: AbortSignal): Promise<boolean>;
80
85
  private receipt;
81
86
  private confirm;
82
- settle(tokens: readonly string[], outcome: Exclude<DeliveryOutcome, 'dispatching'>): Promise<boolean>;
87
+ settle(tokens: readonly string[], outcome: Exclude<DeliveryOutcome, 'dispatching'>, signal?: AbortSignal): Promise<boolean>;
83
88
  private handoffFacts;
84
89
  private matchingHandoff;
85
90
  private currentHandoff;
86
- handoff(token: string): Promise<(DeliveryHandoffRef & {
91
+ handoff(token: string, signal?: AbortSignal): Promise<(DeliveryHandoffRef & {
87
92
  routeKind: string;
88
93
  }) | undefined>;
89
- guardHandoff(ref: DeliveryHandoffRef): Promise<boolean>;
90
- acceptHandoff(ref: DeliveryHandoffRef): Promise<boolean>;
94
+ guardHandoff(ref: DeliveryHandoffRef, signal?: AbortSignal): Promise<boolean>;
95
+ acceptHandoff(ref: DeliveryHandoffRef, signal?: AbortSignal): Promise<boolean>;
91
96
  observePresentation(handoff: string | DeliveryHandoffRef, options?: {
92
97
  readonly requireCurrent?: boolean;
93
- }): Promise<boolean>;
98
+ }, signal?: AbortSignal): Promise<boolean>;
94
99
  receipts(owner: string, options: {
95
100
  keys?: readonly string[];
96
101
  since?: number;
97
- }): Promise<readonly DeliveryReceipt[]>;
98
- health(owner?: string): Promise<DeliveryHealth>;
102
+ }, signal?: AbortSignal): Promise<readonly DeliveryReceipt[]>;
103
+ health(owner?: string, signal?: AbortSignal): Promise<DeliveryHealth>;
99
104
  }