Repository navigation
feat(lifecycle): detached handoff supervisor with OS-owned callback - #14
nullStack65 wants to merge 1 commit into
Conversation
An agent running inside T3 cannot upgrade T3: the lifecycle action kills the agent before it can report back. This adds a detached handoff that moves the action to an OS-owned supervisor whose lifetime is independent of T3. - handwritten envelope (owner-private, credentials refused) describing the originating thread, command, wait target, relaunch, readiness and callback; - macOS: transient per-handoff LaunchAgent; Windows: transient current-user Scheduled Task (InteractiveToken, LeastPrivilege, one-shot); - explicit state machine PREPARED..COMPLETE/FAILED, single-run claim, and terminal-result idempotency that refuses to re-run the destructive command; - independence proof (direct parent pid 1, no initiator ancestor) required before the initiator may stop T3; - callback contract: local result envelope + GitHub receipt; no state.sqlite writes, no fabricated orchestration events, no copied credentials. Zero-dependency ESM under scripts/lifecycle-handoff with unit tests and a real launchd fixture (fake parent exits, supervisor survives, runs a harmless command, relaunches a fake service, calls back once, unregisters itself). Non-overlapping with the Windows SCM lifecycle work on PR #10.
Thread transfer impact
This comment will update automatically after the next completed run. |
|
Closing as an obsolete proposal under the current approved delivery direction, not as superseded or implemented. Exact head Audited against current main |
Detached T3 lifecycle handoff (supervisor + callback)
An agent running inside T3 cannot upgrade T3: the lifecycle action kills the agent
before it can report back. This adds a detached handoff that moves the action to
an OS-owned supervisor whose lifetime is independent of T3.
Draft. Non-overlapping with the Windows SCM lifecycle work on #10 (no shared
files:
prepared#10 changes and this branch touch disjoint paths). Relatedlifecycle hub: #10; environment coordinator: nullStack65/closura-agent-config#237.
What is here
lib/envelope.mjs): owner-private, credentials refused at writetime, describes originating thread/project/machine, command, wait target,
relaunch, readiness, identity and callback.
RunAtLoad,KeepAlive=false,Background). Its direct parent is launchd (pid 1), so T3/Electron job-object
teardown cannot reach it. It removes its own plist and
bootctl bootouts itself.InteractiveToken,LeastPrivilege, one-shot). Implemented; not executed on a Windows host.lib/run.mjs):PREPARED → DETACHED → WAITING_FOR_EXIT → APPLYING → RELAUNCHING → WAITING_FOR_T3 → CALLBACK_PENDING → COMPLETE/FAILED,single-run claim, terminal-result idempotency that refuses to re-run the
destructive command, bounded timeouts, and a hard rule that a still-alive
waited-for pid fails the handoff rather than being force-killed.
lib/process.mjs): parent chain walk assertingdirectParent === 1and no initiator ancestor — required before the initiatormay quit T3.
ghdurable receipt(or generic HTTP POST). No
state.sqlitewrites, no fabricated orchestrationevents, no copied auth tokens.
docs/internals/lifecycle-handoff.md(Mac, Windows, callbackcontract, security model, recovery, agent invocation). Operator README under
scripts/lifecycle-handoff/.Evidence on this head
node --test test/envelope.test.mjs test/run.test.mjs→ 11 passed / 0 failed.node --test test/fixture.integration.test.mjs→ 1 passed.Real LaunchAgent; fake parent prepares a handoff and exits; the detached
supervisor survives (independence proof: pid
directParent=1, ancestry[1],independent=true), runs the harmless command, relaunches a fake service,delivers the callback exactly once (
state=COMPLETE), unregisters its own job,and refuses to re-run on idempotent replay.
Callback status
T3 exposes no supported CLI/REST callback today; the only mutation transport
is authenticated
/wsorchestration.dispatchCommand. This PR therefore ships theresult-envelope + GitHub-receipt fallback behind a stable
callback.kind, so afuture supported endpoint slots in without touching lifecycle logic.
NOT DONE (explicit)
terminate that session); the mechanism is proven by the fixture. Run it from an
idle T3 with a bounded, non-mutating command.
bin.ts; invocation is documented in the README.