github-issue-tower-defence-management 1.114.0 → 1.114.2
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/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/bin/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.js +36 -27
- package/bin/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +14 -14
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSessionRecordReader.js +69 -0
- package/bin/adapter/repositories/FileSystemSessionRecordReader.js.map +1 -0
- package/bin/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.js +9 -2
- package/bin/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.js.map +1 -1
- package/bin/domain/usecases/ResolveInteractiveLiveSessionsUseCase.js +43 -0
- package/bin/domain/usecases/ResolveInteractiveLiveSessionsUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/SessionRecordReader.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SessionRecordReader.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.test.ts +153 -26
- package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts +49 -29
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +57 -5
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +14 -14
- package/src/adapter/repositories/FileSystemSessionRecordReader.test.ts +59 -0
- package/src/adapter/repositories/FileSystemSessionRecordReader.ts +31 -0
- package/src/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.test.ts +51 -0
- package/src/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.ts +10 -1
- package/src/domain/entities/InteractiveLiveSession.ts +1 -0
- package/src/domain/entities/LiveSessionProcessSnapshot.ts +1 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +4 -0
- package/src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.test.ts +142 -59
- package/src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.ts +50 -0
- package/src/domain/usecases/adapter-interfaces/SessionRecordReader.ts +3 -0
- package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts +4 -11
- package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +8 -5
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSessionRecordReader.d.ts +5 -0
- package/types/adapter/repositories/FileSystemSessionRecordReader.d.ts.map +1 -0
- package/types/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.d.ts +3 -1
- package/types/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.d.ts.map +1 -1
- package/types/domain/entities/InteractiveLiveSession.d.ts +1 -0
- package/types/domain/entities/InteractiveLiveSession.d.ts.map +1 -1
- package/types/domain/entities/LiveSessionProcessSnapshot.d.ts +1 -0
- package/types/domain/entities/LiveSessionProcessSnapshot.d.ts.map +1 -1
- package/types/domain/usecases/ResolveInteractiveLiveSessionsUseCase.d.ts +13 -0
- package/types/domain/usecases/ResolveInteractiveLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SessionRecordReader.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/SessionRecordReader.d.ts.map +1 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LocalProcessLiveSessionProcessSnapshotProvider } from './LocalProcessLiveSessionProcessSnapshotProvider';
|
|
2
2
|
import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
3
3
|
import { ProcessEnvironReader } from '../../domain/usecases/adapter-interfaces/ProcessEnvironReader';
|
|
4
|
+
import { SessionRecordReader } from '../../domain/usecases/adapter-interfaces/SessionRecordReader';
|
|
4
5
|
|
|
5
6
|
describe('LocalProcessLiveSessionProcessSnapshotProvider', () => {
|
|
6
7
|
const makeRunner = (
|
|
@@ -29,6 +30,13 @@ describe('LocalProcessLiveSessionProcessSnapshotProvider', () => {
|
|
|
29
30
|
readEnviron: (pid: number) => byPid[pid] ?? null,
|
|
30
31
|
});
|
|
31
32
|
|
|
33
|
+
const makeSessionRecordReader = (
|
|
34
|
+
byPid: Record<number, string>,
|
|
35
|
+
): SessionRecordReader => ({
|
|
36
|
+
readCurrentSessionId: (_configDir: string, pid: number) =>
|
|
37
|
+
byPid[pid] ?? null,
|
|
38
|
+
});
|
|
39
|
+
|
|
32
40
|
it('builds a snapshot of sessions, pane pids, and processes with environment', async () => {
|
|
33
41
|
const runner = makeRunner({
|
|
34
42
|
'tmux list-sessions -F #{session_name}': {
|
|
@@ -56,9 +64,14 @@ describe('LocalProcessLiveSessionProcessSnapshotProvider', () => {
|
|
|
56
64
|
CLAUDE_CONFIG_DIR: '/config/workbench',
|
|
57
65
|
},
|
|
58
66
|
});
|
|
67
|
+
const sessionRecordReader = makeSessionRecordReader({
|
|
68
|
+
101: 'issue-rotated-uuid',
|
|
69
|
+
201: 'wb-rotated-uuid',
|
|
70
|
+
});
|
|
59
71
|
const provider = new LocalProcessLiveSessionProcessSnapshotProvider(
|
|
60
72
|
runner,
|
|
61
73
|
environReader,
|
|
74
|
+
sessionRecordReader,
|
|
62
75
|
);
|
|
63
76
|
|
|
64
77
|
const snapshot = await provider.getSnapshot();
|
|
@@ -75,6 +88,7 @@ describe('LocalProcessLiveSessionProcessSnapshotProvider', () => {
|
|
|
75
88
|
ppid: 200,
|
|
76
89
|
commandLine: 'claude --name workbench',
|
|
77
90
|
sessionId: 'wb-uuid',
|
|
91
|
+
currentSessionId: 'wb-rotated-uuid',
|
|
78
92
|
configDir: '/config/workbench',
|
|
79
93
|
});
|
|
80
94
|
expect(snapshot.processes).toContainEqual({
|
|
@@ -82,10 +96,44 @@ describe('LocalProcessLiveSessionProcessSnapshotProvider', () => {
|
|
|
82
96
|
ppid: 1,
|
|
83
97
|
commandLine: 'shell',
|
|
84
98
|
sessionId: null,
|
|
99
|
+
currentSessionId: null,
|
|
85
100
|
configDir: null,
|
|
86
101
|
});
|
|
87
102
|
});
|
|
88
103
|
|
|
104
|
+
it('leaves currentSessionId null when the session record is absent', async () => {
|
|
105
|
+
const runner = makeRunner({
|
|
106
|
+
'tmux list-sessions -F #{session_name}': { stdout: 'workbench\n' },
|
|
107
|
+
'tmux list-panes -t workbench -F #{pane_pid}': { stdout: '200\n' },
|
|
108
|
+
'ps -eo pid=,ppid=,args=': {
|
|
109
|
+
stdout: ' 201 200 claude --name workbench',
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const provider = new LocalProcessLiveSessionProcessSnapshotProvider(
|
|
113
|
+
runner,
|
|
114
|
+
makeEnvironReader({
|
|
115
|
+
201: {
|
|
116
|
+
CLAUDE_CODE_SESSION_ID: 'wb-uuid',
|
|
117
|
+
CLAUDE_CONFIG_DIR: '/config/workbench',
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
makeSessionRecordReader({}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const snapshot = await provider.getSnapshot();
|
|
124
|
+
|
|
125
|
+
expect(snapshot.processes).toEqual([
|
|
126
|
+
{
|
|
127
|
+
pid: 201,
|
|
128
|
+
ppid: 200,
|
|
129
|
+
commandLine: 'claude --name workbench',
|
|
130
|
+
sessionId: 'wb-uuid',
|
|
131
|
+
currentSessionId: null,
|
|
132
|
+
configDir: '/config/workbench',
|
|
133
|
+
},
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
89
137
|
it('returns an empty snapshot when tmux reports no sessions', async () => {
|
|
90
138
|
const runner = makeRunner({
|
|
91
139
|
'tmux list-sessions -F #{session_name}': { stdout: '' },
|
|
@@ -94,6 +142,7 @@ describe('LocalProcessLiveSessionProcessSnapshotProvider', () => {
|
|
|
94
142
|
const provider = new LocalProcessLiveSessionProcessSnapshotProvider(
|
|
95
143
|
runner,
|
|
96
144
|
makeEnvironReader({}),
|
|
145
|
+
makeSessionRecordReader({}),
|
|
97
146
|
);
|
|
98
147
|
|
|
99
148
|
const snapshot = await provider.getSnapshot();
|
|
@@ -121,6 +170,7 @@ describe('LocalProcessLiveSessionProcessSnapshotProvider', () => {
|
|
|
121
170
|
CLAUDE_CONFIG_DIR: '/config/workbench',
|
|
122
171
|
},
|
|
123
172
|
}),
|
|
173
|
+
makeSessionRecordReader({ 201: 'wb-rotated-uuid' }),
|
|
124
174
|
);
|
|
125
175
|
|
|
126
176
|
const snapshot = await provider.getSnapshot();
|
|
@@ -131,6 +181,7 @@ describe('LocalProcessLiveSessionProcessSnapshotProvider', () => {
|
|
|
131
181
|
ppid: 200,
|
|
132
182
|
commandLine: 'claude --name workbench',
|
|
133
183
|
sessionId: 'wb-uuid',
|
|
184
|
+
currentSessionId: 'wb-rotated-uuid',
|
|
134
185
|
configDir: '/config/workbench',
|
|
135
186
|
},
|
|
136
187
|
]);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
2
2
|
import { LiveSessionProcessSnapshotProvider } from '../../domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider';
|
|
3
3
|
import { ProcessEnvironReader } from '../../domain/usecases/adapter-interfaces/ProcessEnvironReader';
|
|
4
|
+
import { SessionRecordReader } from '../../domain/usecases/adapter-interfaces/SessionRecordReader';
|
|
4
5
|
import {
|
|
5
6
|
LiveSessionPaneProcess,
|
|
6
7
|
LiveSessionProcessInfo,
|
|
7
8
|
LiveSessionProcessSnapshot,
|
|
8
9
|
} from '../../domain/entities/LiveSessionProcessSnapshot';
|
|
10
|
+
import { FileSystemSessionRecordReader } from './FileSystemSessionRecordReader';
|
|
9
11
|
|
|
10
12
|
const SESSION_ID_ENV_KEY = 'CLAUDE_CODE_SESSION_ID';
|
|
11
13
|
const CONFIG_DIR_ENV_KEY = 'CLAUDE_CONFIG_DIR';
|
|
@@ -21,6 +23,7 @@ export class LocalProcessLiveSessionProcessSnapshotProvider implements LiveSessi
|
|
|
21
23
|
constructor(
|
|
22
24
|
private readonly localCommandRunner: LocalCommandRunner,
|
|
23
25
|
private readonly environReader: ProcessEnvironReader,
|
|
26
|
+
private readonly sessionRecordReader: SessionRecordReader = new FileSystemSessionRecordReader(),
|
|
24
27
|
) {}
|
|
25
28
|
|
|
26
29
|
getSnapshot = async (): Promise<LiveSessionProcessSnapshot> => {
|
|
@@ -80,12 +83,18 @@ export class LocalProcessLiveSessionProcessSnapshotProvider implements LiveSessi
|
|
|
80
83
|
const ppid = Number(match[2]);
|
|
81
84
|
const commandLine = match[3].trim();
|
|
82
85
|
const environ = this.environReader.readEnviron(pid);
|
|
86
|
+
const configDir = environ?.[CONFIG_DIR_ENV_KEY] ?? null;
|
|
87
|
+
const currentSessionId =
|
|
88
|
+
configDir === null
|
|
89
|
+
? null
|
|
90
|
+
: this.sessionRecordReader.readCurrentSessionId(configDir, pid);
|
|
83
91
|
processes.push({
|
|
84
92
|
pid,
|
|
85
93
|
ppid,
|
|
86
94
|
commandLine,
|
|
87
95
|
sessionId: environ?.[SESSION_ID_ENV_KEY] ?? null,
|
|
88
|
-
|
|
96
|
+
currentSessionId,
|
|
97
|
+
configDir,
|
|
89
98
|
});
|
|
90
99
|
}
|
|
91
100
|
return processes;
|
|
@@ -60,6 +60,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
60
60
|
const sessionFor = (sessionName: string): InteractiveLiveSession => ({
|
|
61
61
|
sessionName,
|
|
62
62
|
sessionId: `${sessionName}-uuid`,
|
|
63
|
+
candidateSessionIds: [`${sessionName}-uuid`],
|
|
63
64
|
configDir: `/config/${sessionName}`,
|
|
64
65
|
});
|
|
65
66
|
|
|
@@ -80,6 +81,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
80
81
|
ppid: 100 + index * 10,
|
|
81
82
|
commandLine: `claude --name ${sessionName}`,
|
|
82
83
|
sessionId: `${sessionName}-uuid`,
|
|
84
|
+
currentSessionId: `${sessionName}-uuid`,
|
|
83
85
|
configDir: `/config/${sessionName}`,
|
|
84
86
|
}));
|
|
85
87
|
return { sessions, processes };
|
|
@@ -299,6 +301,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
299
301
|
commandLine:
|
|
300
302
|
'claude --verbose -p Take ownership of https://example.com/issues/1 and finish it',
|
|
301
303
|
sessionId: 'aw-uuid',
|
|
304
|
+
currentSessionId: 'aw-uuid',
|
|
302
305
|
configDir: '/config/aw',
|
|
303
306
|
},
|
|
304
307
|
],
|
|
@@ -407,6 +410,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
407
410
|
expect(sessionFor('workbench')).toEqual({
|
|
408
411
|
sessionName: 'workbench',
|
|
409
412
|
sessionId: 'workbench-uuid',
|
|
413
|
+
candidateSessionIds: ['workbench-uuid'],
|
|
410
414
|
configDir: '/config/workbench',
|
|
411
415
|
});
|
|
412
416
|
});
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { ResolveInteractiveLiveSessionsUseCase } from './ResolveInteractiveLiveSessionsUseCase';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
LiveSessionProcessInfo,
|
|
4
|
+
LiveSessionProcessSnapshot,
|
|
5
|
+
} from '../entities/LiveSessionProcessSnapshot';
|
|
3
6
|
|
|
4
7
|
describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
5
8
|
const useCase = new ResolveInteractiveLiveSessionsUseCase();
|
|
6
9
|
|
|
10
|
+
const processInfo = (
|
|
11
|
+
overrides: Partial<LiveSessionProcessInfo> &
|
|
12
|
+
Pick<LiveSessionProcessInfo, 'pid' | 'ppid' | 'commandLine'>,
|
|
13
|
+
): LiveSessionProcessInfo => ({
|
|
14
|
+
sessionId: null,
|
|
15
|
+
currentSessionId: null,
|
|
16
|
+
configDir: null,
|
|
17
|
+
...overrides,
|
|
18
|
+
});
|
|
19
|
+
|
|
7
20
|
it('resolves an issue-url-named session through its pane child process', () => {
|
|
8
21
|
const snapshot: LiveSessionProcessSnapshot = {
|
|
9
22
|
sessions: [
|
|
@@ -13,20 +26,14 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
|
13
26
|
},
|
|
14
27
|
],
|
|
15
28
|
processes: [
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
ppid: 1,
|
|
19
|
-
commandLine: 'tmux pane shell',
|
|
20
|
-
sessionId: null,
|
|
21
|
-
configDir: null,
|
|
22
|
-
},
|
|
23
|
-
{
|
|
29
|
+
processInfo({ pid: 100, ppid: 1, commandLine: 'tmux pane shell' }),
|
|
30
|
+
processInfo({
|
|
24
31
|
pid: 101,
|
|
25
32
|
ppid: 100,
|
|
26
33
|
commandLine: 'claude --model opus --resume abc',
|
|
27
34
|
sessionId: 'abc',
|
|
28
35
|
configDir: '/config/issues-9',
|
|
29
|
-
},
|
|
36
|
+
}),
|
|
30
37
|
],
|
|
31
38
|
};
|
|
32
39
|
|
|
@@ -36,29 +43,51 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
|
36
43
|
{
|
|
37
44
|
sessionName: 'https_//github_com/owner/repo/issues/9',
|
|
38
45
|
sessionId: 'abc',
|
|
46
|
+
candidateSessionIds: ['abc'],
|
|
39
47
|
configDir: '/config/issues-9',
|
|
40
48
|
},
|
|
41
49
|
]);
|
|
42
50
|
});
|
|
43
51
|
|
|
44
|
-
it('
|
|
52
|
+
it('puts the rotated current session id before the launch env id', () => {
|
|
45
53
|
const snapshot: LiveSessionProcessSnapshot = {
|
|
46
54
|
sessions: [{ sessionName: 'workbench', panePids: [200] }],
|
|
47
55
|
processes: [
|
|
48
|
-
{
|
|
49
|
-
pid:
|
|
50
|
-
ppid:
|
|
51
|
-
commandLine: '
|
|
52
|
-
sessionId:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
processInfo({
|
|
57
|
+
pid: 201,
|
|
58
|
+
ppid: 200,
|
|
59
|
+
commandLine: 'claude --name workbench',
|
|
60
|
+
sessionId: 'launch-uuid',
|
|
61
|
+
currentSessionId: 'rotated-uuid',
|
|
62
|
+
configDir: '/config/workbench',
|
|
63
|
+
}),
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const result = useCase.resolve(snapshot);
|
|
68
|
+
|
|
69
|
+
expect(result).toEqual([
|
|
70
|
+
{
|
|
71
|
+
sessionName: 'workbench',
|
|
72
|
+
sessionId: 'launch-uuid',
|
|
73
|
+
candidateSessionIds: ['rotated-uuid', 'launch-uuid'],
|
|
74
|
+
configDir: '/config/workbench',
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('keeps a single candidate id when the current id equals the launch id', () => {
|
|
80
|
+
const snapshot: LiveSessionProcessSnapshot = {
|
|
81
|
+
sessions: [{ sessionName: 'workbench', panePids: [200] }],
|
|
82
|
+
processes: [
|
|
83
|
+
processInfo({
|
|
56
84
|
pid: 201,
|
|
57
85
|
ppid: 200,
|
|
58
86
|
commandLine: 'claude --model opus --name workbench',
|
|
59
87
|
sessionId: 'wb-uuid',
|
|
88
|
+
currentSessionId: 'wb-uuid',
|
|
60
89
|
configDir: '/config/workbench',
|
|
61
|
-
},
|
|
90
|
+
}),
|
|
62
91
|
],
|
|
63
92
|
};
|
|
64
93
|
|
|
@@ -68,6 +97,7 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
|
68
97
|
{
|
|
69
98
|
sessionName: 'workbench',
|
|
70
99
|
sessionId: 'wb-uuid',
|
|
100
|
+
candidateSessionIds: ['wb-uuid'],
|
|
71
101
|
configDir: '/config/workbench',
|
|
72
102
|
},
|
|
73
103
|
]);
|
|
@@ -77,27 +107,15 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
|
77
107
|
const snapshot: LiveSessionProcessSnapshot = {
|
|
78
108
|
sessions: [{ sessionName: 'control-room', panePids: [300] }],
|
|
79
109
|
processes: [
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
commandLine: 'shell',
|
|
84
|
-
sessionId: null,
|
|
85
|
-
configDir: null,
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
pid: 301,
|
|
89
|
-
ppid: 300,
|
|
90
|
-
commandLine: 'node wrapper',
|
|
91
|
-
sessionId: null,
|
|
92
|
-
configDir: null,
|
|
93
|
-
},
|
|
94
|
-
{
|
|
110
|
+
processInfo({ pid: 300, ppid: 1, commandLine: 'shell' }),
|
|
111
|
+
processInfo({ pid: 301, ppid: 300, commandLine: 'node wrapper' }),
|
|
112
|
+
processInfo({
|
|
95
113
|
pid: 302,
|
|
96
114
|
ppid: 301,
|
|
97
115
|
commandLine: 'claude --name control-room',
|
|
98
116
|
sessionId: 'cr-uuid',
|
|
99
117
|
configDir: '/config/control-room',
|
|
100
|
-
},
|
|
118
|
+
}),
|
|
101
119
|
],
|
|
102
120
|
};
|
|
103
121
|
|
|
@@ -107,6 +125,7 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
|
107
125
|
{
|
|
108
126
|
sessionName: 'control-room',
|
|
109
127
|
sessionId: 'cr-uuid',
|
|
128
|
+
candidateSessionIds: ['cr-uuid'],
|
|
110
129
|
configDir: '/config/control-room',
|
|
111
130
|
},
|
|
112
131
|
]);
|
|
@@ -116,21 +135,15 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
|
116
135
|
const snapshot: LiveSessionProcessSnapshot = {
|
|
117
136
|
sessions: [{ sessionName: 'aw-host', panePids: [400] }],
|
|
118
137
|
processes: [
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
ppid: 1,
|
|
122
|
-
commandLine: 'shell',
|
|
123
|
-
sessionId: null,
|
|
124
|
-
configDir: null,
|
|
125
|
-
},
|
|
126
|
-
{
|
|
138
|
+
processInfo({ pid: 400, ppid: 1, commandLine: 'shell' }),
|
|
139
|
+
processInfo({
|
|
127
140
|
pid: 401,
|
|
128
141
|
ppid: 400,
|
|
129
142
|
commandLine:
|
|
130
143
|
'claude --verbose -p Take ownership of https://example.com/issues/1 and finish it',
|
|
131
144
|
sessionId: 'aw-uuid',
|
|
132
145
|
configDir: '/config/aw',
|
|
133
|
-
},
|
|
146
|
+
}),
|
|
134
147
|
],
|
|
135
148
|
};
|
|
136
149
|
|
|
@@ -143,13 +156,12 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
|
143
156
|
const snapshot: LiveSessionProcessSnapshot = {
|
|
144
157
|
sessions: [{ sessionName: 'partial', panePids: [500] }],
|
|
145
158
|
processes: [
|
|
146
|
-
{
|
|
159
|
+
processInfo({
|
|
147
160
|
pid: 501,
|
|
148
161
|
ppid: 500,
|
|
149
162
|
commandLine: 'claude --model opus',
|
|
150
|
-
sessionId: null,
|
|
151
163
|
configDir: '/config/partial',
|
|
152
|
-
},
|
|
164
|
+
}),
|
|
153
165
|
],
|
|
154
166
|
};
|
|
155
167
|
|
|
@@ -162,13 +174,13 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
|
162
174
|
const snapshot: LiveSessionProcessSnapshot = {
|
|
163
175
|
sessions: [{ sessionName: 'monitored', panePids: [600] }],
|
|
164
176
|
processes: [
|
|
165
|
-
{
|
|
177
|
+
processInfo({
|
|
166
178
|
pid: 601,
|
|
167
179
|
ppid: 600,
|
|
168
180
|
commandLine: 'node monitor.js',
|
|
169
181
|
sessionId: 'mon-uuid',
|
|
170
182
|
configDir: '/config/monitor',
|
|
171
|
-
},
|
|
183
|
+
}),
|
|
172
184
|
],
|
|
173
185
|
};
|
|
174
186
|
|
|
@@ -184,20 +196,14 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
|
184
196
|
{ sessionName: 'empty', panePids: [800] },
|
|
185
197
|
],
|
|
186
198
|
processes: [
|
|
187
|
-
{
|
|
199
|
+
processInfo({
|
|
188
200
|
pid: 701,
|
|
189
201
|
ppid: 700,
|
|
190
202
|
commandLine: 'claude --name workbench',
|
|
191
203
|
sessionId: 'wb-uuid',
|
|
192
204
|
configDir: '/config/workbench',
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
pid: 801,
|
|
196
|
-
ppid: 800,
|
|
197
|
-
commandLine: 'bash idle',
|
|
198
|
-
sessionId: null,
|
|
199
|
-
configDir: null,
|
|
200
|
-
},
|
|
205
|
+
}),
|
|
206
|
+
processInfo({ pid: 801, ppid: 800, commandLine: 'bash idle' }),
|
|
201
207
|
],
|
|
202
208
|
};
|
|
203
209
|
|
|
@@ -207,11 +213,88 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
|
|
|
207
213
|
{
|
|
208
214
|
sessionName: 'workbench',
|
|
209
215
|
sessionId: 'wb-uuid',
|
|
216
|
+
candidateSessionIds: ['wb-uuid'],
|
|
210
217
|
configDir: '/config/workbench',
|
|
211
218
|
},
|
|
212
219
|
]);
|
|
213
220
|
});
|
|
214
221
|
|
|
222
|
+
it('collects the descendant-propagated session id after the own launch id for a non-resume session', () => {
|
|
223
|
+
const snapshot: LiveSessionProcessSnapshot = {
|
|
224
|
+
sessions: [{ sessionName: 'non-resume', panePids: [900] }],
|
|
225
|
+
processes: [
|
|
226
|
+
processInfo({ pid: 900, ppid: 1, commandLine: 'shell' }),
|
|
227
|
+
processInfo({
|
|
228
|
+
pid: 901,
|
|
229
|
+
ppid: 900,
|
|
230
|
+
commandLine: 'claude --model opus',
|
|
231
|
+
sessionId: 'launch-id',
|
|
232
|
+
configDir: '/config/non-resume',
|
|
233
|
+
}),
|
|
234
|
+
processInfo({
|
|
235
|
+
pid: 902,
|
|
236
|
+
ppid: 901,
|
|
237
|
+
commandLine: 'node tool worker',
|
|
238
|
+
sessionId: 'harness-id',
|
|
239
|
+
configDir: '/config/non-resume',
|
|
240
|
+
}),
|
|
241
|
+
processInfo({
|
|
242
|
+
pid: 903,
|
|
243
|
+
ppid: 902,
|
|
244
|
+
commandLine: 'node nested worker',
|
|
245
|
+
sessionId: 'harness-id',
|
|
246
|
+
configDir: '/config/non-resume',
|
|
247
|
+
}),
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const result = useCase.resolve(snapshot);
|
|
252
|
+
|
|
253
|
+
expect(result).toEqual([
|
|
254
|
+
{
|
|
255
|
+
sessionName: 'non-resume',
|
|
256
|
+
sessionId: 'launch-id',
|
|
257
|
+
candidateSessionIds: ['launch-id', 'harness-id'],
|
|
258
|
+
configDir: '/config/non-resume',
|
|
259
|
+
},
|
|
260
|
+
]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('orders the rotated current id, then the launch id, then the descendant id', () => {
|
|
264
|
+
const snapshot: LiveSessionProcessSnapshot = {
|
|
265
|
+
sessions: [{ sessionName: 'non-resume', panePids: [910] }],
|
|
266
|
+
processes: [
|
|
267
|
+
processInfo({ pid: 910, ppid: 1, commandLine: 'shell' }),
|
|
268
|
+
processInfo({
|
|
269
|
+
pid: 911,
|
|
270
|
+
ppid: 910,
|
|
271
|
+
commandLine: 'claude --model opus',
|
|
272
|
+
sessionId: 'launch-id',
|
|
273
|
+
currentSessionId: 'rotated-id',
|
|
274
|
+
configDir: '/config/non-resume',
|
|
275
|
+
}),
|
|
276
|
+
processInfo({
|
|
277
|
+
pid: 912,
|
|
278
|
+
ppid: 911,
|
|
279
|
+
commandLine: 'node tool worker',
|
|
280
|
+
sessionId: 'harness-id',
|
|
281
|
+
configDir: '/config/non-resume',
|
|
282
|
+
}),
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const result = useCase.resolve(snapshot);
|
|
287
|
+
|
|
288
|
+
expect(result).toEqual([
|
|
289
|
+
{
|
|
290
|
+
sessionName: 'non-resume',
|
|
291
|
+
sessionId: 'launch-id',
|
|
292
|
+
candidateSessionIds: ['rotated-id', 'launch-id', 'harness-id'],
|
|
293
|
+
configDir: '/config/non-resume',
|
|
294
|
+
},
|
|
295
|
+
]);
|
|
296
|
+
});
|
|
297
|
+
|
|
215
298
|
it('returns an empty list when there are no sessions', () => {
|
|
216
299
|
const result = useCase.resolve({ sessions: [], processes: [] });
|
|
217
300
|
|
|
@@ -66,15 +66,65 @@ export class ResolveInteractiveLiveSessionsUseCase {
|
|
|
66
66
|
) {
|
|
67
67
|
continue;
|
|
68
68
|
}
|
|
69
|
+
const candidateSessionIds = this.collectCandidateSessionIds(
|
|
70
|
+
interactiveProcess,
|
|
71
|
+
childrenByPpid,
|
|
72
|
+
);
|
|
69
73
|
sessions.push({
|
|
70
74
|
sessionName: session.sessionName,
|
|
71
75
|
sessionId: interactiveProcess.sessionId,
|
|
76
|
+
candidateSessionIds,
|
|
72
77
|
configDir: interactiveProcess.configDir,
|
|
73
78
|
});
|
|
74
79
|
}
|
|
75
80
|
return sessions;
|
|
76
81
|
};
|
|
77
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Collects the distinct session ids that may name the actively-written
|
|
85
|
+
* transcript on disk, in priority order. The rotated current session id
|
|
86
|
+
* recorded for the interactive process is first, because for a resumed or
|
|
87
|
+
* compacted session the id rotates and the live transcript is named by the
|
|
88
|
+
* current id. The interactive process's own launch id follows, then the
|
|
89
|
+
* distinct ids propagated to its descendant processes. For a `--resume`
|
|
90
|
+
* session these ids coincide, yielding a single id; for a non-resume session
|
|
91
|
+
* the own (launch) id names no transcript and the live transcript is named by
|
|
92
|
+
* the descendant-propagated id, which is included here so the resolver can
|
|
93
|
+
* find the actively-written file.
|
|
94
|
+
*/
|
|
95
|
+
private collectCandidateSessionIds = (
|
|
96
|
+
interactiveProcess: LiveSessionProcessInfo,
|
|
97
|
+
childrenByPpid: Map<number, LiveSessionProcessInfo[]>,
|
|
98
|
+
): string[] => {
|
|
99
|
+
const candidateSessionIds: string[] = [];
|
|
100
|
+
const seenSessionIds = new Set<string>();
|
|
101
|
+
const pushSessionId = (sessionId: string | null): void => {
|
|
102
|
+
if (sessionId !== null && !seenSessionIds.has(sessionId)) {
|
|
103
|
+
seenSessionIds.add(sessionId);
|
|
104
|
+
candidateSessionIds.push(sessionId);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
pushSessionId(interactiveProcess.currentSessionId);
|
|
108
|
+
const visitedPids = new Set<number>();
|
|
109
|
+
const queue: LiveSessionProcessInfo[] = [interactiveProcess];
|
|
110
|
+
let head = 0;
|
|
111
|
+
while (head < queue.length) {
|
|
112
|
+
const process = queue[head];
|
|
113
|
+
head += 1;
|
|
114
|
+
if (visitedPids.has(process.pid)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
visitedPids.add(process.pid);
|
|
118
|
+
pushSessionId(process.sessionId);
|
|
119
|
+
for (const child of childrenByPpid.get(process.pid) ?? []) {
|
|
120
|
+
if (!visitedPids.has(child.pid)) {
|
|
121
|
+
queue.push(child);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return candidateSessionIds;
|
|
126
|
+
};
|
|
127
|
+
|
|
78
128
|
private findInteractiveProcess = (
|
|
79
129
|
panePids: number[],
|
|
80
130
|
processByPid: Map<number, LiveSessionProcessInfo>,
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { InteractiveLiveSession } from '../../domain/entities/InteractiveLiveSession';
|
|
2
2
|
import { InteractiveLiveSessionTranscriptResolver } from '../../domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver';
|
|
3
|
-
/**
|
|
4
|
-
* Resolves the real transcript path of an interactive Claude Code session from
|
|
5
|
-
* the session id and config directory taken from the session's process
|
|
6
|
-
* environment. The transcript lives at
|
|
7
|
-
* `<configDir>/projects/<cwd-slug>/<sessionId>.jsonl`; this resolver scans the
|
|
8
|
-
* `projects` subdirectories for a file named `<sessionId>.jsonl` and returns the
|
|
9
|
-
* most recently modified match. Because resolution is keyed on the process
|
|
10
|
-
* session id rather than on a session name or issue URL, a plain-named session
|
|
11
|
-
* (for example one named `workbench`) resolves just as well as an issue-url
|
|
12
|
-
* named one.
|
|
13
|
-
*/
|
|
14
3
|
export declare class FileSystemInteractiveLiveSessionTranscriptResolver implements InteractiveLiveSessionTranscriptResolver {
|
|
4
|
+
private readonly sharedProjectsDirectory;
|
|
5
|
+
constructor(sharedProjectsDirectory?: string);
|
|
15
6
|
resolveTranscriptPaths: (sessions: InteractiveLiveSession[]) => Map<string, string>;
|
|
16
7
|
private resolveTranscriptPath;
|
|
8
|
+
private listProjectsDirectories;
|
|
9
|
+
private listCandidatePaths;
|
|
17
10
|
}
|
|
18
11
|
//# sourceMappingURL=FileSystemInteractiveLiveSessionTranscriptResolver.d.ts.map
|
package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileSystemInteractiveLiveSessionTranscriptResolver.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FileSystemInteractiveLiveSessionTranscriptResolver.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,sBAAsB,EAAE,MAAM,8CAA8C,CAAC;AACtF,OAAO,EAAE,wCAAwC,EAAE,MAAM,mFAAmF,CAAC;AAc7I,qBAAa,kDAAmD,YAAW,wCAAwC;IAE/G,OAAO,CAAC,QAAQ,CAAC,uBAAuB;gBAAvB,uBAAuB,GAAE,MAAyC;IAGrF,sBAAsB,GACpB,UAAU,sBAAsB,EAAE,KACjC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CASpB;IAEF,OAAO,CAAC,qBAAqB,CAyB3B;IAEF,OAAO,CAAC,uBAAuB,CAM7B;IAEF,OAAO,CAAC,kBAAkB,CAsBxB;CACH"}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { LiveSessionOutputActivity } from '../../domain/entities/LiveSessionOutputActivity';
|
|
2
2
|
import { SessionOutputActivityRepository } from '../../domain/usecases/adapter-interfaces/SessionOutputActivityRepository';
|
|
3
3
|
/**
|
|
4
|
-
* Reads the last main-session
|
|
4
|
+
* Reads the last main-session activity time for each live session from its
|
|
5
5
|
* already-resolved transcript path. Idle time is computed from the timestamp of
|
|
6
|
-
* the latest
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* the latest entry of any kind (assistant text, owner replies, tool results, or
|
|
7
|
+
* any other entry type) rather than from the transcript file modification time.
|
|
8
|
+
* Because a session that is actively running tool calls keeps appending entries
|
|
9
|
+
* such as `user` and `tool_result` even while it emits no assistant text, every
|
|
10
|
+
* entry with a parseable timestamp counts as activity, so a working session is
|
|
11
|
+
* not mistaken for a silent one.
|
|
9
12
|
*/
|
|
10
13
|
export declare class FileSystemSessionOutputActivityRepository implements SessionOutputActivityRepository {
|
|
11
14
|
listSessionOutputActivities: (transcriptPathBySessionName: Map<string, string>) => Promise<LiveSessionOutputActivity[]>;
|
|
12
|
-
private
|
|
15
|
+
private readLastActivityEpochSeconds;
|
|
13
16
|
}
|
|
14
17
|
//# sourceMappingURL=FileSystemSessionOutputActivityRepository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileSystemSessionOutputActivityRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC5F,OAAO,EAAE,+BAA+B,EAAE,MAAM,0EAA0E,CAAC;AAqB3H
|
|
1
|
+
{"version":3,"file":"FileSystemSessionOutputActivityRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC5F,OAAO,EAAE,+BAA+B,EAAE,MAAM,0EAA0E,CAAC;AAqB3H;;;;;;;;;GASG;AACH,qBAAa,yCAA0C,YAAW,+BAA+B;IAC/F,2BAA2B,GACzB,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAUrC;IAEF,OAAO,CAAC,4BAA4B,CAoClC;CACH"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { SessionRecordReader } from '../../domain/usecases/adapter-interfaces/SessionRecordReader';
|
|
2
|
+
export declare class FileSystemSessionRecordReader implements SessionRecordReader {
|
|
3
|
+
readCurrentSessionId: (configDir: string, pid: number) => string | null;
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=FileSystemSessionRecordReader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileSystemSessionRecordReader.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemSessionRecordReader.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,8DAA8D,CAAC;AAKnG,qBAAa,6BAA8B,YAAW,mBAAmB;IACvE,oBAAoB,GAAI,WAAW,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,GAAG,IAAI,CAqBpE;CACH"}
|