github-issue-tower-defence-management 1.110.1 → 1.111.1
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 +10 -9
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +9 -21
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +10 -15
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.js +104 -0
- package/bin/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.js.map +1 -0
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +55 -19
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.js +75 -0
- package/bin/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.js.map +1 -0
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +1 -1
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -1
- package/bin/adapter/repositories/ProcFsProcessEnvironReader.js +73 -0
- package/bin/adapter/repositories/ProcFsProcessEnvironReader.js.map +1 -0
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +8 -16
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +2 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/domain/entities/InteractiveLiveSession.js +3 -0
- package/bin/domain/entities/InteractiveLiveSession.js.map +1 -0
- package/bin/domain/entities/LiveSessionProcessSnapshot.js +3 -0
- package/bin/domain/entities/LiveSessionProcessSnapshot.js.map +1 -0
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +20 -33
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/bin/domain/usecases/ResolveInteractiveLiveSessionsUseCase.js +89 -0
- package/bin/domain/usecases/ResolveInteractiveLiveSessionsUseCase.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/ProcessEnvironReader.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/ProcessEnvironReader.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +5 -15
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +96 -204
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +19 -40
- package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.test.ts +147 -0
- package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts +75 -0
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +82 -44
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +62 -24
- package/src/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.test.ts +138 -0
- package/src/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.ts +93 -0
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +6 -4
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +1 -1
- package/src/adapter/repositories/ProcFsProcessEnvironReader.test.ts +60 -0
- package/src/adapter/repositories/ProcFsProcessEnvironReader.ts +38 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +61 -81
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +8 -25
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +29 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +4 -1
- package/src/domain/entities/InteractiveLiveSession.ts +5 -0
- package/src/domain/entities/LiveSessionProcessSnapshot.ts +17 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +125 -349
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +33 -55
- package/src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.test.ts +220 -0
- package/src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.ts +105 -0
- package/src/domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver.ts +7 -0
- package/src/domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider.ts +5 -0
- package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +1 -1
- package/src/domain/usecases/adapter-interfaces/ProcessEnvironReader.ts +3 -0
- package/src/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.ts +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +3 -7
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts +18 -0
- package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts.map +1 -0
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +9 -5
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.d.ts +21 -0
- package/types/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.d.ts.map +1 -0
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +1 -1
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/adapter/repositories/ProcFsProcessEnvironReader.d.ts +12 -0
- package/types/adapter/repositories/ProcFsProcessEnvironReader.d.ts.map +1 -0
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +2 -4
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/domain/entities/InteractiveLiveSession.d.ts +6 -0
- package/types/domain/entities/InteractiveLiveSession.d.ts.map +1 -0
- package/types/domain/entities/LiveSessionProcessSnapshot.d.ts +16 -0
- package/types/domain/entities/LiveSessionProcessSnapshot.d.ts.map +1 -0
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +6 -11
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ResolveInteractiveLiveSessionsUseCase.d.ts +23 -0
- package/types/domain/usecases/ResolveInteractiveLiveSessionsUseCase.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/ProcessEnvironReader.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/ProcessEnvironReader.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts.map +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { ProcessEnvironReader } from '../../domain/usecases/adapter-interfaces/ProcessEnvironReader';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads a process environment from the Linux procfs (`/proc/<pid>/environ`),
|
|
7
|
+
* where entries are NUL-separated `KEY=value` pairs. Returns null when the
|
|
8
|
+
* environment cannot be read (the process has exited or is not accessible).
|
|
9
|
+
*/
|
|
10
|
+
export class ProcFsProcessEnvironReader implements ProcessEnvironReader {
|
|
11
|
+
constructor(private readonly procDirectory: string = '/proc') {}
|
|
12
|
+
|
|
13
|
+
readEnviron = (pid: number): Record<string, string> | null => {
|
|
14
|
+
let raw: string;
|
|
15
|
+
try {
|
|
16
|
+
raw = fs.readFileSync(
|
|
17
|
+
path.join(this.procDirectory, String(pid), 'environ'),
|
|
18
|
+
'utf8',
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const environ: Record<string, string> = {};
|
|
24
|
+
for (const entry of raw.split('\0')) {
|
|
25
|
+
if (entry.length === 0) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const separatorIndex = entry.indexOf('=');
|
|
29
|
+
if (separatorIndex === -1) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const key = entry.slice(0, separatorIndex);
|
|
33
|
+
const value = entry.slice(separatorIndex + 1);
|
|
34
|
+
environ[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return environ;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -16,14 +16,14 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
16
16
|
fs.rmSync(rootDirectory, { force: true, recursive: true });
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const writeTranscript = (
|
|
20
|
-
const fileName = `${sessionName.replace(/\//g, '_')}.jsonl`;
|
|
19
|
+
const writeTranscript = (fileName: string, lines: object[]): string => {
|
|
21
20
|
const filePath = path.join(rootDirectory, fileName);
|
|
22
21
|
fs.writeFileSync(
|
|
23
22
|
filePath,
|
|
24
23
|
lines.map((line) => JSON.stringify(line)).join('\n'),
|
|
25
24
|
'utf8',
|
|
26
25
|
);
|
|
26
|
+
return filePath;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const assistantWithMarker = (timestamp: string): object => ({
|
|
@@ -63,141 +63,104 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
63
63
|
},
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
const sessionName = 'workbench';
|
|
67
|
+
|
|
66
68
|
it('reports a session as waiting when the last owner call is newer than the last owner reply', async () => {
|
|
67
|
-
const
|
|
68
|
-
writeTranscript(sessionName, [
|
|
69
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
69
70
|
ownerReply('2026-06-27T10:00:00.000Z'),
|
|
70
71
|
assistantWithMarker('2026-06-27T10:05:00.000Z'),
|
|
71
72
|
]);
|
|
72
|
-
const provider = new TranscriptOwnerCallStatusProvider(
|
|
73
|
-
rootDirectory,
|
|
74
|
-
'<<OWNER_CALL>>',
|
|
75
|
-
);
|
|
73
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
76
74
|
|
|
77
|
-
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
78
|
-
sessionName,
|
|
79
|
-
|
|
75
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
76
|
+
new Map([[sessionName, transcriptPath]]),
|
|
77
|
+
);
|
|
80
78
|
|
|
81
79
|
expect(result.has(sessionName)).toBe(true);
|
|
82
80
|
});
|
|
83
81
|
|
|
84
82
|
it('does not report a session as waiting when a later owner reply followed the call', async () => {
|
|
85
|
-
const
|
|
86
|
-
writeTranscript(sessionName, [
|
|
83
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
87
84
|
assistantWithMarker('2026-06-27T10:00:00.000Z'),
|
|
88
85
|
ownerReply('2026-06-27T10:05:00.000Z'),
|
|
89
86
|
]);
|
|
90
|
-
const provider = new TranscriptOwnerCallStatusProvider(
|
|
91
|
-
rootDirectory,
|
|
92
|
-
'<<OWNER_CALL>>',
|
|
93
|
-
);
|
|
87
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
94
88
|
|
|
95
|
-
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
96
|
-
sessionName,
|
|
97
|
-
|
|
89
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
90
|
+
new Map([[sessionName, transcriptPath]]),
|
|
91
|
+
);
|
|
98
92
|
|
|
99
93
|
expect(result.has(sessionName)).toBe(false);
|
|
100
94
|
});
|
|
101
95
|
|
|
102
96
|
it('compares full datetimes rather than only the time of day', async () => {
|
|
103
|
-
const
|
|
104
|
-
writeTranscript(sessionName, [
|
|
97
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
105
98
|
assistantWithMarker('2026-06-27T23:00:00.000Z'),
|
|
106
99
|
ownerReply('2026-06-28T01:00:00.000Z'),
|
|
107
100
|
]);
|
|
108
|
-
const provider = new TranscriptOwnerCallStatusProvider(
|
|
109
|
-
rootDirectory,
|
|
110
|
-
'<<OWNER_CALL>>',
|
|
111
|
-
);
|
|
101
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
112
102
|
|
|
113
|
-
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
114
|
-
sessionName,
|
|
115
|
-
|
|
103
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
104
|
+
new Map([[sessionName, transcriptPath]]),
|
|
105
|
+
);
|
|
116
106
|
|
|
117
107
|
expect(result.has(sessionName)).toBe(false);
|
|
118
108
|
});
|
|
119
109
|
|
|
120
110
|
it('treats tool results as not being owner replies', async () => {
|
|
121
|
-
const
|
|
122
|
-
writeTranscript(sessionName, [
|
|
111
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
123
112
|
assistantWithMarker('2026-06-27T10:00:00.000Z'),
|
|
124
113
|
toolResult('2026-06-27T10:05:00.000Z'),
|
|
125
114
|
]);
|
|
126
|
-
const provider = new TranscriptOwnerCallStatusProvider(
|
|
127
|
-
rootDirectory,
|
|
128
|
-
'<<OWNER_CALL>>',
|
|
129
|
-
);
|
|
115
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
130
116
|
|
|
131
|
-
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
132
|
-
sessionName,
|
|
133
|
-
|
|
117
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
118
|
+
new Map([[sessionName, transcriptPath]]),
|
|
119
|
+
);
|
|
134
120
|
|
|
135
121
|
expect(result.has(sessionName)).toBe(true);
|
|
136
122
|
});
|
|
137
123
|
|
|
138
124
|
it('does not report a session that never raised an owner call', async () => {
|
|
139
|
-
const
|
|
140
|
-
writeTranscript(sessionName, [
|
|
125
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
141
126
|
assistantPlain('2026-06-27T10:00:00.000Z'),
|
|
142
127
|
ownerReply('2026-06-27T10:05:00.000Z'),
|
|
143
128
|
]);
|
|
144
|
-
const provider = new TranscriptOwnerCallStatusProvider(
|
|
145
|
-
rootDirectory,
|
|
146
|
-
'<<OWNER_CALL>>',
|
|
147
|
-
);
|
|
129
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
148
130
|
|
|
149
|
-
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
150
|
-
sessionName,
|
|
151
|
-
]);
|
|
152
|
-
|
|
153
|
-
expect(result.has(sessionName)).toBe(false);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('returns an empty set when the root directory is null', async () => {
|
|
157
|
-
const provider = new TranscriptOwnerCallStatusProvider(
|
|
158
|
-
null,
|
|
159
|
-
'<<OWNER_CALL>>',
|
|
131
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
132
|
+
new Map([[sessionName, transcriptPath]]),
|
|
160
133
|
);
|
|
161
134
|
|
|
162
|
-
|
|
163
|
-
'https_//github_com/owner/repo/issues/9',
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
|
-
expect(result.size).toBe(0);
|
|
135
|
+
expect(result.has(sessionName)).toBe(false);
|
|
167
136
|
});
|
|
168
137
|
|
|
169
138
|
it('returns an empty set when the marker is null', async () => {
|
|
170
|
-
const
|
|
171
|
-
writeTranscript(sessionName, [
|
|
139
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
172
140
|
ownerReply('2026-06-27T10:00:00.000Z'),
|
|
173
141
|
assistantWithMarker('2026-06-27T10:05:00.000Z'),
|
|
174
142
|
]);
|
|
175
|
-
const provider = new TranscriptOwnerCallStatusProvider(
|
|
143
|
+
const provider = new TranscriptOwnerCallStatusProvider(null);
|
|
176
144
|
|
|
177
|
-
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
178
|
-
sessionName,
|
|
179
|
-
|
|
145
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
146
|
+
new Map([[sessionName, transcriptPath]]),
|
|
147
|
+
);
|
|
180
148
|
|
|
181
149
|
expect(result.size).toBe(0);
|
|
182
150
|
});
|
|
183
151
|
|
|
184
152
|
it('ignores a session whose transcript file is missing', async () => {
|
|
185
|
-
const provider = new TranscriptOwnerCallStatusProvider(
|
|
186
|
-
rootDirectory,
|
|
187
|
-
'<<OWNER_CALL>>',
|
|
188
|
-
);
|
|
153
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
189
154
|
|
|
190
|
-
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
191
|
-
'
|
|
192
|
-
|
|
155
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
156
|
+
new Map([[sessionName, path.join(rootDirectory, 'missing.jsonl')]]),
|
|
157
|
+
);
|
|
193
158
|
|
|
194
159
|
expect(result.size).toBe(0);
|
|
195
160
|
});
|
|
196
161
|
|
|
197
162
|
it('ignores malformed transcript lines', async () => {
|
|
198
|
-
const
|
|
199
|
-
const fileName = `${sessionName.replace(/\//g, '_')}.jsonl`;
|
|
200
|
-
const filePath = path.join(rootDirectory, fileName);
|
|
163
|
+
const filePath = path.join(rootDirectory, 'workbench.jsonl');
|
|
201
164
|
fs.writeFileSync(
|
|
202
165
|
filePath,
|
|
203
166
|
[
|
|
@@ -206,14 +169,31 @@ describe('TranscriptOwnerCallStatusProvider', () => {
|
|
|
206
169
|
].join('\n'),
|
|
207
170
|
'utf8',
|
|
208
171
|
);
|
|
209
|
-
const provider = new TranscriptOwnerCallStatusProvider(
|
|
210
|
-
|
|
211
|
-
|
|
172
|
+
const provider = new TranscriptOwnerCallStatusProvider('<<OWNER_CALL>>');
|
|
173
|
+
|
|
174
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
175
|
+
new Map([[sessionName, filePath]]),
|
|
212
176
|
);
|
|
213
177
|
|
|
214
|
-
|
|
215
|
-
|
|
178
|
+
expect(result.has(sessionName)).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('uses the canonical call-to-user marker string', async () => {
|
|
182
|
+
const transcriptPath = writeTranscript('workbench.jsonl', [
|
|
183
|
+
{
|
|
184
|
+
type: 'assistant',
|
|
185
|
+
timestamp: '2026-06-27T10:05:00.000Z',
|
|
186
|
+
message: {
|
|
187
|
+
role: 'assistant',
|
|
188
|
+
content: [{ type: 'text', text: 'Please decide <call-to-user>' }],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
216
191
|
]);
|
|
192
|
+
const provider = new TranscriptOwnerCallStatusProvider('<call-to-user>');
|
|
193
|
+
|
|
194
|
+
const result = await provider.listSessionNamesWithUnansweredOwnerCall(
|
|
195
|
+
new Map([[sessionName, transcriptPath]]),
|
|
196
|
+
);
|
|
217
197
|
|
|
218
198
|
expect(result.has(sessionName)).toBe(true);
|
|
219
199
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
2
|
import { OwnerCallStatusProvider } from '../../domain/usecases/adapter-interfaces/OwnerCallStatusProvider';
|
|
4
3
|
|
|
5
4
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -51,24 +50,18 @@ const hasOwnerTextReply = (content: unknown): boolean => {
|
|
|
51
50
|
};
|
|
52
51
|
|
|
53
52
|
export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvider {
|
|
54
|
-
constructor(
|
|
55
|
-
private readonly rootDirectory: string | null,
|
|
56
|
-
private readonly ownerCallMarker: string | null,
|
|
57
|
-
) {}
|
|
53
|
+
constructor(private readonly ownerCallMarker: string | null) {}
|
|
58
54
|
|
|
59
55
|
listSessionNamesWithUnansweredOwnerCall = async (
|
|
60
|
-
|
|
56
|
+
transcriptPathBySessionName: Map<string, string>,
|
|
61
57
|
): Promise<Set<string>> => {
|
|
62
58
|
const waiting = new Set<string>();
|
|
63
|
-
if (
|
|
64
|
-
this.rootDirectory === null ||
|
|
65
|
-
this.ownerCallMarker === null ||
|
|
66
|
-
this.ownerCallMarker.length === 0
|
|
67
|
-
) {
|
|
59
|
+
if (this.ownerCallMarker === null || this.ownerCallMarker.length === 0) {
|
|
68
60
|
return waiting;
|
|
69
61
|
}
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
const marker = this.ownerCallMarker;
|
|
63
|
+
for (const [sessionName, transcriptPath] of transcriptPathBySessionName) {
|
|
64
|
+
if (this.isWaitingForOwnerReply(transcriptPath, marker)) {
|
|
72
65
|
waiting.add(sessionName);
|
|
73
66
|
}
|
|
74
67
|
}
|
|
@@ -76,19 +69,12 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
|
|
|
76
69
|
};
|
|
77
70
|
|
|
78
71
|
private isWaitingForOwnerReply = (
|
|
79
|
-
|
|
72
|
+
transcriptPath: string,
|
|
80
73
|
marker: string,
|
|
81
74
|
): boolean => {
|
|
82
|
-
if (this.rootDirectory === null) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
const filePath = path.join(
|
|
86
|
-
this.rootDirectory,
|
|
87
|
-
this.toTranscriptFileName(sessionName),
|
|
88
|
-
);
|
|
89
75
|
let content: string;
|
|
90
76
|
try {
|
|
91
|
-
content = fs.readFileSync(
|
|
77
|
+
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
92
78
|
} catch {
|
|
93
79
|
return false;
|
|
94
80
|
}
|
|
@@ -133,7 +119,4 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
|
|
|
133
119
|
lastOwnerCallEpochMs > lastOwnerReplyEpochMs
|
|
134
120
|
);
|
|
135
121
|
};
|
|
136
|
-
|
|
137
|
-
private toTranscriptFileName = (sessionName: string): string =>
|
|
138
|
-
`${sessionName.replace(/\//g, '_')}.jsonl`;
|
|
139
122
|
}
|
|
@@ -67,6 +67,15 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
67
67
|
},
|
|
68
68
|
];
|
|
69
69
|
|
|
70
|
+
const stopSequenceFinishedEntries = (startTimestamp: string): object[] => [
|
|
71
|
+
{ type: 'user', timestamp: startTimestamp, message: { role: 'user' } },
|
|
72
|
+
{
|
|
73
|
+
type: 'assistant',
|
|
74
|
+
timestamp: startTimestamp,
|
|
75
|
+
message: { role: 'assistant', stop_reason: 'stop_sequence' },
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
70
79
|
it('reports a running sub-agent with silent seconds from the file mtime and running seconds from the first entry', async () => {
|
|
71
80
|
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
72
81
|
const startTimestamp = '2026-06-27T11:45:00.000Z';
|
|
@@ -110,6 +119,26 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
110
119
|
expect(result.size).toBe(0);
|
|
111
120
|
});
|
|
112
121
|
|
|
122
|
+
it('excludes a finished sub-agent whose last entry stop_reason is stop_sequence', async () => {
|
|
123
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
124
|
+
writeAgentTranscript(
|
|
125
|
+
sessionName,
|
|
126
|
+
'done2',
|
|
127
|
+
stopSequenceFinishedEntries('2026-06-27T11:00:00.000Z'),
|
|
128
|
+
nowEpochSeconds - 30,
|
|
129
|
+
);
|
|
130
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
131
|
+
createResolver(),
|
|
132
|
+
now,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const result = await repository.listSubAgentActivitiesBySessionName([
|
|
136
|
+
sessionName,
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
expect(result.size).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
113
142
|
it('follows a symlinked transcript when computing silent seconds', async () => {
|
|
114
143
|
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
115
144
|
const targetDir = fs.mkdtempSync(
|
|
@@ -126,7 +126,10 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
126
126
|
return null;
|
|
127
127
|
}
|
|
128
128
|
const transcript = parseTranscript(content);
|
|
129
|
-
if (
|
|
129
|
+
if (
|
|
130
|
+
transcript.lastStopReason === 'end_turn' ||
|
|
131
|
+
transcript.lastStopReason === 'stop_sequence'
|
|
132
|
+
) {
|
|
130
133
|
return null;
|
|
131
134
|
}
|
|
132
135
|
const silentSeconds = clampToZero(
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type LiveSessionPaneProcess = {
|
|
2
|
+
sessionName: string;
|
|
3
|
+
panePids: number[];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type LiveSessionProcessInfo = {
|
|
7
|
+
pid: number;
|
|
8
|
+
ppid: number;
|
|
9
|
+
commandLine: string;
|
|
10
|
+
sessionId: string | null;
|
|
11
|
+
configDir: string | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type LiveSessionProcessSnapshot = {
|
|
15
|
+
sessions: LiveSessionPaneProcess[];
|
|
16
|
+
processes: LiveSessionProcessInfo[];
|
|
17
|
+
};
|