@zgeoff/atc 0.1.11 → 0.1.12
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/README.md +4 -3
- package/package.json +1 -1
- package/src/daemon.ts +18 -7
- package/src/sessions.ts +24 -14
- package/src/state-store.ts +12 -3
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ fine nested inside zellij/tmux (give the pane locked mode so Ctrl-Space reaches
|
|
|
56
56
|
| `P` | overlay | revive: a fresh terminal resumes a headless or killed session in place |
|
|
57
57
|
| `y` | overlay | yank the resume command (`claude --resume <id>`, `grok --resume <id>`, or `codex resume <id>`) |
|
|
58
58
|
| `Y` | overlay | eject: yank the resume command, then kill the session here |
|
|
59
|
-
| `K` | overlay | kill selected (confirm with `y`) — the entry stays revivable with `P
|
|
59
|
+
| `K` | overlay | kill selected (confirm with `y`) — the entry stays revivable with `P`, even across daemon restarts; a second `K` forgets it |
|
|
60
60
|
| `?` | overlay | full key reference — the hint row only shows actions valid for the selected session |
|
|
61
61
|
| `u` | overlay | restart an outdated daemon and restore the fleet — offered only while `⟳ update ready` shows |
|
|
62
62
|
| `q` | home/overlay | quit the client — sessions keep running in the daemon |
|
|
@@ -186,8 +186,9 @@ restarts the daemon and restores the fleet at a moment you choose. Only a protoc
|
|
|
186
186
|
the two could miscommunicate, forces the restart immediately. The daemon continuously writes the
|
|
187
187
|
live fleet (name, cwd, agent, session id) to its SQLite store. If the daemon itself dies — crash,
|
|
188
188
|
SIGKILL, reboot — the child processes die with it, but every session's transcript is already on
|
|
189
|
-
disk. Start atc and press `R`: the whole fleet respawns with the matching CLI.
|
|
190
|
-
|
|
189
|
+
disk. Start atc and press `R`: the whole fleet respawns with the matching CLI. Killed sessions (`K`,
|
|
190
|
+
`Y` eject) stay in the fleet as exited entries — restore lists them as killed without booting a
|
|
191
|
+
terminal, and `P` still revives them. A second `K` forgets an entry for good.
|
|
191
192
|
|
|
192
193
|
Restoring shows the whole fleet immediately — every incoming session appears in the list marked
|
|
193
194
|
"waiting to restore" — and revives one at a time, most recently active first: the next resume starts
|
package/package.json
CHANGED
package/src/daemon.ts
CHANGED
|
@@ -615,29 +615,40 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
615
615
|
getEffectiveDims: (sessionID) =>
|
|
616
616
|
attachments.findEffectiveDims(sessionID) ?? ptyDims.get(sessionID) ?? { cols: 80, rows: 24 },
|
|
617
617
|
restoreFleet: (cols, rows) => {
|
|
618
|
-
const
|
|
618
|
+
const hasLiveSession = (agentSessionID: string) =>
|
|
619
619
|
mgr.sessions.some(
|
|
620
620
|
(s) =>
|
|
621
621
|
s.agentSessionID === agentSessionID &&
|
|
622
622
|
(s.pty !== null || (s.kind === 'headless' && s.state !== 'exited')),
|
|
623
623
|
);
|
|
624
624
|
|
|
625
|
+
const hasAnySession = (agentSessionID: string) =>
|
|
626
|
+
mgr.sessions.some((s) => s.agentSessionID === agentSessionID);
|
|
627
|
+
|
|
625
628
|
const recency = store.collectFleetRecency();
|
|
626
629
|
|
|
627
630
|
// Most recently active sessions revive first, so the ones the user was
|
|
628
631
|
// just working in come back before long-idle ones; entries that never
|
|
629
|
-
// reported an event keep their stored order at the end.
|
|
632
|
+
// reported an event keep their stored order at the end. Exited entries
|
|
633
|
+
// dedupe against every listed session, so repeated restores never
|
|
634
|
+
// double up the killed archive.
|
|
630
635
|
const entries = store
|
|
631
636
|
.loadFleet()
|
|
632
|
-
.filter((entry) =>
|
|
637
|
+
.filter((entry) =>
|
|
638
|
+
entry.exited === true
|
|
639
|
+
? !hasAnySession(entry.agentSessionID)
|
|
640
|
+
: !hasLiveSession(entry.agentSessionID),
|
|
641
|
+
)
|
|
633
642
|
.toSorted((a, b) =>
|
|
634
643
|
(recency.get(b.agentSessionID) ?? '').localeCompare(recency.get(a.agentSessionID) ?? ''),
|
|
635
644
|
);
|
|
636
645
|
|
|
637
646
|
// The whole fleet registers as terminal-less sessions up front, so the
|
|
638
647
|
// list shows every incoming session immediately instead of revealing
|
|
639
|
-
// them one boot at a time.
|
|
640
|
-
|
|
648
|
+
// them one boot at a time. Exited entries only register — they stay
|
|
649
|
+
// killed until revived by hand, so no terminal is adopted for them.
|
|
650
|
+
const registered = entries.map((entry) => mgr.restore(entry));
|
|
651
|
+
const queued = registered.filter((s) => s.state !== 'exited');
|
|
641
652
|
|
|
642
653
|
const adoptQueued = (s: Session): boolean => {
|
|
643
654
|
if (mgr.adoptTerminal(s.id, cols, rows) === null) {
|
|
@@ -656,7 +667,7 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
656
667
|
const [first, ...rest] = queued;
|
|
657
668
|
|
|
658
669
|
if (first === undefined) {
|
|
659
|
-
return
|
|
670
|
+
return registered.length;
|
|
660
671
|
}
|
|
661
672
|
|
|
662
673
|
// The first terminal attaches synchronously so a caller can attach at
|
|
@@ -682,7 +693,7 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
682
693
|
|
|
683
694
|
void adoptRest(firstBooted);
|
|
684
695
|
|
|
685
|
-
return
|
|
696
|
+
return registered.length;
|
|
686
697
|
},
|
|
687
698
|
};
|
|
688
699
|
|
package/src/sessions.ts
CHANGED
|
@@ -73,6 +73,7 @@ export interface FleetEntry {
|
|
|
73
73
|
readonly agent: AgentKind;
|
|
74
74
|
readonly pinned?: boolean;
|
|
75
75
|
readonly lastAttachedAt?: number;
|
|
76
|
+
readonly exited?: boolean;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
const fleetFile = join(stateDir, 'fleet.json');
|
|
@@ -111,6 +112,7 @@ export function parseFleetEntry(raw: unknown): FleetEntry | undefined {
|
|
|
111
112
|
agent: toAgentKind(raw['agent']),
|
|
112
113
|
...(raw['pinned'] === true ? { pinned: true } : {}),
|
|
113
114
|
...(typeof raw['lastAttachedAt'] === 'number' ? { lastAttachedAt: raw['lastAttachedAt'] } : {}),
|
|
115
|
+
...(raw['exited'] === true ? { exited: true } : {}),
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
118
|
|
|
@@ -206,17 +208,20 @@ export class SessionManager {
|
|
|
206
208
|
|
|
207
209
|
// Registers a fleet entry as a session with no terminal yet, so a
|
|
208
210
|
// fleet-wide restore can show every incoming session at once; adopting it
|
|
209
|
-
// later attaches the terminal.
|
|
211
|
+
// later attaches the terminal. Exited entries come back as killed
|
|
212
|
+
// sessions: still listed and revivable, never auto-adopted.
|
|
210
213
|
restore(entry: FleetEntry): Session {
|
|
214
|
+
const exited = entry.exited === true;
|
|
215
|
+
|
|
211
216
|
const session: Session = {
|
|
212
217
|
id: `s${++counter}-${Date.now().toString(36)}`,
|
|
213
218
|
name: entry.name,
|
|
214
219
|
cwd: entry.cwd,
|
|
215
220
|
kind: 'headless',
|
|
216
221
|
pty: null,
|
|
217
|
-
state: 'running',
|
|
222
|
+
state: exited ? 'exited' : 'running',
|
|
218
223
|
unread: false,
|
|
219
|
-
lastMsg: 'waiting to restore',
|
|
224
|
+
lastMsg: exited ? 'killed' : 'waiting to restore',
|
|
220
225
|
agentSessionID: entry.agentSessionID,
|
|
221
226
|
agent: entry.agent,
|
|
222
227
|
pinned: entry.pinned ?? false,
|
|
@@ -691,23 +696,28 @@ export class SessionManager {
|
|
|
691
696
|
|
|
692
697
|
// Persisted continuously so a crash (or quit) leaves a restorable fleet.
|
|
693
698
|
// Deliberate kills rewrite the file; unexpected session/atc deaths do not,
|
|
694
|
-
// so the last known fleet survives for `R` restore.
|
|
699
|
+
// so the last known fleet survives for `R` restore. Sessions whose
|
|
700
|
+
// terminal is gone persist as exited entries, so the killed archive
|
|
701
|
+
// survives a daemon restart too.
|
|
695
702
|
writeFleet() {
|
|
696
703
|
const fleet: FleetEntry[] = [];
|
|
697
704
|
|
|
698
705
|
for (const s of this.sessions) {
|
|
706
|
+
if (s.agentSessionID === undefined) {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
699
710
|
const live = s.pty !== null || (s.kind === 'headless' && s.state !== 'exited');
|
|
700
711
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
}
|
|
712
|
+
fleet.push({
|
|
713
|
+
name: s.name,
|
|
714
|
+
cwd: s.cwd,
|
|
715
|
+
agentSessionID: s.agentSessionID,
|
|
716
|
+
agent: s.agent,
|
|
717
|
+
...(s.pinned ? { pinned: true } : {}),
|
|
718
|
+
lastAttachedAt: s.lastAttachedAt,
|
|
719
|
+
...(live ? {} : { exited: true }),
|
|
720
|
+
});
|
|
711
721
|
}
|
|
712
722
|
|
|
713
723
|
this.store.writeFleet(fleet);
|
package/src/state-store.ts
CHANGED
|
@@ -28,7 +28,8 @@ export class StateStore {
|
|
|
28
28
|
cwd TEXT NOT NULL,
|
|
29
29
|
pinned INTEGER NOT NULL DEFAULT 0,
|
|
30
30
|
last_attached INTEGER,
|
|
31
|
-
agent TEXT NOT NULL DEFAULT 'claude'
|
|
31
|
+
agent TEXT NOT NULL DEFAULT 'claude',
|
|
32
|
+
exited INTEGER NOT NULL DEFAULT 0
|
|
32
33
|
);
|
|
33
34
|
CREATE TABLE IF NOT EXISTS events (
|
|
34
35
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -75,6 +76,10 @@ export class StateStore {
|
|
|
75
76
|
this.db.run("ALTER TABLE fleet ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude'");
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
if (!fleetColumns.has('exited')) {
|
|
80
|
+
this.db.run('ALTER TABLE fleet ADD COLUMN exited INTEGER NOT NULL DEFAULT 0');
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
if (legacyFleetPath !== undefined) {
|
|
79
84
|
this.adoptLegacyFleet(legacyFleetPath);
|
|
80
85
|
}
|
|
@@ -90,9 +95,10 @@ export class StateStore {
|
|
|
90
95
|
pinned: number;
|
|
91
96
|
last_attached: number | null;
|
|
92
97
|
agent: string | null;
|
|
98
|
+
exited: number;
|
|
93
99
|
},
|
|
94
100
|
[]
|
|
95
|
-
>('SELECT agent_session_id, name, cwd, pinned, last_attached, agent FROM fleet')
|
|
101
|
+
>('SELECT agent_session_id, name, cwd, pinned, last_attached, agent, exited FROM fleet')
|
|
96
102
|
.all();
|
|
97
103
|
|
|
98
104
|
const entries: FleetEntry[] = [];
|
|
@@ -105,6 +111,7 @@ export class StateStore {
|
|
|
105
111
|
agent: toAgentKind(row.agent),
|
|
106
112
|
...(row.pinned === 0 ? {} : { pinned: true }),
|
|
107
113
|
...(row.last_attached === null ? {} : { lastAttachedAt: row.last_attached }),
|
|
114
|
+
...(row.exited === 0 ? {} : { exited: true }),
|
|
108
115
|
});
|
|
109
116
|
}
|
|
110
117
|
|
|
@@ -128,11 +135,12 @@ export class StateStore {
|
|
|
128
135
|
this.db.run('DELETE FROM fleet');
|
|
129
136
|
|
|
130
137
|
const insert = this.db.query(
|
|
131
|
-
'INSERT OR REPLACE INTO fleet (agent_session_id, name, cwd, pinned, last_attached, agent) VALUES (?1, ?2, ?3, ?4, ?5, ?6)',
|
|
138
|
+
'INSERT OR REPLACE INTO fleet (agent_session_id, name, cwd, pinned, last_attached, agent, exited) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)',
|
|
132
139
|
);
|
|
133
140
|
|
|
134
141
|
for (const entry of all) {
|
|
135
142
|
const pinned = entry.pinned === true ? 1 : 0;
|
|
143
|
+
const exited = entry.exited === true ? 1 : 0;
|
|
136
144
|
|
|
137
145
|
insert.run(
|
|
138
146
|
entry.agentSessionID,
|
|
@@ -141,6 +149,7 @@ export class StateStore {
|
|
|
141
149
|
pinned,
|
|
142
150
|
entry.lastAttachedAt ?? null,
|
|
143
151
|
entry.agent,
|
|
152
|
+
exited,
|
|
144
153
|
);
|
|
145
154
|
}
|
|
146
155
|
});
|