@astrosheep/square 0.4.4 → 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 (98) 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 +25 -24
  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 +1 -1
  23. package/dist/codex-hook.js +11 -8
  24. package/dist/codex-queue.d.ts +3 -2
  25. package/dist/codex-queue.js +8 -11
  26. package/dist/codex-watch.d.ts +6 -1
  27. package/dist/codex-watch.js +27 -29
  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/file-lock.js +4 -1
  48. package/dist/perceive-decisions.d.ts +28 -0
  49. package/dist/{catch-decisions.js → perceive-decisions.js} +24 -24
  50. package/dist/{catch-selection.d.ts → perceive-selection.d.ts} +2 -2
  51. package/dist/{catch-selection.js → perceive-selection.js} +3 -3
  52. package/dist/plugins/opencode/index.js +4 -4
  53. package/dist/plugins/opencode-shared/delivery.d.ts +6 -6
  54. package/dist/plugins/opencode-shared/delivery.js +27 -21
  55. package/dist/plugins/opencode-v1/runtime.js +3 -3
  56. package/dist/ports.d.ts +0 -4
  57. package/dist/presentation.d.ts +1 -1
  58. package/dist/presentation.js +13 -13
  59. package/dist/session-lifecycle.d.ts +2 -2
  60. package/dist/session-lifecycle.js +11 -11
  61. package/dist/sqlite-square.d.ts +2 -4
  62. package/dist/sqlite-square.js +47 -54
  63. package/dist/sqlite-transaction.d.ts +2 -0
  64. package/dist/sqlite-transaction.js +25 -3
  65. package/dist/square-api.d.ts +1 -1
  66. package/dist/square-api.js +1 -1
  67. package/dist/square-domain.js +1 -1
  68. package/dist/square-facade.d.ts +15 -13
  69. package/dist/square-location.js +1 -1
  70. package/dist/square-posting.d.ts +5 -5
  71. package/dist/square-posting.js +39 -38
  72. package/dist/square-tools.d.ts +15 -4
  73. package/dist/square-tools.js +35 -17
  74. package/dist/views.d.ts +7 -3
  75. package/dist/views.js +6 -3
  76. package/guides/architect.md +2 -2
  77. package/guides/change-waits.md +1 -1
  78. package/guides/codex-delivery.md +40 -35
  79. package/guides/delivery-primitives.md +22 -7
  80. package/guides/mcp.md +4 -2
  81. package/guides/operation-cancellation.md +11 -3
  82. package/guides/pi-native-delivery.md +1 -1
  83. package/guides/pi-tools.md +11 -6
  84. package/guides/receive-primitive.md +3 -3
  85. package/guides/square-api.md +15 -8
  86. package/package.json +2 -2
  87. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  88. package/plugins/claude/skills/square/SKILL.md +10 -10
  89. package/plugins/codex/.codex-plugin/plugin.json +2 -2
  90. package/plugins/codex/hooks/hooks.json +3 -0
  91. package/plugins/opencode/index.ts +4 -4
  92. package/plugins/opencode-shared/delivery.ts +27 -21
  93. package/plugins/opencode-v1/runtime.ts +3 -3
  94. package/plugins/pi/square-pi.js +18 -12
  95. package/skills/square/SKILL.md +10 -10
  96. package/dist/catch-decisions.d.ts +0 -28
  97. package/dist/cli/catch-presentation.d.ts +0 -3
  98. 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,7 +1,7 @@
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
- /** Active delivery uses PostToolUse only; each async Stop owns its own wait. */
4
+ /** Active delivery uses UserPromptSubmit and PostToolUse; each async Stop owns its own wait. */
5
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. */
@@ -3,10 +3,10 @@ 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 { codexWakeAddress } from './codex-address.js';
6
+ import { codexWakeAddress, codexBoundaryStore } from './codex-address.js';
7
7
  export const CODEX_HOOK_BUDGET_MS = 4500;
8
8
  const defaultTrace = line => { process.stderr.write(`${line}\n`); };
9
- /** Active delivery uses PostToolUse only; each async Stop owns its own wait. */
9
+ /** Active delivery uses UserPromptSubmit and PostToolUse; each async Stop owns its own wait. */
10
10
  export async function codexHookResponse(value, sink, env = process.env, trace = defaultTrace) {
11
11
  const input = parseCodexHook(value);
12
12
  if (!input)
@@ -15,28 +15,31 @@ export async function codexHookResponse(value, sink, env = process.env, trace =
15
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);
16
16
  const signal = AbortSignal.timeout(timeoutMs);
17
17
  const owner = harnessOwner({ channel: 'codex', session: input.session_id });
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 });
18
21
  if (input.hook_event_name === 'SessionStart') {
19
22
  await attachDeliverySessionRoutes(owner, env, [codexWakeAddress(input.session_id, env)], signal);
20
23
  }
21
24
  else if (input.hook_event_name === 'SessionEnd' || input.hook_event_name === 'Interrupt') {
22
- const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
25
+ const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY, signal });
23
26
  try {
24
- const captured = await ledger.session(owner);
27
+ const captured = await ledger.session(owner, signal);
25
28
  if (!captured || signal.aborted)
26
29
  return;
27
30
  if (input.hook_event_name === 'SessionEnd')
28
- await detachDeliverySession(owner, captured.generation, env);
31
+ await detachDeliverySession(owner, captured.generation, env, signal);
29
32
  else
30
- await ledger.cancel({ owner, generation: captured.generation });
33
+ await ledger.cancel({ owner, generation: captured.generation }, signal);
31
34
  }
32
35
  finally {
33
36
  await ledger.close();
34
37
  }
35
38
  }
36
- else if (input.hook_event_name === 'PostToolUse' && sink) {
39
+ else if ((input.hook_event_name === 'UserPromptSubmit' || input.hook_event_name === 'PostToolUse') && sink) {
37
40
  try {
38
41
  await presentPendingAtBoundary(owner, async (context) => {
39
- await sink(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } })}\n`);
42
+ await sink(`${JSON.stringify({ hookSpecificOutput: { hookEventName: input.hook_event_name, additionalContext: context } })}\n`);
40
43
  }, env, signal);
41
44
  trace('square-codex-hook: stage=presentation');
42
45
  }
@@ -1,10 +1,11 @@
1
1
  import type { DeliveryCard, DeliveryTransport, DeliveryTransportContext, TransportOutcome } from './delivery-ledger.js';
2
2
  export interface CodexQueueTransportOptions {
3
- readonly turnId: string;
4
3
  readonly address: Readonly<Record<string, string>>;
5
4
  readonly env?: NodeJS.ProcessEnv;
6
5
  }
7
- /** 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. */
8
9
  export declare class CodexQueueTransport implements DeliveryTransport {
9
10
  private readonly opts;
10
11
  readonly kind = "codex-queue";
@@ -1,8 +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
3
  import { renderDeliveryReminder } from './delivery-frame.js';
5
- /** 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. */
6
7
  export class CodexQueueTransport {
7
8
  opts;
8
9
  kind = 'codex-queue';
@@ -15,18 +16,14 @@ export class CodexQueueTransport {
15
16
  !Object.entries(this.opts.address).every(([key, value]) => address[key] === value))
16
17
  return 'not-capable';
17
18
  const env = this.opts.env ?? process.env;
18
- const turnId = this.opts.turnId;
19
19
  try {
20
- // Receiver delivery addresses the already-hosting endpoint explicitly;
21
- // 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.
22
22
  const endpoint = address.endpoint ?? codexQueueEndpoint({ CODEX_HOME: address.codexHome });
23
23
  const agent = await connect({ harness: 'codex', transport: 'cli', sessionId: threadId,
24
- boundaries: { queueEligible: async (id) => {
25
- // Repeat the native sample after claim, immediately before the final
26
- // ledger fences. These reads are not an atomic native enqueue.
27
- if (!await codexIdleAtStop({ harness: 'codex', sessionId: id, codexHome: address.codexHome,
28
- endpoint, timeoutMs: Math.max(1, context.deadline - Date.now()), signal: context.signal }, turnId))
29
- return false;
24
+ boundaries: { queueEligible: async () => {
25
+ // Only ledger fences remain: currentness, suppression, cancellation,
26
+ // attachment generation, signal and deadline immediately before launch.
30
27
  return await context.beforeSend()
31
28
  && !context.signal.aborted && Date.now() < context.deadline;
32
29
  } },
@@ -1,2 +1,7 @@
1
- /** One async Stop waits only for its captured native turn; it never adopts another Stop. */
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,16 +6,19 @@ 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 { codexWakeAddress } from './codex-address.js';
9
+ import { codexWakeAddress, codexBoundaryStore } from './codex-address.js';
10
10
  import { parseCodexHookText } from './codex-hook.js';
11
11
  import { CodexQueueTransport } from './codex-queue.js';
12
- import { codexStopState } from './packages/agent-delivery/src/codex.js';
13
- /** One async Stop waits only for its captured native turn; it never adopts another Stop. */
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. */
14
18
  export async function runCodexWatch(inputText, env = process.env, signal) {
15
19
  const input = parseCodexHookText(inputText);
16
20
  if (input?.hook_event_name !== 'Stop' || signal?.aborted)
17
21
  return '';
18
- const turnId = input.turn_id;
19
22
  const controller = new AbortController();
20
23
  const stop = () => controller.abort();
21
24
  const timer = setTimeout(stop, 86_400_000);
@@ -26,26 +29,35 @@ export async function runCodexWatch(inputText, env = process.env, signal) {
26
29
  try {
27
30
  const route = codexWakeAddress(input.session_id, env);
28
31
  const home = await canonicalPath(route.address.codexHome);
29
- const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env) });
32
+ const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env), signal: combined });
30
33
  lifetime = retainDeliveryLedger(ledger);
31
34
  const owner = harnessOwner({ channel: 'codex', session: input.session_id });
32
- const captured = await ledger.session(owner);
35
+ const captured = await ledger.session(owner, combined);
33
36
  if (captured?.state !== 'attached')
34
37
  return '';
35
38
  // Same-turn duplicates cannot overlap; a later Stop need not wait for an old
36
39
  // watcher or a slow late settlement to release a session-wide lock.
37
- const coordinate = [input.session_id, route.address.endpoint ?? home, captured.generation, turnId];
40
+ const coordinate = [input.session_id, route.address.endpoint ?? home, captured.generation, input.turn_id];
38
41
  const root = path.join(path.dirname(deliveryLedgerLocation(env)), 'codex-watchers');
39
42
  await fs.mkdir(root, { recursive: true, mode: 0o700 });
40
43
  const lock = path.join(root, createHash('sha256').update(JSON.stringify(coordinate)).digest('hex') + '.lock');
41
44
  const file = await fs.open(lock, 'a', 0o600);
42
45
  await file.close();
43
46
  await withFileLock(lock, { retryMs: 10, skipIfBusy: true, signal: combined }, async () => {
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;
44
54
  const observer = await ledger.observe(owner);
45
55
  try {
46
56
  while (!combined.aborted) {
47
57
  const version = await observer.read(combined);
48
- const session = await ledger.session(owner);
58
+ if (!await boundaries.queueEligible(input.session_id, epoch))
59
+ return;
60
+ const session = await ledger.session(owner, combined);
49
61
  if (session?.state !== 'attached' || session.generation !== captured.generation)
50
62
  return;
51
63
  const registered = session.routes.find(candidate => candidate.kind === 'codex-queue');
@@ -53,29 +65,15 @@ export async function runCodexWatch(inputText, env = process.env, signal) {
53
65
  registered.address.endpoint !== route.address.endpoint ||
54
66
  !registered.address.codexHome || await canonicalPath(registered.address.codexHome) !== home)
55
67
  return;
56
- // Probe even with no pending card: a new user/model turn retires this
57
- // watcher without requiring UserPromptSubmit or tool hooks to disarm it.
58
- const state = await codexStopState({ harness: 'codex', sessionId: input.session_id,
59
- codexHome: registered.address.codexHome, endpoint: registered.address.endpoint,
60
- timeoutMs: 2000, signal: combined }, turnId);
61
- if (state === 'stale')
62
- return;
63
- if (state === 'unavailable') {
64
- if (!combined.aborted)
65
- console.warn('Square Codex idle delivery unavailable: requires a reachable hosting app-server endpoint and supported native turn history. PostToolUse delivery remains available.');
68
+ const held = new Set((await ledger.handoffs(owner, { generation: captured.generation }, combined))
69
+ .filter(attempt => !['failed', 'released'].includes(attempt.outcome)).map(attempt => attempt.seq));
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.
66
75
  return;
67
76
  }
68
- if (state === 'idle') {
69
- const held = new Set((await ledger.handoffs(owner, { generation: captured.generation }))
70
- .filter(attempt => !['failed', 'released'].includes(attempt.outcome)).map(attempt => attempt.seq));
71
- const card = (await ledger.pending(owner, { generation: captured.generation })).find(card => !held.has(card.seq));
72
- if (card) {
73
- lifetime.track(await ledger.dispatchHandoff(owner, card.key, new CodexQueueTransport({ turnId, env, address: registered.address }), { generation: captured.generation, timeoutMs: 5000, signal: combined }));
74
- // One handoff attempt ends this wait, including unknown/refused
75
- // outcomes. Never replay or turn our own writes into a retry loop.
76
- return;
77
- }
78
- }
79
77
  await observer.changed(version, 1000, combined);
80
78
  }
81
79
  }
@@ -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
  }