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
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import { Issue } from '../../../domain/entities/Issue';
|
|
5
|
-
import { Project } from '../../../domain/entities/Project';
|
|
6
4
|
import { LocalCommandRunner } from '../../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
7
|
-
import {
|
|
8
|
-
import { IN_TMUX_STATUS_NAME } from '../../../domain/entities/WorkflowStatus';
|
|
5
|
+
import { ProcessEnvironReader } from '../../../domain/usecases/adapter-interfaces/ProcessEnvironReader';
|
|
9
6
|
import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCacheRepository';
|
|
10
7
|
import { LocalStorageRepository } from '../../repositories/LocalStorageRepository';
|
|
11
8
|
import { SilentSessionMessageTemplates } from '../../repositories/ConfigurableSilentSessionMessageComposer';
|
|
@@ -16,8 +13,10 @@ import {
|
|
|
16
13
|
|
|
17
14
|
const NOW = new Date('2026-06-26T00:00:00.000Z');
|
|
18
15
|
const NOW_EPOCH_SECONDS = Math.floor(NOW.getTime() / 1000);
|
|
19
|
-
const
|
|
20
|
-
const
|
|
16
|
+
const SESSION_NAME = 'workbench';
|
|
17
|
+
const SESSION_ID = 'wb-uuid';
|
|
18
|
+
const PANE_PID = 200;
|
|
19
|
+
const CLAUDE_PID = 201;
|
|
21
20
|
|
|
22
21
|
const EMPTY_TEMPLATES: SilentSessionMessageTemplates = {
|
|
23
22
|
mainStalledMessage: null,
|
|
@@ -25,52 +24,6 @@ const EMPTY_TEMPLATES: SilentSessionMessageTemplates = {
|
|
|
25
24
|
subAgentMessageFooter: null,
|
|
26
25
|
};
|
|
27
26
|
|
|
28
|
-
const makeProject = (): Project => ({
|
|
29
|
-
id: 'project-1',
|
|
30
|
-
url: 'https://github.com/users/user/projects/1',
|
|
31
|
-
databaseId: 1,
|
|
32
|
-
name: 'Test Project',
|
|
33
|
-
status: {
|
|
34
|
-
name: 'Status',
|
|
35
|
-
fieldId: 'field-1',
|
|
36
|
-
statuses: [],
|
|
37
|
-
},
|
|
38
|
-
nextActionDate: null,
|
|
39
|
-
nextActionHour: null,
|
|
40
|
-
story: null,
|
|
41
|
-
remainingEstimationMinutes: null,
|
|
42
|
-
dependedIssueUrlSeparatedByComma: null,
|
|
43
|
-
completionDate50PercentConfidence: null,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const makeIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
47
|
-
nameWithOwner: 'demo/repo',
|
|
48
|
-
number: 1,
|
|
49
|
-
title: 'Issue 1',
|
|
50
|
-
state: 'OPEN',
|
|
51
|
-
status: IN_TMUX_STATUS_NAME,
|
|
52
|
-
story: null,
|
|
53
|
-
nextActionDate: null,
|
|
54
|
-
nextActionHour: null,
|
|
55
|
-
estimationMinutes: null,
|
|
56
|
-
dependedIssueUrls: [],
|
|
57
|
-
completionDate50PercentConfidence: null,
|
|
58
|
-
url: 'https://github.com/demo/repo/issues/1',
|
|
59
|
-
assignees: [],
|
|
60
|
-
labels: [],
|
|
61
|
-
org: 'demo',
|
|
62
|
-
repo: 'repo',
|
|
63
|
-
body: '',
|
|
64
|
-
itemId: 'item-1',
|
|
65
|
-
isPr: false,
|
|
66
|
-
isInProgress: false,
|
|
67
|
-
isClosed: false,
|
|
68
|
-
createdAt: new Date(),
|
|
69
|
-
author: '',
|
|
70
|
-
closingIssueReferenceUrls: [],
|
|
71
|
-
...overrides,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
27
|
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
75
28
|
|
|
76
29
|
const createMockRunner = (): Mocked<LocalCommandRunner> => ({
|
|
@@ -81,34 +34,46 @@ const createMockRunner = (): Mocked<LocalCommandRunner> => ({
|
|
|
81
34
|
}),
|
|
82
35
|
});
|
|
83
36
|
|
|
84
|
-
const createMockIssueRepository = (
|
|
85
|
-
issues: Issue[],
|
|
86
|
-
): Pick<IssueRepository, 'getAllOpened'> => ({
|
|
87
|
-
getAllOpened: jest.fn().mockResolvedValue(issues),
|
|
88
|
-
});
|
|
89
|
-
|
|
90
37
|
describe('notifySilentTmuxSessions', () => {
|
|
91
|
-
let
|
|
38
|
+
let configDir: string;
|
|
92
39
|
let cacheDirectory: string;
|
|
93
40
|
|
|
94
41
|
beforeEach(() => {
|
|
95
42
|
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
96
|
-
|
|
43
|
+
configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'silent-config-'));
|
|
97
44
|
cacheDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'silent-cache-'));
|
|
98
45
|
});
|
|
99
46
|
|
|
100
47
|
afterEach(() => {
|
|
101
48
|
jest.restoreAllMocks();
|
|
102
|
-
fs.rmSync(
|
|
49
|
+
fs.rmSync(configDir, { force: true, recursive: true });
|
|
103
50
|
fs.rmSync(cacheDirectory, { force: true, recursive: true });
|
|
104
51
|
});
|
|
105
52
|
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
fs.writeFileSync(
|
|
110
|
-
|
|
111
|
-
|
|
53
|
+
const writeTranscript = (lines: object[]): void => {
|
|
54
|
+
const projectDirectory = path.join(configDir, 'projects', '-home-user');
|
|
55
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
path.join(projectDirectory, `${SESSION_ID}.jsonl`),
|
|
58
|
+
lines.map((line) => JSON.stringify(line)).join('\n'),
|
|
59
|
+
'utf8',
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const silentAssistantTranscript = (): void => {
|
|
64
|
+
const silentTimestamp = new Date(
|
|
65
|
+
(NOW_EPOCH_SECONDS - 11 * 60) * 1000,
|
|
66
|
+
).toISOString();
|
|
67
|
+
writeTranscript([
|
|
68
|
+
{
|
|
69
|
+
type: 'assistant',
|
|
70
|
+
timestamp: silentTimestamp,
|
|
71
|
+
message: {
|
|
72
|
+
role: 'assistant',
|
|
73
|
+
content: [{ type: 'text', text: 'progress update' }],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
112
77
|
};
|
|
113
78
|
|
|
114
79
|
const makeCacheRepository = (): LocalStorageCacheRepository =>
|
|
@@ -117,16 +82,44 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
117
82
|
cacheDirectory,
|
|
118
83
|
);
|
|
119
84
|
|
|
85
|
+
const makeEnvironReader = (): ProcessEnvironReader => ({
|
|
86
|
+
readEnviron: (pid: number) =>
|
|
87
|
+
pid === CLAUDE_PID
|
|
88
|
+
? {
|
|
89
|
+
CLAUDE_CODE_SESSION_ID: SESSION_ID,
|
|
90
|
+
CLAUDE_CONFIG_DIR: configDir,
|
|
91
|
+
}
|
|
92
|
+
: null,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const liveSessionRunner = (): Mocked<LocalCommandRunner> => {
|
|
96
|
+
const runner = createMockRunner();
|
|
97
|
+
runner.runCommand.mockImplementation(async (program, args) => {
|
|
98
|
+
if (program === 'tmux' && args[0] === 'list-sessions') {
|
|
99
|
+
return { stdout: `${SESSION_NAME}\n`, stderr: '', exitCode: 0 };
|
|
100
|
+
}
|
|
101
|
+
if (program === 'tmux' && args[0] === 'list-panes') {
|
|
102
|
+
return { stdout: `${PANE_PID}\n`, stderr: '', exitCode: 0 };
|
|
103
|
+
}
|
|
104
|
+
if (program === 'ps') {
|
|
105
|
+
return {
|
|
106
|
+
stdout: ` ${PANE_PID} 1 shell\n ${CLAUDE_PID} ${PANE_PID} claude --name ${SESSION_NAME}\n`,
|
|
107
|
+
stderr: '',
|
|
108
|
+
exitCode: 0,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
112
|
+
});
|
|
113
|
+
return runner;
|
|
114
|
+
};
|
|
115
|
+
|
|
120
116
|
const baseParams = (
|
|
121
117
|
runner: LocalCommandRunner,
|
|
122
118
|
): Parameters<typeof notifySilentTmuxSessions>[0] => ({
|
|
123
|
-
|
|
124
|
-
allowCacheMinutes: ALLOW_CACHE_MINUTES,
|
|
125
|
-
issueRepository: createMockIssueRepository([makeIssue()]),
|
|
119
|
+
enabled: true,
|
|
126
120
|
localCommandRunner: runner,
|
|
121
|
+
processEnvironReader: makeEnvironReader(),
|
|
127
122
|
cacheRepository: makeCacheRepository(),
|
|
128
|
-
sessionOutputRootDirectory: outputDirectory,
|
|
129
|
-
sessionTranscriptRootDirectory: null,
|
|
130
123
|
ownerCallMarker: null,
|
|
131
124
|
subAgentOutputRootDirectory: null,
|
|
132
125
|
subAgentProcessMatchPattern: null,
|
|
@@ -136,20 +129,9 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
136
129
|
...DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS,
|
|
137
130
|
});
|
|
138
131
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
runner
|
|
142
|
-
if (program === 'tmux' && args[0] === 'list-sessions') {
|
|
143
|
-
return { stdout: `${SESSION_NAME}\n`, stderr: '', exitCode: 0 };
|
|
144
|
-
}
|
|
145
|
-
return { stdout: '', stderr: '', exitCode: 0 };
|
|
146
|
-
});
|
|
147
|
-
return runner;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
it('sends a main stalled notification to a silent monitored live session', async () => {
|
|
151
|
-
writeSilentOutputFile(SESSION_NAME);
|
|
152
|
-
const runner = listSessionsRunner();
|
|
132
|
+
it('sends a main stalled notification to a silent plain-named live session', async () => {
|
|
133
|
+
silentAssistantTranscript();
|
|
134
|
+
const runner = liveSessionRunner();
|
|
153
135
|
|
|
154
136
|
await notifySilentTmuxSessions(baseParams(runner));
|
|
155
137
|
|
|
@@ -160,52 +142,21 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
160
142
|
expect(sendCall?.[1][4]).toContain('You have produced no output for');
|
|
161
143
|
});
|
|
162
144
|
|
|
163
|
-
it('does nothing when
|
|
164
|
-
|
|
165
|
-
const runner =
|
|
145
|
+
it('does nothing when the step is not enabled', async () => {
|
|
146
|
+
silentAssistantTranscript();
|
|
147
|
+
const runner = liveSessionRunner();
|
|
166
148
|
|
|
167
149
|
await notifySilentTmuxSessions({
|
|
168
150
|
...baseParams(runner),
|
|
169
|
-
|
|
170
|
-
subAgentProcessMatchPattern: null,
|
|
151
|
+
enabled: false,
|
|
171
152
|
});
|
|
172
153
|
|
|
173
154
|
expect(runner.runCommand.mock.calls).toHaveLength(0);
|
|
174
155
|
});
|
|
175
156
|
|
|
176
|
-
it('sends a sub-agent notification driven by the process match pattern even when the main session is not silent', async () => {
|
|
177
|
-
const runner = createMockRunner();
|
|
178
|
-
runner.runCommand.mockImplementation(async (program, args) => {
|
|
179
|
-
if (program === 'tmux' && args[0] === 'list-sessions') {
|
|
180
|
-
return { stdout: `${SESSION_NAME}\n`, stderr: '', exitCode: 0 };
|
|
181
|
-
}
|
|
182
|
-
if (program === 'ps') {
|
|
183
|
-
return {
|
|
184
|
-
stdout: ` 1200 worker session=${SESSION_NAME} label=task-a\n`,
|
|
185
|
-
stderr: '',
|
|
186
|
-
exitCode: 0,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
return { stdout: '', stderr: '', exitCode: 0 };
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
await notifySilentTmuxSessions({
|
|
193
|
-
...baseParams(runner),
|
|
194
|
-
sessionOutputRootDirectory: null,
|
|
195
|
-
subAgentProcessMatchPattern:
|
|
196
|
-
'session=(?<session>[^ ]+) label=(?<label>[^ ]+)',
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const sendCall = runner.runCommand.mock.calls.find(
|
|
200
|
-
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
201
|
-
);
|
|
202
|
-
expect(sendCall?.[1][2]).toBe(SESSION_NAME);
|
|
203
|
-
expect(sendCall?.[1][4]).toContain('task-a');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
157
|
it('uses the configured main stalled message template when provided', async () => {
|
|
207
|
-
|
|
208
|
-
const runner =
|
|
158
|
+
silentAssistantTranscript();
|
|
159
|
+
const runner = liveSessionRunner();
|
|
209
160
|
|
|
210
161
|
await notifySilentTmuxSessions({
|
|
211
162
|
...baseParams(runner),
|
|
@@ -223,41 +174,28 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
223
174
|
});
|
|
224
175
|
|
|
225
176
|
it('suppresses the main stalled notification when the transcript shows an unanswered owner call', async () => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
message: {
|
|
246
|
-
role: 'assistant',
|
|
247
|
-
stop_reason: 'end_turn',
|
|
248
|
-
content: [
|
|
249
|
-
{ type: 'text', text: 'waiting <<OWNER_CALL>> please decide' },
|
|
250
|
-
],
|
|
251
|
-
},
|
|
252
|
-
}),
|
|
253
|
-
].join('\n'),
|
|
254
|
-
'utf8',
|
|
255
|
-
);
|
|
256
|
-
const runner = listSessionsRunner();
|
|
177
|
+
writeTranscript([
|
|
178
|
+
{
|
|
179
|
+
type: 'user',
|
|
180
|
+
timestamp: '2026-06-25T23:00:00.000Z',
|
|
181
|
+
message: { role: 'user', content: 'go ahead' },
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: 'assistant',
|
|
185
|
+
timestamp: '2026-06-25T23:50:00.000Z',
|
|
186
|
+
message: {
|
|
187
|
+
role: 'assistant',
|
|
188
|
+
stop_reason: 'end_turn',
|
|
189
|
+
content: [
|
|
190
|
+
{ type: 'text', text: 'waiting <<OWNER_CALL>> please decide' },
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
]);
|
|
195
|
+
const runner = liveSessionRunner();
|
|
257
196
|
|
|
258
197
|
await notifySilentTmuxSessions({
|
|
259
198
|
...baseParams(runner),
|
|
260
|
-
sessionTranscriptRootDirectory: transcriptDirectory,
|
|
261
199
|
ownerCallMarker: '<<OWNER_CALL>>',
|
|
262
200
|
});
|
|
263
201
|
|
|
@@ -265,65 +203,19 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
265
203
|
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
266
204
|
);
|
|
267
205
|
expect(sendCall).toBeUndefined();
|
|
268
|
-
fs.rmSync(transcriptDirectory, { force: true, recursive: true });
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('sends a sub-agent notification driven by a running transcript even when the main session is not silent', async () => {
|
|
272
|
-
const transcriptRoot = fs.mkdtempSync(
|
|
273
|
-
path.join(os.tmpdir(), 'silent-subagent-tx-'),
|
|
274
|
-
);
|
|
275
|
-
const subAgentsDir = path.join(
|
|
276
|
-
transcriptRoot,
|
|
277
|
-
SESSION_NAME.replace(/\//g, '_'),
|
|
278
|
-
'subagents',
|
|
279
|
-
);
|
|
280
|
-
fs.mkdirSync(subAgentsDir, { recursive: true });
|
|
281
|
-
const agentPath = path.join(subAgentsDir, 'agent-running1.jsonl');
|
|
282
|
-
fs.writeFileSync(
|
|
283
|
-
agentPath,
|
|
284
|
-
[
|
|
285
|
-
JSON.stringify({
|
|
286
|
-
type: 'user',
|
|
287
|
-
timestamp: '2026-06-25T23:30:00.000Z',
|
|
288
|
-
message: { role: 'user' },
|
|
289
|
-
}),
|
|
290
|
-
JSON.stringify({
|
|
291
|
-
type: 'assistant',
|
|
292
|
-
timestamp: '2026-06-25T23:40:00.000Z',
|
|
293
|
-
message: { role: 'assistant', stop_reason: 'tool_use' },
|
|
294
|
-
}),
|
|
295
|
-
].join('\n'),
|
|
296
|
-
'utf8',
|
|
297
|
-
);
|
|
298
|
-
const silentEpoch = NOW_EPOCH_SECONDS - 6 * 60;
|
|
299
|
-
fs.utimesSync(agentPath, silentEpoch, silentEpoch);
|
|
300
|
-
const runner = listSessionsRunner();
|
|
301
|
-
|
|
302
|
-
await notifySilentTmuxSessions({
|
|
303
|
-
...baseParams(runner),
|
|
304
|
-
sessionOutputRootDirectory: null,
|
|
305
|
-
subAgentTranscriptRootDirectory: transcriptRoot,
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const sendCall = runner.runCommand.mock.calls.find(
|
|
309
|
-
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
310
|
-
);
|
|
311
|
-
expect(sendCall?.[1][2]).toBe(SESSION_NAME);
|
|
312
|
-
expect(sendCall?.[1][4]).toContain('agent-running1');
|
|
313
|
-
fs.rmSync(transcriptRoot, { force: true, recursive: true });
|
|
314
206
|
});
|
|
315
207
|
|
|
316
208
|
it('does not re-notify the same silent session on the next cycle within cooldown', async () => {
|
|
317
|
-
|
|
209
|
+
silentAssistantTranscript();
|
|
318
210
|
const cacheRepository = makeCacheRepository();
|
|
319
|
-
const firstRunner =
|
|
211
|
+
const firstRunner = liveSessionRunner();
|
|
320
212
|
|
|
321
213
|
await notifySilentTmuxSessions({
|
|
322
214
|
...baseParams(firstRunner),
|
|
323
215
|
cacheRepository,
|
|
324
216
|
});
|
|
325
217
|
|
|
326
|
-
const secondRunner =
|
|
218
|
+
const secondRunner = liveSessionRunner();
|
|
327
219
|
await notifySilentTmuxSessions({
|
|
328
220
|
...baseParams(secondRunner),
|
|
329
221
|
cacheRepository,
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { Project } from '../../../domain/entities/Project';
|
|
2
1
|
import { LocalCommandRunner } from '../../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
3
|
-
import { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
4
2
|
import { SessionSubAgentActivityRepository } from '../../../domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository';
|
|
5
3
|
import { OwnerCallStatusProvider } from '../../../domain/usecases/adapter-interfaces/OwnerCallStatusProvider';
|
|
4
|
+
import { ProcessEnvironReader } from '../../../domain/usecases/adapter-interfaces/ProcessEnvironReader';
|
|
6
5
|
import {
|
|
7
6
|
NotifySilentLiveSessionsUseCase,
|
|
8
|
-
DEFAULT_MONITORED_STATUS,
|
|
9
7
|
DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
10
8
|
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
11
9
|
DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
@@ -13,7 +11,9 @@ import {
|
|
|
13
11
|
DEFAULT_NOTIFICATION_STAGGER_SECONDS,
|
|
14
12
|
} from '../../../domain/usecases/NotifySilentLiveSessionsUseCase';
|
|
15
13
|
import { DefaultSilentSessionMessageComposer } from '../../../domain/usecases/DefaultSilentSessionMessageComposer';
|
|
16
|
-
import {
|
|
14
|
+
import { LocalProcessLiveSessionProcessSnapshotProvider } from '../../repositories/LocalProcessLiveSessionProcessSnapshotProvider';
|
|
15
|
+
import { ProcFsProcessEnvironReader } from '../../repositories/ProcFsProcessEnvironReader';
|
|
16
|
+
import { FileSystemInteractiveLiveSessionTranscriptResolver } from '../../repositories/FileSystemInteractiveLiveSessionTranscriptResolver';
|
|
17
17
|
import { FileSystemSessionOutputActivityRepository } from '../../repositories/FileSystemSessionOutputActivityRepository';
|
|
18
18
|
import { TmuxSilentSessionNotificationRepository } from '../../repositories/TmuxSilentSessionNotificationRepository';
|
|
19
19
|
import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCacheRepository';
|
|
@@ -31,13 +31,10 @@ import {
|
|
|
31
31
|
import { RealSleeper } from '../../repositories/RealSleeper';
|
|
32
32
|
|
|
33
33
|
export type NotifySilentTmuxSessionsParams = {
|
|
34
|
-
|
|
35
|
-
allowCacheMinutes: number;
|
|
36
|
-
issueRepository: Pick<IssueRepository, 'getAllOpened'>;
|
|
34
|
+
enabled: boolean;
|
|
37
35
|
localCommandRunner: LocalCommandRunner;
|
|
36
|
+
processEnvironReader?: ProcessEnvironReader;
|
|
38
37
|
cacheRepository: Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>;
|
|
39
|
-
sessionOutputRootDirectory: string | null;
|
|
40
|
-
sessionTranscriptRootDirectory: string | null;
|
|
41
38
|
ownerCallMarker: string | null;
|
|
42
39
|
subAgentOutputRootDirectory: string | null;
|
|
43
40
|
subAgentProcessMatchPattern: string | null;
|
|
@@ -52,18 +49,10 @@ export type NotifySilentTmuxSessionsParams = {
|
|
|
52
49
|
};
|
|
53
50
|
|
|
54
51
|
const createOwnerCallStatusProvider = (
|
|
55
|
-
sessionTranscriptRootDirectory: string | null,
|
|
56
52
|
ownerCallMarker: string | null,
|
|
57
53
|
): OwnerCallStatusProvider => {
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
ownerCallMarker !== null &&
|
|
61
|
-
ownerCallMarker.length > 0
|
|
62
|
-
) {
|
|
63
|
-
return new TranscriptOwnerCallStatusProvider(
|
|
64
|
-
sessionTranscriptRootDirectory,
|
|
65
|
-
ownerCallMarker,
|
|
66
|
-
);
|
|
54
|
+
if (ownerCallMarker !== null && ownerCallMarker.length > 0) {
|
|
55
|
+
return new TranscriptOwnerCallStatusProvider(ownerCallMarker);
|
|
67
56
|
}
|
|
68
57
|
return new NoUnansweredOwnerCallStatusProvider();
|
|
69
58
|
};
|
|
@@ -97,13 +86,10 @@ export const notifySilentTmuxSessions = async (
|
|
|
97
86
|
params: NotifySilentTmuxSessionsParams,
|
|
98
87
|
): Promise<void> => {
|
|
99
88
|
const {
|
|
100
|
-
|
|
101
|
-
allowCacheMinutes,
|
|
102
|
-
issueRepository,
|
|
89
|
+
enabled,
|
|
103
90
|
localCommandRunner,
|
|
91
|
+
processEnvironReader,
|
|
104
92
|
cacheRepository,
|
|
105
|
-
sessionOutputRootDirectory,
|
|
106
|
-
sessionTranscriptRootDirectory,
|
|
107
93
|
ownerCallMarker,
|
|
108
94
|
subAgentOutputRootDirectory,
|
|
109
95
|
subAgentProcessMatchPattern,
|
|
@@ -116,13 +102,9 @@ export const notifySilentTmuxSessions = async (
|
|
|
116
102
|
messageTemplates,
|
|
117
103
|
now,
|
|
118
104
|
} = params;
|
|
119
|
-
if (
|
|
120
|
-
sessionOutputRootDirectory === null &&
|
|
121
|
-
subAgentProcessMatchPattern === null &&
|
|
122
|
-
subAgentTranscriptRootDirectory === null
|
|
123
|
-
) {
|
|
105
|
+
if (!enabled) {
|
|
124
106
|
console.log(
|
|
125
|
-
'Silent live session notification skipped:
|
|
107
|
+
'Silent live session notification skipped: not enabled (set silentNotificationEnabled or TDPM_SILENT_NOTIFICATION_ENABLED=true to enable).',
|
|
126
108
|
);
|
|
127
109
|
return;
|
|
128
110
|
}
|
|
@@ -131,9 +113,12 @@ export const notifySilentTmuxSessions = async (
|
|
|
131
113
|
new DefaultSilentSessionMessageComposer(),
|
|
132
114
|
);
|
|
133
115
|
const useCase = new NotifySilentLiveSessionsUseCase(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
116
|
+
new LocalProcessLiveSessionProcessSnapshotProvider(
|
|
117
|
+
localCommandRunner,
|
|
118
|
+
processEnvironReader ?? new ProcFsProcessEnvironReader(),
|
|
119
|
+
),
|
|
120
|
+
new FileSystemInteractiveLiveSessionTranscriptResolver(),
|
|
121
|
+
new FileSystemSessionOutputActivityRepository(),
|
|
137
122
|
createSubAgentActivityRepository(
|
|
138
123
|
subAgentTranscriptRootDirectory,
|
|
139
124
|
subAgentProcessMatchPattern,
|
|
@@ -141,10 +126,7 @@ export const notifySilentTmuxSessions = async (
|
|
|
141
126
|
localCommandRunner,
|
|
142
127
|
now,
|
|
143
128
|
),
|
|
144
|
-
createOwnerCallStatusProvider(
|
|
145
|
-
sessionTranscriptRootDirectory,
|
|
146
|
-
ownerCallMarker,
|
|
147
|
-
),
|
|
129
|
+
createOwnerCallStatusProvider(ownerCallMarker),
|
|
148
130
|
new TmuxSilentSessionNotificationRepository(
|
|
149
131
|
localCommandRunner,
|
|
150
132
|
cacheRepository,
|
|
@@ -153,9 +135,6 @@ export const notifySilentTmuxSessions = async (
|
|
|
153
135
|
new RealSleeper(),
|
|
154
136
|
);
|
|
155
137
|
await useCase.run({
|
|
156
|
-
project,
|
|
157
|
-
allowCacheMinutes,
|
|
158
|
-
monitoredStatus: DEFAULT_MONITORED_STATUS,
|
|
159
138
|
mainSilentThresholdSeconds,
|
|
160
139
|
subAgentSilentThresholdSeconds,
|
|
161
140
|
subAgentRunningThresholdSeconds,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { FileSystemInteractiveLiveSessionTranscriptResolver } from './FileSystemInteractiveLiveSessionTranscriptResolver';
|
|
5
|
+
|
|
6
|
+
describe('FileSystemInteractiveLiveSessionTranscriptResolver', () => {
|
|
7
|
+
let configRoot: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
configRoot = fs.mkdtempSync(
|
|
11
|
+
path.join(os.tmpdir(), 'interactive-transcript-'),
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
fs.rmSync(configRoot, { force: true, recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const writeTranscript = (params: {
|
|
20
|
+
configDir: string;
|
|
21
|
+
cwdSlug: string;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
mtimeEpochSeconds?: number;
|
|
24
|
+
}): string => {
|
|
25
|
+
const projectDirectory = path.join(
|
|
26
|
+
params.configDir,
|
|
27
|
+
'projects',
|
|
28
|
+
params.cwdSlug,
|
|
29
|
+
);
|
|
30
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
31
|
+
const filePath = path.join(projectDirectory, `${params.sessionId}.jsonl`);
|
|
32
|
+
fs.writeFileSync(filePath, '{"type":"custom-title"}', 'utf8');
|
|
33
|
+
if (params.mtimeEpochSeconds !== undefined) {
|
|
34
|
+
fs.utimesSync(
|
|
35
|
+
filePath,
|
|
36
|
+
params.mtimeEpochSeconds,
|
|
37
|
+
params.mtimeEpochSeconds,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return filePath;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
it('resolves a transcript by config dir and session id for a plain-named session', () => {
|
|
44
|
+
const configDir = path.join(configRoot, 'workbench');
|
|
45
|
+
const filePath = writeTranscript({
|
|
46
|
+
configDir,
|
|
47
|
+
cwdSlug: '-home-user',
|
|
48
|
+
sessionId: 'wb-uuid',
|
|
49
|
+
});
|
|
50
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver();
|
|
51
|
+
|
|
52
|
+
const result = resolver.resolveTranscriptPaths([
|
|
53
|
+
{ sessionName: 'workbench', sessionId: 'wb-uuid', configDir },
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
expect(result.get('workbench')).toBe(filePath);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('chooses the most recently modified match across project directories', () => {
|
|
60
|
+
const configDir = path.join(configRoot, 'workbench');
|
|
61
|
+
writeTranscript({
|
|
62
|
+
configDir,
|
|
63
|
+
cwdSlug: '-home-user-old',
|
|
64
|
+
sessionId: 'wb-uuid',
|
|
65
|
+
mtimeEpochSeconds: 1700000000,
|
|
66
|
+
});
|
|
67
|
+
const newerPath = writeTranscript({
|
|
68
|
+
configDir,
|
|
69
|
+
cwdSlug: '-home-user-new',
|
|
70
|
+
sessionId: 'wb-uuid',
|
|
71
|
+
mtimeEpochSeconds: 1700000500,
|
|
72
|
+
});
|
|
73
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver();
|
|
74
|
+
|
|
75
|
+
const result = resolver.resolveTranscriptPaths([
|
|
76
|
+
{ sessionName: 'workbench', sessionId: 'wb-uuid', configDir },
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
expect(result.get('workbench')).toBe(newerPath);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('resolves several sessions in one call', () => {
|
|
83
|
+
const workbenchConfig = path.join(configRoot, 'workbench');
|
|
84
|
+
const controlRoomConfig = path.join(configRoot, 'control-room');
|
|
85
|
+
const workbenchPath = writeTranscript({
|
|
86
|
+
configDir: workbenchConfig,
|
|
87
|
+
cwdSlug: '-home-user',
|
|
88
|
+
sessionId: 'wb-uuid',
|
|
89
|
+
});
|
|
90
|
+
const controlRoomPath = writeTranscript({
|
|
91
|
+
configDir: controlRoomConfig,
|
|
92
|
+
cwdSlug: '-home-user',
|
|
93
|
+
sessionId: 'cr-uuid',
|
|
94
|
+
});
|
|
95
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver();
|
|
96
|
+
|
|
97
|
+
const result = resolver.resolveTranscriptPaths([
|
|
98
|
+
{
|
|
99
|
+
sessionName: 'workbench',
|
|
100
|
+
sessionId: 'wb-uuid',
|
|
101
|
+
configDir: workbenchConfig,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
sessionName: 'control-room',
|
|
105
|
+
sessionId: 'cr-uuid',
|
|
106
|
+
configDir: controlRoomConfig,
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
expect(result.get('workbench')).toBe(workbenchPath);
|
|
111
|
+
expect(result.get('control-room')).toBe(controlRoomPath);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('omits a session whose transcript file is absent', () => {
|
|
115
|
+
const configDir = path.join(configRoot, 'workbench');
|
|
116
|
+
fs.mkdirSync(path.join(configDir, 'projects', '-home-user'), {
|
|
117
|
+
recursive: true,
|
|
118
|
+
});
|
|
119
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver();
|
|
120
|
+
|
|
121
|
+
const result = resolver.resolveTranscriptPaths([
|
|
122
|
+
{ sessionName: 'workbench', sessionId: 'missing-uuid', configDir },
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
expect(result.has('workbench')).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('omits a session whose config dir has no projects directory', () => {
|
|
129
|
+
const configDir = path.join(configRoot, 'workbench');
|
|
130
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
131
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver();
|
|
132
|
+
|
|
133
|
+
const result = resolver.resolveTranscriptPaths([
|
|
134
|
+
{ sessionName: 'workbench', sessionId: 'wb-uuid', configDir },
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
expect(result.has('workbench')).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns an empty map for an empty session list', () => {
|
|
141
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver();
|
|
142
|
+
|
|
143
|
+
const result = resolver.resolveTranscriptPaths([]);
|
|
144
|
+
|
|
145
|
+
expect(result.size).toBe(0);
|
|
146
|
+
});
|
|
147
|
+
});
|