instar 1.3.1302 → 1.3.1304
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.
- package/dist/cli.js +37 -18
- package/dist/cli.js.map +1 -1
- package/dist/core/SessionManager.d.ts +9 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +34 -2
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/processIdentity.d.ts +39 -0
- package/dist/core/processIdentity.d.ts.map +1 -0
- package/dist/core/processIdentity.js +72 -0
- package/dist/core/processIdentity.js.map +1 -0
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/feedback-factory/drain/FeedbackDrainStore.d.ts +6 -0
- package/dist/feedback-factory/drain/FeedbackDrainStore.d.ts.map +1 -1
- package/dist/feedback-factory/drain/FeedbackDrainStore.js +18 -5
- package/dist/feedback-factory/drain/FeedbackDrainStore.js.map +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +1 -119
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/lifeline/lifelineLock.d.ts +38 -0
- package/dist/lifeline/lifelineLock.d.ts.map +1 -0
- package/dist/lifeline/lifelineLock.js +153 -0
- package/dist/lifeline/lifelineLock.js.map +1 -0
- package/dist/lifeline/startupMarker.d.ts +3 -0
- package/dist/lifeline/startupMarker.d.ts.map +1 -1
- package/dist/lifeline/startupMarker.js +2 -0
- package/dist/lifeline/startupMarker.js.map +1 -1
- package/dist/monitoring/OrphanProcessReaper.d.ts +21 -1
- package/dist/monitoring/OrphanProcessReaper.d.ts.map +1 -1
- package/dist/monitoring/OrphanProcessReaper.js +59 -12
- package/dist/monitoring/OrphanProcessReaper.js.map +1 -1
- package/dist/threadline/PipeSessionSpawner.d.ts.map +1 -1
- package/dist/threadline/PipeSessionSpawner.js +66 -45
- package/dist/threadline/PipeSessionSpawner.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +37 -37
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/{1.3.1302.md → 1.3.1303.md} +26 -0
- package/upgrades/1.3.1304.md +89 -0
- package/upgrades/side-effects/feedback-drain-identity.md +78 -0
- package/upgrades/side-effects/test-instance-foreign-kills.md +261 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
An investigation into six `claude -p` builders SIGKILLed on the Mac Studio
|
|
9
|
+
(2026-09-29/30) did not identify their cause. It did find automatic kill
|
|
10
|
+
paths that picked their target by name, keyword or bare pid. None of those
|
|
11
|
+
proves the target is the agent's own. Each path now proves ownership first,
|
|
12
|
+
and when it cannot, it does not signal:
|
|
13
|
+
|
|
14
|
+
- New `src/core/processIdentity.ts`: a recorded pid counts as the same process
|
|
15
|
+
only while its start time (`ps -o lstart`) matches the recorded one.
|
|
16
|
+
- `instar lifeline restart` fallback (used when `launchctl kickstart` fails):
|
|
17
|
+
`lifeline.lock` and the startup marker now record `procStart`. The fallback
|
|
18
|
+
signals the lock holder and the marker pid only when each is proven to be
|
|
19
|
+
that same process, and it proves this again before SIGKILL. The old
|
|
20
|
+
`pkill -f '<agent>.*lifeline'` and the round-1 "command contains lifeline"
|
|
21
|
+
check are both gone. Legacy records without a start time are never
|
|
22
|
+
signalled.
|
|
23
|
+
- Lifeline startup takeover (`acquireLockFile`, now in
|
|
24
|
+
`src/lifeline/lifelineLock.ts`): a stuck holder is terminated only when
|
|
25
|
+
proven. A pid that started after the lock was written counts as reused, and
|
|
26
|
+
the lock is taken without signalling anyone. If ownership is unproven, the
|
|
27
|
+
lock is respected. That includes a start time `ps` cannot read: the holder
|
|
28
|
+
counts as dead only when the kernel confirms (ESRCH) nothing runs there.
|
|
29
|
+
- `PipeSessionSpawner`: records tmux `#{session_id}` + pane pid at creation. It
|
|
30
|
+
kills only that exact session, by id, and never a same-name replacement or a
|
|
31
|
+
stale pid's group. It signals the pane's process group only while tmux
|
|
32
|
+
reports the pane live; a retained dead pane (`remain-on-exit`) gets only its
|
|
33
|
+
session removed. It refuses to spawn over a `pipe-<threadId>` session it
|
|
34
|
+
did not start.
|
|
35
|
+
- `SessionManager.spawnTriageSession`: it replaces an existing session under
|
|
36
|
+
the triage name only when `ownsLiveTmuxSession()` proves that session is its
|
|
37
|
+
own. Proof means the session's `INSTAR_SESSION_ID` names this agent's record
|
|
38
|
+
for that name.
|
|
39
|
+
- `OrphanProcessReaper`: auto-kill requires `ownsLiveTmuxSession()`. A process
|
|
40
|
+
in a session that only reuses a recorded name is reported as external. Only
|
|
41
|
+
live panes map a process to a session. The start time seen when ownership
|
|
42
|
+
was established must still match before SIGTERM (unreadable sends nothing)
|
|
43
|
+
and before the delayed SIGKILL. Ownership is re-checked before the tmux
|
|
44
|
+
session is removed.
|
|
45
|
+
- Tests: `createTempProject().cleanup()` removes only sessions named after its
|
|
46
|
+
unique temp dir. The broad `cleanupTmuxSessions('akit-integ-'/'akit-sched-')`
|
|
47
|
+
calls are removed. `scheduler-basic` now waits for spawns that are still in
|
|
48
|
+
flight before cleanup. Its every-second cron could land a spawn after
|
|
49
|
+
cleanup and leak a dead `instar-test-*-job-fast-test-*` session, which was
|
|
50
|
+
seen twice during a full `npm test` run here.
|
|
51
|
+
|
|
52
|
+
## What to Tell Your User
|
|
53
|
+
|
|
54
|
+
Several of my automatic clean-up paths for stuck sessions and processes now
|
|
55
|
+
check that something is mine before they stop it. Before, a few repair paths went by a name or a
|
|
56
|
+
process number, and on a busy machine those can end up belonging to some
|
|
57
|
+
other program. If I can't prove something is mine, I leave it running.
|
|
58
|
+
|
|
59
|
+
## Summary of New Capabilities
|
|
60
|
+
|
|
61
|
+
- None new. Lifeline restart and takeover, pipe-session and triage cleanup,
|
|
62
|
+
and orphan cleanup now check that a process or session is their own before
|
|
63
|
+
stopping it, in the cases listed under Evidence.
|
|
64
|
+
|
|
65
|
+
## Evidence
|
|
66
|
+
|
|
67
|
+
- `tests/integration/foreign-process-survives-test-instance.test.ts` (real
|
|
68
|
+
tmux, 8 cases, including a retained dead pane that must get no signal). The orphan pass reaps an owned orphaned session. A foreign
|
|
69
|
+
`claude`-shaped process survives, and so does a foreign process in a session
|
|
70
|
+
that reuses a name the instance recorded. Fixture cleanup removes the
|
|
71
|
+
instance's own sessions and nothing else. Triage replaces its own session and
|
|
72
|
+
refuses a foreign one. The pipe spawner kills its own session, leaves a
|
|
73
|
+
same-name replacement and a stale pid alone, and refuses to spawn over a
|
|
74
|
+
foreign session. We restored each old behavior in turn: the reaper's name
|
|
75
|
+
match, the round-1 spawner, no triage guard, and the old helper. Each one
|
|
76
|
+
makes the matching case fail.
|
|
77
|
+
- `tests/unit/lifeline/lifeline-lock-identity.test.ts` (real child processes).
|
|
78
|
+
A stale lock whose pid now runs a foreign command mentioning "lifeline" is
|
|
79
|
+
neither a restart target nor signalled at startup. The own proven holder is
|
|
80
|
+
a target and is terminated when wedged. Making the identity check accept any
|
|
81
|
+
live pid fails 3 cases. A `ps` that always fails leaves a live holder's lock
|
|
82
|
+
in place, and a confirmed-dead holder's lock is still taken over.
|
|
83
|
+
- `tests/unit/orphan-reaper-pane-identity.test.ts` (scripted `ps`/tmux, no
|
|
84
|
+
real signals): a pid reused from a dead pane is not reaped; a changed or
|
|
85
|
+
unreadable start time sends nothing; a live owned orphan is still reaped.
|
|
86
|
+
- These tests cover the specific cases listed. They do not cover every kill
|
|
87
|
+
path in instar, and PID reuse is simulated rather than reproduced. The six
|
|
88
|
+
builder deaths remain unattributed; these paths are not ruled out as their
|
|
89
|
+
cause.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Side-Effects Review — feedback drain file identity drops st_dev
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `feedback-drain-identity`
|
|
4
|
+
**Date:** `2026-09-30`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required (one pure comparison function; checksum + epoch checks untouched)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`FeedbackDrainStore.sqliteFileIdentity()` recorded `dev:ino:birthtimeMs`. macOS APFS renumbers
|
|
11
|
+
`st_dev` across reboots/remounts. Live on the Mac Studio the checkpoint held
|
|
12
|
+
`16777231:454176:1787103639952.4048` while the untouched file now stats as dev `16777232`, same
|
|
13
|
+
inode and birth time. `restorePending()` saw a different identity, the checksum and epoch matched,
|
|
14
|
+
so it returned `true`: the drain refused every tick and checkpoint (274 reports waiting) and the
|
|
15
|
+
only exit was the PIN-gated failover-finalize route — a false operator escalation.
|
|
16
|
+
|
|
17
|
+
Change: identity is now `ino:birthtimeMs`. New exported pure helper `sameSqliteFileIdentity(recorded, current)`
|
|
18
|
+
compares a legacy three-part identity on its `ino:birthtime` parts. Both comparison sites
|
|
19
|
+
(`restorePending()` and `finalizeRestore()`) use it. Checksum and epoch checks are unchanged.
|
|
20
|
+
|
|
21
|
+
## Decision-point inventory
|
|
22
|
+
|
|
23
|
+
- Is the DB file the checkpointed one? — **modify** — compare on inode + birth time only.
|
|
24
|
+
|
|
25
|
+
## 1. Over-block
|
|
26
|
+
|
|
27
|
+
Less than before: a reboot or remount no longer makes the drain refuse work.
|
|
28
|
+
|
|
29
|
+
## 2. Under-block
|
|
30
|
+
|
|
31
|
+
A replacement file that keeps the same inode AND the same birth time would pass. A restore by copy
|
|
32
|
+
or rename produces a new inode and a new birth time (tested). The previous form had the same
|
|
33
|
+
exposure for same-device replacements; st_dev only added detection for a cross-volume move with a
|
|
34
|
+
colliding inode and birth time, which is not credible. On filesystems without birth time
|
|
35
|
+
(`birthtimeMs` 0) identity falls to the inode alone, the same as before within one device.
|
|
36
|
+
|
|
37
|
+
## 3. Level-of-abstraction fit
|
|
38
|
+
|
|
39
|
+
The fix lives in the one function that defines identity; no caller changes.
|
|
40
|
+
|
|
41
|
+
## 4. Signal vs authority compliance
|
|
42
|
+
|
|
43
|
+
Unchanged: restore detection still blocks via `DrainConflictError`/restorePending, with the same
|
|
44
|
+
checksum and epoch authority.
|
|
45
|
+
|
|
46
|
+
## 5. Interactions
|
|
47
|
+
|
|
48
|
+
`finalizeRestore()` requires the identity to DIFFER from the checkpoint; it now uses the same
|
|
49
|
+
comparison, so a same-file finalize is still refused and a genuine restore still finalizes
|
|
50
|
+
(existing destructive-restore test passes).
|
|
51
|
+
|
|
52
|
+
## 6. External surfaces
|
|
53
|
+
|
|
54
|
+
None. `/feedback-factory/drain/status` output shape unchanged; the stuck drain reports
|
|
55
|
+
`restorePending:false` after the update.
|
|
56
|
+
|
|
57
|
+
## 7. Multi-machine posture
|
|
58
|
+
|
|
59
|
+
Per-machine file identity; not replicated. No change.
|
|
60
|
+
|
|
61
|
+
## 7b. Constitutional Rules touched
|
|
62
|
+
|
|
63
|
+
Durable intake floor: restores the drain's intake without weakening restore detection.
|
|
64
|
+
Occam (Rule 116): one comparison helper, no new state or options.
|
|
65
|
+
|
|
66
|
+
## 8. Rollback cost
|
|
67
|
+
|
|
68
|
+
Release revert. Checkpoints written in the two-part form read as "different" to the old code,
|
|
69
|
+
which reproduces today's escalation, no data loss.
|
|
70
|
+
|
|
71
|
+
## Conclusion
|
|
72
|
+
|
|
73
|
+
Safe to ship. Tests: unit (st_dev change on the same file → not restored; replaced file → still
|
|
74
|
+
restored, new and legacy formats), integration backup-restore and e2e drain lifecycle all green.
|
|
75
|
+
|
|
76
|
+
## Evidence pointers
|
|
77
|
+
|
|
78
|
+
- `tests/unit/feedback-drain-store.test.ts` — new identity tests; the st_dev tests fail on the old code.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Side-Effects Review — automatic kills signal only what the agent can prove it started
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `test-instance-foreign-kills`
|
|
4
|
+
**Date:** `2026-09-30`
|
|
5
|
+
**Author:** `Echo (builder)`
|
|
6
|
+
**Second-pass reviewer:** `independent reviewer subagent (see below)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Round 3, after Astra's round-2 review (four must-fixes: M1 unreadable identity
|
|
11
|
+
read as a dead lifeline; M2 and M3 retained dead tmux panes; M4 overreaching
|
|
12
|
+
claims). The kill paths the reviews named now check ownership through one
|
|
13
|
+
small shared check before they signal, in the cases the tests below exercise.
|
|
14
|
+
Where a check cannot prove ownership, the path logs or reports and does not
|
|
15
|
+
signal.
|
|
16
|
+
|
|
17
|
+
Round 3 changes:
|
|
18
|
+
|
|
19
|
+
- `processIdentity.checkRecordedProcess`: an unreadable start time (ps denied,
|
|
20
|
+
timed out, unparseable) is `gone` only when `kill(pid, 0)` returns ESRCH;
|
|
21
|
+
otherwise `unproven`, so a live lifeline lock is kept (M1). In
|
|
22
|
+
`acquireLockFile`, if the holder's identity turns unreadable during the
|
|
23
|
+
SIGTERM grace, the new lifeline neither sends SIGKILL nor takes the lock
|
|
24
|
+
(found by the round-3 second-pass review).
|
|
25
|
+
- `PipeSessionSpawner.killPipeSession`: reads `#{pane_dead}`. The process
|
|
26
|
+
group is signalled only for an owned pane tmux reports live; an owned dead
|
|
27
|
+
or unreadable pane gets only `kill-session -t <session_id>`. The fallback
|
|
28
|
+
SIGKILL of the bare pane pid (when the pgid read failed) is removed (M2).
|
|
29
|
+
- `OrphanProcessReaper`: only live panes (`pane_dead=0`) map a pid to a
|
|
30
|
+
session. The start time read at classification is carried on the orphan and
|
|
31
|
+
must still match right before SIGTERM; null sends nothing and the tmux
|
|
32
|
+
session is left alone. Session ownership is re-checked before the terminal
|
|
33
|
+
`kill-session`. The operator-requested external kill is a separate mode of
|
|
34
|
+
`killProcess` (M3).
|
|
35
|
+
|
|
36
|
+
- `src/core/processIdentity.ts` (new, ~60 lines): `processStartMs(pid)` reads
|
|
37
|
+
`ps -o lstart=`. `checkRecordedProcess({pid, procStart, startedAt})` returns
|
|
38
|
+
one of four verdicts: `same` (the start time matches), `gone`, `reused` (the
|
|
39
|
+
live process started after the record was written), or `unproven`.
|
|
40
|
+
- `src/lifeline/lifelineLock.ts` (new; `acquireLockFile` moved here from
|
|
41
|
+
`TelegramLifeline.ts`): the lock records `procStart`. Takeover signals are
|
|
42
|
+
sent only on `same`, and identity is proven again before SIGKILL. On
|
|
43
|
+
`reused`, the new lifeline takes the lock without a signal. On `unproven`,
|
|
44
|
+
it respects the lock. `provenLifelineRecords()` selects the `lifeline
|
|
45
|
+
restart` targets.
|
|
46
|
+
- `src/lifeline/startupMarker.ts`: the marker records `procStart`.
|
|
47
|
+
- `src/cli.ts` (`lifeline restart` fallback): targets come from
|
|
48
|
+
`provenLifelineRecords(lock, marker)`, and SIGKILL goes only to targets
|
|
49
|
+
that are still `same` after the grace period. The round-1 keyword check is
|
|
50
|
+
removed.
|
|
51
|
+
- `src/threadline/PipeSessionSpawner.ts`: `new-session -P -F '#{session_id}
|
|
52
|
+
#{pane_pid}'` records the incarnation. `killPipeSession` signals the group
|
|
53
|
+
and runs `kill-session -t <session_id>` only while the named session still
|
|
54
|
+
lists that exact id and pid; otherwise it signals nothing. `spawn()` replaces
|
|
55
|
+
a same-name session only when it is recorded in `activeSessions`, and
|
|
56
|
+
otherwise refuses.
|
|
57
|
+
- `src/core/SessionManager.ts`: new `ownsLiveTmuxSession(name)`. It reads the
|
|
58
|
+
live session's `INSTAR_SESSION_ID` (every spawn path sets it) and checks
|
|
59
|
+
that this agent's state holds a record with that id for that tmux name.
|
|
60
|
+
`spawnTriageSession` refuses to kill an occupied name without that proof.
|
|
61
|
+
- `src/monitoring/OrphanProcessReaper.ts`: a historical-name match becomes
|
|
62
|
+
`instar-orphan` (auto-kill eligible) only with `ownsLiveTmuxSession`.
|
|
63
|
+
Without it, the process is classed `external` and only reported.
|
|
64
|
+
`killProcess` records the start time before SIGTERM and sends SIGKILL only
|
|
65
|
+
if it is unchanged.
|
|
66
|
+
- Tests: the broad `cleanupTmuxSessions('akit-integ-'/'akit-sched-')` calls are
|
|
67
|
+
removed. `scheduler-basic` waits for in-flight spawns before cleanup (a
|
|
68
|
+
late spawn leaked a dead-pane session). The regression test is rewritten with 7 real-tmux cases, both sides
|
|
69
|
+
of each decision, with teardown in `finally`. There is a new unit test with
|
|
70
|
+
real child processes for the lifeline identity.
|
|
71
|
+
|
|
72
|
+
## Decision-point inventory
|
|
73
|
+
|
|
74
|
+
- Lifeline restart target selection — modify — keyword → proven incarnation.
|
|
75
|
+
- Lifeline lock takeover — modify — any live pid → proven incarnation; reused → silent takeover; unproven → respect.
|
|
76
|
+
- Pipe kill / pipe spawn over an existing name — modify — name → recorded session id + pane pid; foreign name → refuse.
|
|
77
|
+
- Triage spawn over an existing name — modify — name → `ownsLiveTmuxSession`; foreign → refuse (throw).
|
|
78
|
+
- Orphan auto-kill eligibility — modify — historical name → name + live `INSTAR_SESSION_ID` match.
|
|
79
|
+
- Delayed SIGKILL escalations (reaper, lifeline) — modify — `kill(pid,0)` → same start time.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 1. Over-block
|
|
84
|
+
|
|
85
|
+
- A legacy lifeline lock or marker, written before this change and so with no
|
|
86
|
+
`procStart`, is never signalled by `lifeline restart`. At startup, a legacy
|
|
87
|
+
lock whose live pid started before the lock was written is respected, not
|
|
88
|
+
taken over. The first restart after the update normally goes through
|
|
89
|
+
launchd's `kickstart -k`, which does not use these paths. Once a lifeline
|
|
90
|
+
from this version is running, its records carry `procStart`.
|
|
91
|
+
- A process whose start time `ps` cannot read is not signalled. For the
|
|
92
|
+
lifeline lock it counts as gone only on a confirmed ESRCH; a live holder
|
|
93
|
+
whose start time cannot be read keeps its lock, so a new lifeline refuses to
|
|
94
|
+
start until `ps` works again.
|
|
95
|
+
- Pipe spawner: a live owned pane whose pgid cannot be read gets no process
|
|
96
|
+
signal; `kill-session` still hangs up the pane.
|
|
97
|
+
- Orphan reaper: an owned orphan whose start time cannot be read is skipped
|
|
98
|
+
(reported as "Skipped orphan"), not killed.
|
|
99
|
+
- Triage: if a session record was purged while its tmux session lived on, the
|
|
100
|
+
triage name stays occupied and the spawn throws. `TriageOrchestrator`
|
|
101
|
+
already handles a failed spawn. That is better than killing an unproven
|
|
102
|
+
session.
|
|
103
|
+
- Orphan reaper: an orphan whose record was purged, or whose tmux env lacks
|
|
104
|
+
`INSTAR_SESSION_ID` (a very old session), is only reported. The operator
|
|
105
|
+
can still use the explicit external-process API.
|
|
106
|
+
- Pipe spawner: a pipe session from before a server restart, which is no
|
|
107
|
+
longer in `activeSessions`, blocks a new pipe spawn for that thread. The
|
|
108
|
+
caller then falls through to the normal A2A path, as it does for any
|
|
109
|
+
`spawned:false`.
|
|
110
|
+
|
|
111
|
+
## 2. Under-block
|
|
112
|
+
|
|
113
|
+
- Start-time resolution is 1 s. A pid reused within the same second as the
|
|
114
|
+
original start would pass. That is not a realistic collision.
|
|
115
|
+
- Two SessionManagers sharing one agent state dir are treated as one agent
|
|
116
|
+
and may clean up each other's sessions. That is the same agent, which is
|
|
117
|
+
intended.
|
|
118
|
+
- The final ownership check and the signal are separate calls, a few
|
|
119
|
+
milliseconds apart. The reaper's terminal `kill-session` goes by name after
|
|
120
|
+
an ownership re-check.
|
|
121
|
+
- Only the paths named here were changed. Other kill paths (for example the
|
|
122
|
+
SessionWatchdog, and SessionManager's own passes) were not re-audited in
|
|
123
|
+
this change.
|
|
124
|
+
- This change does not identify what killed the six builders. Those deaths
|
|
125
|
+
remain unattributed, and these paths are not excluded as a possible cause.
|
|
126
|
+
|
|
127
|
+
## 3. Level-of-abstraction fit
|
|
128
|
+
|
|
129
|
+
There is one shared primitive for pids (`processIdentity`) and one for
|
|
130
|
+
sessions (`ownsLiveTmuxSession`, which uses the `INSTAR_SESSION_ID` every
|
|
131
|
+
spawn already sets). The pipe spawner uses tmux's own unique session id. No
|
|
132
|
+
new subsystem, store or loop is added.
|
|
133
|
+
|
|
134
|
+
## 4. Signal vs authority compliance
|
|
135
|
+
|
|
136
|
+
- [x] No — no block/allow surface on information flow. These checks narrow an
|
|
137
|
+
irreversible action (a signal) to proven targets. Each one removes reach and
|
|
138
|
+
none adds authority.
|
|
139
|
+
|
|
140
|
+
## 4b. Judgment-point check
|
|
141
|
+
|
|
142
|
+
The checks are deterministic safety floors on a SIGKILL. They are not
|
|
143
|
+
heuristics at a competing-signals decision point.
|
|
144
|
+
|
|
145
|
+
## 5. Interactions
|
|
146
|
+
|
|
147
|
+
- The SessionWatchdog already checks the parent pid, command and incarnation
|
|
148
|
+
before its descendant signals, and this change leaves it untouched.
|
|
149
|
+
SessionManager's own passes iterate its own records.
|
|
150
|
+
- The reaper's `trackedNow` path to `terminateSession` is unchanged.
|
|
151
|
+
Ownership proof only gates the non-tracked, historical-record path.
|
|
152
|
+
- `TelegramLifeline.releaseLockFile` still compares pid only when removing its
|
|
153
|
+
own lock file (a file, not a signal). That is unchanged.
|
|
154
|
+
- `acquireLockFile` behavior for a dead pid (take over) and a fresh proven
|
|
155
|
+
holder (respect) is unchanged.
|
|
156
|
+
|
|
157
|
+
## 6. External surfaces
|
|
158
|
+
|
|
159
|
+
- New optional JSON fields: `procStart` in `state/lifeline.lock` and
|
|
160
|
+
`state/lifeline-started-at.json`. Readers ignore unknown fields, and
|
|
161
|
+
`readStartupMarker` keeps its existing validation.
|
|
162
|
+
- A new `spawned:false` reason from the pipe spawner, and a new triage spawn
|
|
163
|
+
error message.
|
|
164
|
+
- Orphan reaper reports can now list a process as `external` with the reason
|
|
165
|
+
"ownership unproven".
|
|
166
|
+
|
|
167
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
168
|
+
|
|
169
|
+
Machine-local BY DESIGN. Pids, start times and tmux sessions are per-host, and
|
|
170
|
+
every check is on the host that would send the signal.
|
|
171
|
+
|
|
172
|
+
## 8. Rollback cost
|
|
173
|
+
|
|
174
|
+
Revert and ship a patch release. The new JSON fields are additive, and older
|
|
175
|
+
code ignores them. No migration is needed.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Conclusion
|
|
180
|
+
|
|
181
|
+
Clear to ship pending review. In the cases the tests exercise, the paths
|
|
182
|
+
Astra named signal only a proven incarnation, and the positive cleanup cases
|
|
183
|
+
still work. The limits in section 2 remain.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Second-pass review (if required)
|
|
188
|
+
|
|
189
|
+
**Round 3 reviewer:** independent reviewer subagent (general-purpose,
|
|
190
|
+
read-only). **Verdict: Concur.** M1-M4 closed in the code read; no automatic
|
|
191
|
+
path read signals an unproven process. Its minor points: (a) identity turning
|
|
192
|
+
unreadable during the lifeline SIGTERM grace fell through to a takeover —
|
|
193
|
+
fixed in this round, with a test; (b) the reaper reads `procStart` just after
|
|
194
|
+
the ownership check, not from the original `ps` scan (documented limit);
|
|
195
|
+
(c) the integration test's teardown killed detached children on an `alive`
|
|
196
|
+
check only — it now also requires the recorded start time.
|
|
197
|
+
|
|
198
|
+
**Round 2 reviewer:** independent reviewer subagent (general-purpose, read-only).
|
|
199
|
+
**Verdict: Concur with the review** (round 2). Astra's round-2 review then
|
|
200
|
+
found M1-M3 below, so this verdict did not hold; the round-3 fixes await
|
|
201
|
+
Astra's next review.
|
|
202
|
+
|
|
203
|
+
- Round-2 reviewer's claim (superseded): all six must-fixes are closed in the
|
|
204
|
+
touched code. No touched path lets a
|
|
205
|
+
name, keyword or bare pid alone authorize an automatic signal. The lifeline
|
|
206
|
+
restart and takeover signal only on `same`, and SIGKILL re-checks. Pipe
|
|
207
|
+
teardown needs an exact id+pid match and kills by id. Triage throws on a
|
|
208
|
+
session it does not own. The reaper's name-only matches are report-only,
|
|
209
|
+
and its SIGKILL re-checks the start time.
|
|
210
|
+
- `ownsLiveTmuxSession` checks the id against a regex before
|
|
211
|
+
`StateManager.getSession`, so its `validateKey` cannot throw. All four spawn
|
|
212
|
+
paths set `INSTAR_SESSION_ID`.
|
|
213
|
+
- The over-block and under-block lists are accurate.
|
|
214
|
+
- Minor residuals, none blocking. After proving ownership, the reaper kills
|
|
215
|
+
the tmux session by name, which leaves a window of milliseconds. Its first
|
|
216
|
+
SIGTERM goes to a pid from the same scan. A wedged legacy lifeline is left to
|
|
217
|
+
launchd's `kickstart -k`, as section 1 says.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Evidence pointers
|
|
222
|
+
|
|
223
|
+
- `tests/integration/foreign-process-survives-test-instance.test.ts`: 7/7
|
|
224
|
+
pass. We restored each old behavior in turn: the reaper name match, the
|
|
225
|
+
round-1 `PipeSessionSpawner.ts`, no triage guard, and the pre-fix
|
|
226
|
+
`tests/helpers/setup.ts`. Each makes its matching case fail (1, 2, 1 and 1
|
|
227
|
+
failures).
|
|
228
|
+
- `tests/unit/lifeline/lifeline-lock-identity.test.ts`: 7/7 pass. With
|
|
229
|
+
`checkRecordedProcess` changed to accept any live pid, 3 fail. The legacy
|
|
230
|
+
"respect" case does not discriminate on its own, because a fresh child cannot
|
|
231
|
+
carry a 5-minute-old lock. Its verdict is covered by the `checkRecordedProcess` case.
|
|
232
|
+
- Round 3: `tests/unit/lifeline/lifeline-lock-identity.test.ts` adds a `ps`
|
|
233
|
+
that always fails on PATH: a live holder is `unproven` and keeps its lock; a
|
|
234
|
+
holder confirmed dead (ESRCH) is `gone` and the lock is taken over.
|
|
235
|
+
- Round 3: a holder that ignores SIGTERM, with `ps` failing after the first
|
|
236
|
+
identity check: `acquireLockFile` returns false, sends no SIGKILL, and the
|
|
237
|
+
lock is unchanged. This case fails against the round-2 `lifelineLock.ts`.
|
|
238
|
+
- Round 3: the integration file adds a real retained dead pane
|
|
239
|
+
(`remain-on-exit`, its pane process killed by exact pid): `process.kill` is
|
|
240
|
+
never called and the owned session is removed. With the round-2 spawner this
|
|
241
|
+
case fails. The live-pane positive case now also asserts the pane process
|
|
242
|
+
exits. 8/8 pass.
|
|
243
|
+
- Round 3: `tests/unit/orphan-reaper-pane-identity.test.ts` (scripted ps/tmux,
|
|
244
|
+
stubbed `process.kill`): a foreign pid reusing a dead or indeterminate
|
|
245
|
+
pane's pid is not an orphan and gets no signal; the same pid in a live owned
|
|
246
|
+
pane is reaped (SIGTERM then SIGKILL, ownership re-checked before tmux
|
|
247
|
+
cleanup); a changed or unreadable start time sends nothing; the operator
|
|
248
|
+
kill still works. All six fail against the round-2 reaper. PID reuse itself
|
|
249
|
+
is simulated, not reproduced.
|
|
250
|
+
- Round-1 live check: a foreign `claude -p … fix the lifeline` process survived
|
|
251
|
+
the `lifeline restart` fallback.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Class-Closure Declaration (display-only mirror)
|
|
256
|
+
|
|
257
|
+
- **`defectClass`** — `unbounded-self-action`
|
|
258
|
+
- **`closure`** — `n/a` (negative declaration)
|
|
259
|
+
- **`reason`** — No new trigger or loop. The change only narrows which
|
|
260
|
+
pid or session existing kills may signal, and adds ownership proof before
|
|
261
|
+
each one.
|