Skip to content

Commit 4f323f0

Browse files
authored
fix(imessage): preserve poll option emoji identity
1 parent 2569c04 commit 4f323f0

148 files changed

Lines changed: 6074 additions & 1911 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DESIGN-cron-on-exit.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# feat(cron): `on-exit` schedule — fire a job when a watched command/process exits
2+
3+
## Problem
4+
5+
Event-driven wakes that start a fresh agent turn already work (the `wake`/`system event` RPC). But an agent cannot reliably arm "wake me when this command/process exits" itself: CLI backends run each turn as a supervisor-spawned **detached process group** that is `signalProcessTree(SIGTERM→SIGKILL)`'d at turn end (`src/process/supervisor/adapters/child.ts`, intentional, #71662). Any process the agent backgrounds via `exec` is in that tree and dies with the turn. The only escape (`setsid` + raw `node dist/entry.js system event …`) is hand-rolled, fragile, and observed to take down the host. Applies to **all** spawn-and-kill CLI backends (claude-cli verified), not the TLS proxy.
6+
7+
## Design
8+
9+
A new cron **schedule kind** `on-exit`, executed by a **gateway-supervisor-owned watcher** — independent of #83738 (rides the existing main-session cron run pipeline, not the manual wake path).
10+
11+
- `CronSchedule` gains `{ kind: "on-exit"; command: string; cwd?: string }` (PID-watch variant deferred).
12+
- `computeNextRunAtMs()` returns `undefined` for `on-exit` → the time-based timer never fires it.
13+
- `createCronExitWatchers()` (extracted into `src/gateway/cron-exit-watchers.ts`, wired from `buildGatewayCronService` via `reconcileExitWatchers()` / `stopExitWatchers()`) owns the watcher lifecycle, backed by `getProcessSupervisor()`. It exposes `reconcile(jobs) / cancel(jobId) / cancelAll() / activeJobIds()`.
14+
- On reconcile, each enabled `on-exit` job reserves a watcher slot synchronously (with an `armToken`), then spawns the command via `supervisor.spawn({ mode:"child", scopeKey:"cron-exit:<jobId>", replaceExistingScope:true, argv:[shell.command, ...shell.argsFor(command)], cwd?, captureOutput:true })`. The shell is platform-aware via `resolveExitWatchShell()`: `cmd.exe /d /s /c` on Windows, `bash -lc` on POSIX.
15+
- The watcher lives under the **gateway** supervisor tree, so per-turn CLI teardown never touches it. An async spawn/wait that loses ownership (job cancelled or re-armed for a changed command/cwd) is a no-op via the `armToken`/slot-identity check.
16+
- `await run.wait()` → on exit, the one-shot completion is **persisted (the job disabled in the store) BEFORE the job fires**, fail-closed: if the store write or `run.wait()` rejects, the job does NOT fire — so a gateway restart cannot re-arm and double-fire the same exit. Only then does it fire via the existing cron run pipeline; the woken turn sees the exit code + last output lines.
17+
- Job `remove`/`disable`, or a changed command/cwd → `reconcile()` cancels or re-arms the watcher (`cancel(jobId)`; `cancelAll()` on shutdown).
18+
- Delivery to the originating conversation is the **existing** `executeMainSessionCronJob` path (`resolveMainSessionCronDeliveryContext`) — already correct on main; no dependency on #83738.
19+
20+
## Reuse / no new delivery code
21+
22+
Everything after "process exited" is the current cron run→system-event→delivery pipeline. The only new surface: the schedule kind, its validation, the watcher lifecycle, and the tool/schema plumbing to create such a job.
23+
24+
## Out of scope
25+
26+
- PID-watch (`{ kind:"on-exit"; pid }`) — follow-up.
27+
- Re-arm/repeat on each exit — v1 is one-shot (job disables after firing, like a one-shot `at`).
28+
- This PR **stacks on #83738** and reuses its origin-aware wake as the firing
29+
mechanism; it adds only the process-exit _trigger_ (the supervisor watcher).

0 commit comments

Comments
 (0)