@hwip/cli 0.1.2 → 0.1.3
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/listener.d.ts +10 -0
- package/dist/listener.js +35 -9
- package/dist/session-registry.d.ts +22 -0
- package/dist/session-registry.js +46 -3
- package/package.json +1 -1
package/dist/listener.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { type HwipConfig } from './config.js';
|
|
24
24
|
import { type ShellAuditEntry } from './shell-audit.js';
|
|
25
|
+
import { type KnownSession } from './session-registry.js';
|
|
25
26
|
import { type AgentKind } from './remote-agents.js';
|
|
26
27
|
import { type TranscriptSource } from './transcript-tail.js';
|
|
27
28
|
import { type AgentState } from './agent-state.js';
|
|
@@ -219,6 +220,8 @@ export interface ReportedKnownSession {
|
|
|
219
220
|
/** The seq block the run's chunks took (Q24): the server reads its conversation from that key range alone. */
|
|
220
221
|
firstSeq?: number;
|
|
221
222
|
lastSeq?: number;
|
|
223
|
+
/** The later tmux sessions the run went on in (Q35): the server ends another run's push window at their start. */
|
|
224
|
+
tmuxRunIds?: string[];
|
|
222
225
|
lastSeenAt: string;
|
|
223
226
|
}
|
|
224
227
|
/** Each live run's first seq, for the report (Q24). A run that has recorded nothing yet has none. */
|
|
@@ -296,6 +299,13 @@ export declare const __checkoutCacheSize: () => number;
|
|
|
296
299
|
* must agree: `epochToIso` keeps whole seconds and this floors them.
|
|
297
300
|
*/
|
|
298
301
|
export declare const runIdOfEpoch: (epoch: string | undefined) => string | undefined;
|
|
302
|
+
/**
|
|
303
|
+
* Each live session's run once the sweep has read the conversations (hwip
|
|
304
|
+
* docs/session-runs.md §3): a session carrying on an earlier run's
|
|
305
|
+
* conversation reports — and its recorder tags — that run instead of its new
|
|
306
|
+
* tmux session. `sessions` and `recordable` are this sweep's, changed in place.
|
|
307
|
+
*/
|
|
308
|
+
export declare const continueRuns: (sessions: AgentSession[], recordable: Record<string, RecordableSession>, conversationOf: (name: string) => string | null | undefined, known?: readonly KnownSession[]) => void;
|
|
299
309
|
/** Test hook. */
|
|
300
310
|
export declare const resetSessionStates: () => void;
|
|
301
311
|
/**
|
package/dist/listener.js
CHANGED
|
@@ -25,9 +25,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.
|
|
29
|
-
exports.
|
|
30
|
-
exports.handleListener = exports.liveScheduleRunnerDeps = exports.listenerSettings = exports.resolveWsUrl = exports.streamSession = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.displayFolder = exports.claimLanFile = exports.saveFileFromS3 = exports.downloadSealedEach = exports.answerFileBound = exports.downloadAttachments = exports.setAttachmentContext = exports.shellTextRefusal = void 0;
|
|
28
|
+
exports.handleScreenRequest = exports.setSubscriberTrust = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.withSettledPrompts = exports.PROMPT_SEAL_WAIT_MS = exports.withBlockedPrompts = exports.setPromptSealer = exports.deriveSessionState = exports.resetSessionStates = exports.continueRuns = exports.runIdOfEpoch = exports.__checkoutCacheSize = exports.parseSessionName = exports.tmuxTargetFor = exports.subagentTranscriptFor = exports.recordTargets = exports.recordSubagentTranscripts = exports.isSubagentSessionName = exports.detectRemoteAgent = exports.sessionLiveness = exports.sessionExists = exports.liveSessionNames = exports.tmuxArgs = exports.silenceListenerLog = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.probePane = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.commandsReportDue = exports.commandsFingerprint = exports.COMMAND_SCAN_INTERVAL_MS = exports.sessionsReportDue = exports.sessionsFingerprint = exports.knownSessionsFingerprint = exports.knownSessionsToReport = exports.withRunSeqs = exports.KNOWN_SESSIONS_REPORTED = exports.staleConnectionAction = exports.askedToReconnect = exports.MAX_STALE_CONNECTION_DROPS = exports.createReportAckWatch = exports.REPORT_ACK_TIMEOUT_MS = exports.SESSION_REPORT_HEARTBEAT_MS = exports.SESSION_REPORT_INTERVAL_MS = exports.heartbeatVerdict = exports.pingDue = exports.PING_IDLE_MS = void 0;
|
|
29
|
+
exports.isInventoried = exports.syncTmuxAgentNames = exports.recordInventory = exports.collectSessionsVerbose = exports.handleCommandInput = exports.pendingInputDecrypts = exports.MAX_PENDING_DECRYPTS = exports.MAX_INPUT_LANES = exports.validateInputMessage = exports.MAX_INPUT_BODY_CHARS = exports.MAX_INPUT_KEYS = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.fitSealedFrame = exports.SEALED_FRAME_MAX_BYTES = exports.stopAllStreams = exports.stopStream = exports.stopStreamOf = exports.handleTurnWatch = exports.sealTurnBatch = exports.resolveWatchTranscript = exports.MAX_CONCURRENT_STREAMS = exports.STREAM_RENEW_FRESH_MS = exports.STREAM_LEASE_MS = exports.claimFrameSend = exports.hasFrameBudget = exports.streamCadence = exports.MAX_FRAMES_PER_SEC = exports.BURST_WINDOW_MS = exports.BURST_INTERVAL_MS = exports.STREAM_INTERVAL_MS = exports.STREAM_CAPTURE_LINES = exports.scheduleInjectSnapshots = exports.historyLinesIn = exports.cursorLineFor = exports.parsePaneGeometry = exports.STREAM_GEOMETRY_FORMAT = exports.handleScreenHistoryRequest = exports.SCREEN_HISTORY_MAX_LINES = exports.handleSessionForgetRequest = exports.handleSessionExitRequest = exports.SESSION_EXIT_STEP_MS = exports.handleSessionResumeRequest = exports.handleScheduleListRequest = exports.handleFileBlobRequest = exports.handleFileTreeRequest = exports.handleDiffFileRequest = exports.handleDiffFilesRequest = exports.fileViewBytes = void 0;
|
|
30
|
+
exports.handleListener = exports.liveScheduleRunnerDeps = exports.listenerSettings = exports.resolveWsUrl = exports.streamSession = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.displayFolder = exports.claimLanFile = exports.saveFileFromS3 = exports.downloadSealedEach = exports.answerFileBound = exports.downloadAttachments = exports.setAttachmentContext = exports.shellTextRefusal = exports.writeRemoteMarker = void 0;
|
|
31
31
|
const child_process_1 = require("child_process");
|
|
32
32
|
const crypto_1 = require("crypto");
|
|
33
33
|
const fs_1 = require("fs");
|
|
@@ -318,11 +318,12 @@ const knownSessionsToReport = (live, now = Date.now(), seqs = (0, run_seqs_js_1.
|
|
|
318
318
|
...(e.runId ? { runId: e.runId } : {}),
|
|
319
319
|
...(e.startedAt ? { startedAt: e.startedAt } : {}),
|
|
320
320
|
...(e.runId ? seqs.get((0, run_seqs_js_1.runSeqsKey)(e.name, e.runId)) : undefined),
|
|
321
|
+
...(e.tmuxRunIds?.length ? { tmuxRunIds: e.tmuxRunIds } : {}),
|
|
321
322
|
lastSeenAt: e.lastSeenAt,
|
|
322
323
|
}));
|
|
323
324
|
};
|
|
324
325
|
exports.knownSessionsToReport = knownSessionsToReport;
|
|
325
|
-
const knownSessionsFingerprint = (known) => known.map((k) => `${k.name}${k.agentKind}${k.runId ?? ''}${k.lastSeenAt}${k.firstSeq ?? ''}${k.lastSeq ?? ''}`).sort().join('|');
|
|
326
|
+
const knownSessionsFingerprint = (known) => known.map((k) => `${k.name}${k.agentKind}${k.runId ?? ''}${k.lastSeenAt}${k.firstSeq ?? ''}${k.lastSeq ?? ''}${k.tmuxRunIds?.join(',') ?? ''}`).sort().join('|');
|
|
326
327
|
exports.knownSessionsFingerprint = knownSessionsFingerprint;
|
|
327
328
|
const sessionsFingerprint = (sessions) => sessions
|
|
328
329
|
.map((s) => JSON.stringify([
|
|
@@ -638,11 +639,33 @@ const epochToIso = (epoch) => {
|
|
|
638
639
|
* from `createdAt` for listeners that do not send one (`runIdOf`), so the two
|
|
639
640
|
* must agree: `epochToIso` keeps whole seconds and this floors them.
|
|
640
641
|
*/
|
|
641
|
-
const runIdOfEpoch = (epoch) =>
|
|
642
|
-
const iso = epochToIso(epoch);
|
|
643
|
-
return iso ? String(Math.floor(Date.parse(iso) / 1000)) : undefined;
|
|
644
|
-
};
|
|
642
|
+
const runIdOfEpoch = (epoch) => runIdOfIso(epochToIso(epoch));
|
|
645
643
|
exports.runIdOfEpoch = runIdOfEpoch;
|
|
644
|
+
/** The run id of the tmux session that started at `iso` (a session's `createdAt`). */
|
|
645
|
+
const runIdOfIso = (iso) => {
|
|
646
|
+
const ms = iso ? Date.parse(iso) : NaN;
|
|
647
|
+
return Number.isFinite(ms) ? String(Math.floor(ms / 1000)) : undefined;
|
|
648
|
+
};
|
|
649
|
+
/**
|
|
650
|
+
* Each live session's run once the sweep has read the conversations (hwip
|
|
651
|
+
* docs/session-runs.md §3): a session carrying on an earlier run's
|
|
652
|
+
* conversation reports — and its recorder tags — that run instead of its new
|
|
653
|
+
* tmux session. `sessions` and `recordable` are this sweep's, changed in place.
|
|
654
|
+
*/
|
|
655
|
+
const continueRuns = (sessions, recordable, conversationOf, known = (0, session_registry_js_1.knownSessions)()) => {
|
|
656
|
+
for (const s of sessions) {
|
|
657
|
+
if (s.parentName || !s.runId)
|
|
658
|
+
continue;
|
|
659
|
+
const runId = (0, session_registry_js_1.continuedRunId)(known, s.name, s.agentKind, s.runId, conversationOf(s.name));
|
|
660
|
+
if (runId === s.runId)
|
|
661
|
+
continue;
|
|
662
|
+
s.runId = runId;
|
|
663
|
+
const source = recordable[s.name];
|
|
664
|
+
if (source)
|
|
665
|
+
recordable[s.name] = { ...source, runId };
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
exports.continueRuns = continueRuns;
|
|
646
669
|
// ─── Agent state detection (SPEC-AGENT-AWARENESS §S1) ──────────────
|
|
647
670
|
// How many pane lines the state rules see. The live status area every
|
|
648
671
|
// agent renders sits in the last handful of lines; 40 leaves margin for
|
|
@@ -1597,7 +1620,8 @@ const handleSessionForgetRequest = (req, send) => {
|
|
|
1597
1620
|
// A name running again is not the run being forgotten: an ended run of it goes on its own (Q29).
|
|
1598
1621
|
// The running run itself, a request naming no run, or a run tmux could not tell apart stay refused.
|
|
1599
1622
|
if (liveness === 'running') {
|
|
1600
|
-
const
|
|
1623
|
+
const tmuxRun = runId ? (0, tmux_liveness_js_1.liveRunIdOf)(sessionName) : undefined;
|
|
1624
|
+
const live = tmuxRun ? (0, session_registry_js_1.runOfTmuxSession)(sessionName, tmuxRun) : undefined;
|
|
1601
1625
|
if (!runId || !live || live === runId)
|
|
1602
1626
|
return reply({ error: 'still_running' });
|
|
1603
1627
|
}
|
|
@@ -3566,6 +3590,7 @@ const collectSessionsVerbose = () => {
|
|
|
3566
3590
|
return undefined;
|
|
3567
3591
|
return resumeIdCount.get(id) === 1 ? id : null;
|
|
3568
3592
|
};
|
|
3593
|
+
(0, exports.continueRuns)(sessions, recordable, ownResumeId);
|
|
3569
3594
|
// Write down what each live session IS, while it still exists to be read.
|
|
3570
3595
|
// tmux forgets a session the moment it ends, which is exactly when the
|
|
3571
3596
|
// phone wants it back — and a resume must take its directory and its binary
|
|
@@ -3584,6 +3609,7 @@ const collectSessionsVerbose = () => {
|
|
|
3584
3609
|
label: s.label,
|
|
3585
3610
|
runId: s.runId,
|
|
3586
3611
|
startedAt: s.createdAt,
|
|
3612
|
+
tmuxRunId: runIdOfIso(s.createdAt),
|
|
3587
3613
|
agentSessionId: ownResumeId(s.name),
|
|
3588
3614
|
})));
|
|
3589
3615
|
return { sessions, rejected, targets, subagentTranscripts, recordable };
|
|
@@ -37,6 +37,12 @@ export interface KnownSession {
|
|
|
37
37
|
* agent whose id this machine cannot read, and in files written before it.
|
|
38
38
|
*/
|
|
39
39
|
agentSessionId?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The later tmux sessions this run went on in (`session_created`, as
|
|
42
|
+
* `runId`): the same conversation carried on after an `exit` is the same
|
|
43
|
+
* run (`continuedRunId`). Absent for a run that has had one tmux session.
|
|
44
|
+
*/
|
|
45
|
+
tmuxRunIds?: string[];
|
|
40
46
|
/** Last time this machine saw the session running. */
|
|
41
47
|
lastSeenAt: string;
|
|
42
48
|
}
|
|
@@ -86,6 +92,20 @@ export declare const recallSession: (name: string, now?: number) => KnownSession
|
|
|
86
92
|
* decides whether to fall back to the newest run or refuse.
|
|
87
93
|
*/
|
|
88
94
|
export declare const recallRun: (name: string, agentKind: string, now?: number) => KnownSession | null;
|
|
95
|
+
/** tmux sessions remembered per run: an `exit` and `-c` every few minutes all day stays a short list. */
|
|
96
|
+
export declare const MAX_TMUX_RUNS_PER_RUN = 10;
|
|
97
|
+
/**
|
|
98
|
+
* Which run the tmux session `tmuxRunId` of `name` is (hwip docs/session-runs.md §3).
|
|
99
|
+
*
|
|
100
|
+
* Its own, unless it carries on an earlier run: it was already joined to one
|
|
101
|
+
* (a sweep or a listener restart since), or the agent's conversation
|
|
102
|
+
* `conversationId` is the one an earlier run of the same name and agent last
|
|
103
|
+
* had. A tmux session that has had a conversation of its own stays its own run
|
|
104
|
+
* — a fresh start `/resume`d into an old conversation keeps what it wrote first.
|
|
105
|
+
*/
|
|
106
|
+
export declare const continuedRunId: (entries: readonly KnownSession[], name: string, agentKind: string, tmuxRunId: string, conversationId: string | null | undefined) => string;
|
|
107
|
+
/** The run going on in tmux session `tmuxRunId` of `name`: the one it was joined to, else its own. */
|
|
108
|
+
export declare const runOfTmuxSession: (name: string, tmuxRunId: string, now?: number) => string;
|
|
89
109
|
/**
|
|
90
110
|
* Make this process a reader of the registry: `rememberSessions` writes nothing from here on. For the
|
|
91
111
|
* CLI commands that look at the sweep (`hwip agent list|read`) — the file is the listener's resume
|
|
@@ -110,6 +130,8 @@ export declare const rememberSessions: (live: Array<{
|
|
|
110
130
|
label?: string | null;
|
|
111
131
|
runId?: string;
|
|
112
132
|
startedAt?: string;
|
|
133
|
+
/** The tmux session it runs in; one other than `runId` means the run goes on in it (`continuedRunId`). */
|
|
134
|
+
tmuxRunId?: string;
|
|
113
135
|
/**
|
|
114
136
|
* The conversation a resume may pick up. Absent: not readable this
|
|
115
137
|
* sweep, so the run keeps the one it had. Null: read, but not this
|
package/dist/session-registry.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* another program.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.sessionDirectoryExists = exports.knownSessionsPath = exports.isKnownSession = exports.forgetEnded = exports.forgetSession = exports.rememberSessions = exports.keepRegistryUntouched = exports.recallRun = exports.recallSession = exports.knownSessions = exports.runKey = exports.KNOWN_SESSION_TTL_MS = exports.MAX_RUNS_PER_NAME = exports.MAX_KNOWN_SESSIONS = void 0;
|
|
19
|
+
exports.sessionDirectoryExists = exports.knownSessionsPath = exports.isKnownSession = exports.forgetEnded = exports.forgetSession = exports.rememberSessions = exports.keepRegistryUntouched = exports.runOfTmuxSession = exports.continuedRunId = exports.MAX_TMUX_RUNS_PER_RUN = exports.recallRun = exports.recallSession = exports.knownSessions = exports.runKey = exports.KNOWN_SESSION_TTL_MS = exports.MAX_RUNS_PER_NAME = exports.MAX_KNOWN_SESSIONS = void 0;
|
|
20
20
|
const fs_1 = require("fs");
|
|
21
21
|
const path_1 = require("path");
|
|
22
22
|
const turn_ring_js_1 = require("./turn-ring.js");
|
|
@@ -35,6 +35,7 @@ exports.MAX_RUNS_PER_NAME = 10;
|
|
|
35
35
|
* likely to have moved than to be what the user meant. */
|
|
36
36
|
exports.KNOWN_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
37
37
|
const registryPath = () => (0, path_1.join)((0, gate_js_1.stateDir)(), 'known-sessions.json');
|
|
38
|
+
const isTmuxRunIds = (v) => v === undefined || (Array.isArray(v) && v.every((id) => typeof id === 'string'));
|
|
38
39
|
const readAll = () => {
|
|
39
40
|
try {
|
|
40
41
|
const raw = (0, fs_1.readFileSync)(registryPath(), 'utf-8');
|
|
@@ -49,7 +50,8 @@ const readAll = () => {
|
|
|
49
50
|
typeof e.cwd === 'string' &&
|
|
50
51
|
typeof e.agentKind === 'string' &&
|
|
51
52
|
typeof e.lastSeenAt === 'string' &&
|
|
52
|
-
(e.runId === undefined || typeof e.runId === 'string')
|
|
53
|
+
(e.runId === undefined || typeof e.runId === 'string') &&
|
|
54
|
+
isTmuxRunIds(e.tmuxRunIds));
|
|
53
55
|
}
|
|
54
56
|
catch {
|
|
55
57
|
return [];
|
|
@@ -109,6 +111,37 @@ exports.recallSession = recallSession;
|
|
|
109
111
|
*/
|
|
110
112
|
const recallRun = (name, agentKind, now = Date.now()) => (0, exports.knownSessions)(now).find((e) => e.name === name && e.agentKind === agentKind) ?? null;
|
|
111
113
|
exports.recallRun = recallRun;
|
|
114
|
+
/** tmux sessions remembered per run: an `exit` and `-c` every few minutes all day stays a short list. */
|
|
115
|
+
exports.MAX_TMUX_RUNS_PER_RUN = 10;
|
|
116
|
+
/** Agents whose own resume is measured to keep the conversation id: Claude Code 2.1.300's `-c` does. */
|
|
117
|
+
const CONTINUING_KINDS = new Set(['claude']);
|
|
118
|
+
/**
|
|
119
|
+
* Which run the tmux session `tmuxRunId` of `name` is (hwip docs/session-runs.md §3).
|
|
120
|
+
*
|
|
121
|
+
* Its own, unless it carries on an earlier run: it was already joined to one
|
|
122
|
+
* (a sweep or a listener restart since), or the agent's conversation
|
|
123
|
+
* `conversationId` is the one an earlier run of the same name and agent last
|
|
124
|
+
* had. A tmux session that has had a conversation of its own stays its own run
|
|
125
|
+
* — a fresh start `/resume`d into an old conversation keeps what it wrote first.
|
|
126
|
+
*/
|
|
127
|
+
const continuedRunId = (entries, name, agentKind, tmuxRunId, conversationId) => {
|
|
128
|
+
const ofName = entries.filter((e) => e.name === name && e.agentKind === agentKind && e.runId);
|
|
129
|
+
const joined = ofName.find((e) => e.runId !== tmuxRunId && e.tmuxRunIds?.includes(tmuxRunId));
|
|
130
|
+
if (joined?.runId)
|
|
131
|
+
return joined.runId;
|
|
132
|
+
if (!conversationId || !CONTINUING_KINDS.has(agentKind))
|
|
133
|
+
return tmuxRunId;
|
|
134
|
+
if (ofName.some((e) => e.runId === tmuxRunId && e.agentSessionId))
|
|
135
|
+
return tmuxRunId;
|
|
136
|
+
const earlier = ofName
|
|
137
|
+
.filter((e) => e.agentSessionId === conversationId && Number(e.runId) < Number(tmuxRunId))
|
|
138
|
+
.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))[0];
|
|
139
|
+
return earlier?.runId ?? tmuxRunId;
|
|
140
|
+
};
|
|
141
|
+
exports.continuedRunId = continuedRunId;
|
|
142
|
+
/** The run going on in tmux session `tmuxRunId` of `name`: the one it was joined to, else its own. */
|
|
143
|
+
const runOfTmuxSession = (name, tmuxRunId, now = Date.now()) => (0, exports.knownSessions)(now).find((e) => e.name === name && e.tmuxRunIds?.includes(tmuxRunId))?.runId ?? tmuxRunId;
|
|
144
|
+
exports.runOfTmuxSession = runOfTmuxSession;
|
|
112
145
|
let readOnly = false;
|
|
113
146
|
/**
|
|
114
147
|
* Make this process a reader of the registry: `rememberSessions` writes nothing from here on. For the
|
|
@@ -146,6 +179,10 @@ const rememberSessions = (live, now = Date.now()) => {
|
|
|
146
179
|
if (before && before.lastSeenAt >= s.startedAt)
|
|
147
180
|
byRun.delete((0, exports.runKey)(s.name, s.agentKind));
|
|
148
181
|
}
|
|
182
|
+
// Carried on in a new tmux session: what an earlier sweep wrote under that session is this run.
|
|
183
|
+
const continued = !!s.runId && !!s.tmuxRunId && s.tmuxRunId !== s.runId;
|
|
184
|
+
if (continued)
|
|
185
|
+
byRun.delete((0, exports.runKey)(s.name, s.agentKind, s.tmuxRunId));
|
|
149
186
|
const key = (0, exports.runKey)(s.name, s.agentKind, s.runId);
|
|
150
187
|
// A sweep that could not read the id — the agent has not written its
|
|
151
188
|
// record yet — keeps the one this run already had. Keyed by run and
|
|
@@ -154,6 +191,11 @@ const rememberSessions = (live, now = Date.now()) => {
|
|
|
154
191
|
const prev = byRun.get(key);
|
|
155
192
|
const keepPrev = s.agentSessionId === undefined && prev?.cwd === s.cwd;
|
|
156
193
|
const agentSessionId = s.agentSessionId || (keepPrev ? prev?.agentSessionId : undefined);
|
|
194
|
+
// A run keeps its first start whichever tmux session it goes on in.
|
|
195
|
+
const startedAt = continued ? prev?.startedAt ?? s.startedAt : s.startedAt;
|
|
196
|
+
const tmuxRunIds = continued && s.tmuxRunId
|
|
197
|
+
? [...(prev?.tmuxRunIds ?? []).filter((id) => id !== s.tmuxRunId), s.tmuxRunId].slice(-exports.MAX_TMUX_RUNS_PER_RUN)
|
|
198
|
+
: prev?.tmuxRunIds;
|
|
157
199
|
byRun.set(key, {
|
|
158
200
|
name: s.name,
|
|
159
201
|
cwd: s.cwd,
|
|
@@ -161,8 +203,9 @@ const rememberSessions = (live, now = Date.now()) => {
|
|
|
161
203
|
...(s.project ? { project: s.project } : {}),
|
|
162
204
|
...(s.label ? { label: s.label } : {}),
|
|
163
205
|
...(s.runId ? { runId: s.runId } : {}),
|
|
164
|
-
...(
|
|
206
|
+
...(startedAt ? { startedAt } : {}),
|
|
165
207
|
...(agentSessionId ? { agentSessionId } : {}),
|
|
208
|
+
...(tmuxRunIds?.length ? { tmuxRunIds } : {}),
|
|
166
209
|
lastSeenAt: seenAt,
|
|
167
210
|
});
|
|
168
211
|
}
|