github-issue-tower-defence-management 1.110.0 → 1.111.0
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 +8 -7
- 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/proxy/RateLimitCache.js +8 -0
- package/bin/adapter/proxy/RateLimitCache.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/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/proxy/RateLimitCache.test.ts +63 -0
- package/src/adapter/proxy/RateLimitCache.ts +11 -0
- 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/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/proxy/RateLimitCache.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/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 @@
|
|
|
1
|
+
{"version":3,"file":"ProcessEnvironReader.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/ProcessEnvironReader.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -108,8 +108,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
108
108
|
inTmuxConsoleToken?: string;
|
|
109
109
|
inTmuxProjectOrder?: string[];
|
|
110
110
|
inTmuxLauncherCommand?: string;
|
|
111
|
-
|
|
112
|
-
sessionTranscriptRootDirectory?: string;
|
|
111
|
+
silentNotificationEnabled?: boolean;
|
|
113
112
|
ownerCallMarker?: string;
|
|
114
113
|
subAgentOutputRootDirectory?: string;
|
|
115
114
|
subAgentProcessMatchPattern?: string;
|
|
@@ -563,10 +562,9 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
563
562
|
}
|
|
564
563
|
|
|
565
564
|
try {
|
|
566
|
-
const
|
|
567
|
-
mergedInput.
|
|
568
|
-
process.env.
|
|
569
|
-
null;
|
|
565
|
+
const silentNotificationEnabled =
|
|
566
|
+
mergedInput.silentNotificationEnabled ??
|
|
567
|
+
process.env.TDPM_SILENT_NOTIFICATION_ENABLED === 'true';
|
|
570
568
|
const subAgentOutputRootDirectory =
|
|
571
569
|
mergedInput.subAgentOutputRootDirectory ??
|
|
572
570
|
process.env.TDPM_SUBAGENT_OUTPUT_ROOT_DIRECTORY ??
|
|
@@ -575,10 +573,6 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
575
573
|
mergedInput.subAgentProcessMatchPattern ??
|
|
576
574
|
process.env.TDPM_SUBAGENT_PROCESS_MATCH_PATTERN ??
|
|
577
575
|
null;
|
|
578
|
-
const sessionTranscriptRootDirectory =
|
|
579
|
-
mergedInput.sessionTranscriptRootDirectory ??
|
|
580
|
-
process.env.TDPM_SESSION_TRANSCRIPT_ROOT_DIRECTORY ??
|
|
581
|
-
null;
|
|
582
576
|
const ownerCallMarker =
|
|
583
577
|
mergedInput.ownerCallMarker ??
|
|
584
578
|
process.env.TDPM_SILENT_OWNER_CALL_MARKER ??
|
|
@@ -588,13 +582,9 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
588
582
|
process.env.TDPM_SUBAGENT_TRANSCRIPT_ROOT_DIRECTORY ??
|
|
589
583
|
null;
|
|
590
584
|
await notifySilentTmuxSessions({
|
|
591
|
-
|
|
592
|
-
allowCacheMinutes: mergedInput.allowIssueCacheMinutes,
|
|
593
|
-
issueRepository,
|
|
585
|
+
enabled: silentNotificationEnabled,
|
|
594
586
|
localCommandRunner: nodeLocalCommandRunner,
|
|
595
587
|
cacheRepository: localStorageCacheRepository,
|
|
596
|
-
sessionOutputRootDirectory,
|
|
597
|
-
sessionTranscriptRootDirectory,
|
|
598
588
|
ownerCallMarker,
|
|
599
589
|
subAgentOutputRootDirectory,
|
|
600
590
|
subAgentProcessMatchPattern,
|
|
@@ -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,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
writeRateLimit,
|
|
16
16
|
writeSubscriptionDisabled,
|
|
17
17
|
} from './RateLimitCache';
|
|
18
|
+
import { OauthTokenSelectUseCase } from '../../domain/usecases/OauthTokenSelectUseCase';
|
|
18
19
|
|
|
19
20
|
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
20
21
|
v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
@@ -863,4 +864,66 @@ describe('RateLimitCache', () => {
|
|
|
863
864
|
expect(snapshot?.subscriptionDisabled).toBe(false);
|
|
864
865
|
});
|
|
865
866
|
});
|
|
867
|
+
|
|
868
|
+
describe('writeRateLimit preserves subscriptionDisabledEpoch across header writes', () => {
|
|
869
|
+
const headerBearingResponse: Record<string, string> = {
|
|
870
|
+
'anthropic-ratelimit-unified-status': 'allowed',
|
|
871
|
+
'anthropic-ratelimit-unified-5h-status': 'allowed',
|
|
872
|
+
'anthropic-ratelimit-unified-5h-reset': '2000100',
|
|
873
|
+
'anthropic-ratelimit-unified-5h-utilization': '0.1',
|
|
874
|
+
'anthropic-ratelimit-unified-7d-status': 'allowed',
|
|
875
|
+
'anthropic-ratelimit-unified-7d-reset': '2000200',
|
|
876
|
+
'anthropic-ratelimit-unified-7d-utilization': '0.1',
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
it('keeps subscriptionDisabled: true after a later header-bearing 200 response', () => {
|
|
880
|
+
const token = 'sub-disabled-then-200-token';
|
|
881
|
+
writeSubscriptionDisabled(token);
|
|
882
|
+
writeRateLimit(token, headerBearingResponse, 200);
|
|
883
|
+
const snapshot = readRateLimit(token);
|
|
884
|
+
expect(snapshot?.subscriptionDisabled).toBe(true);
|
|
885
|
+
expect(snapshot?.fiveHourReset).toBe(2000100);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it('keeps subscriptionDisabled: true after a later header-bearing 429 response', () => {
|
|
889
|
+
const token = 'sub-disabled-then-429-token';
|
|
890
|
+
writeSubscriptionDisabled(token);
|
|
891
|
+
writeRateLimit(token, headerBearingResponse, 429);
|
|
892
|
+
const snapshot = readRateLimit(token);
|
|
893
|
+
expect(snapshot?.subscriptionDisabled).toBe(true);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it('still excludes the token from selection after a header-bearing response', () => {
|
|
897
|
+
const token = 'sub-disabled-then-200-selection-token';
|
|
898
|
+
writeSubscriptionDisabled(token);
|
|
899
|
+
writeRateLimit(token, headerBearingResponse, 200);
|
|
900
|
+
const snapshot = readRateLimit(token);
|
|
901
|
+
const useCase = new OauthTokenSelectUseCase();
|
|
902
|
+
const result = useCase.run(
|
|
903
|
+
[
|
|
904
|
+
{
|
|
905
|
+
name: 'disabled-token',
|
|
906
|
+
token,
|
|
907
|
+
snapshot:
|
|
908
|
+
snapshot === null
|
|
909
|
+
? null
|
|
910
|
+
: {
|
|
911
|
+
fiveHourUtilization: snapshot.fiveHourUtilization,
|
|
912
|
+
fiveHourReset: snapshot.fiveHourReset,
|
|
913
|
+
sevenDayUtilization: snapshot.sevenDayUtilization,
|
|
914
|
+
sevenDayReset: snapshot.sevenDayReset,
|
|
915
|
+
},
|
|
916
|
+
subscriptionDisabled: snapshot?.subscriptionDisabled ?? false,
|
|
917
|
+
unifiedRejected: snapshot?.unifiedRejected ?? false,
|
|
918
|
+
},
|
|
919
|
+
],
|
|
920
|
+
Date.now() / 1000,
|
|
921
|
+
);
|
|
922
|
+
expect(result.selected).toBeNull();
|
|
923
|
+
expect(result.metrics[0]?.eligible).toBe(false);
|
|
924
|
+
expect(result.metrics[0]?.exclusionReason).toBe(
|
|
925
|
+
'organization has disabled Claude subscription access for Claude Code',
|
|
926
|
+
);
|
|
927
|
+
});
|
|
928
|
+
});
|
|
866
929
|
});
|