github-issue-tower-defence-management 1.146.5 → 1.146.7
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/.github/dependabot.yml +2 -0
- package/.github/workflows/commit-lint.yml +20 -0
- package/.github/workflows/umino-project.yml +0 -1
- package/CHANGELOG.md +14 -0
- package/bin/adapter/entry-points/cli/index.js +2 -2
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js +4 -3
- package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js +4 -3
- package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +3 -2
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.js +1 -1
- package/bin/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js +19 -1
- package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js.map +1 -1
- package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +73 -2
- package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -1
- package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js +3 -1
- package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js.map +1 -1
- package/bin/domain/usecases/OauthTokenSelectUseCase.js +13 -4
- package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
- package/package.json +1 -1
- package/renovate.json +5 -0
- package/src/adapter/entry-points/cli/index.ts +2 -2
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +7 -4
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts +4 -1
- package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.test.ts +6 -2
- package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.ts +8 -2
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +14 -2
- package/src/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.ts +4 -1
- package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.test.ts +49 -1
- package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.ts +21 -1
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +157 -15
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +115 -1
- package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +77 -5
- package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.ts +10 -1
- package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +39 -8
- package/src/domain/usecases/OauthTokenSelectUseCase.ts +25 -5
- package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts +3 -1
- package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/OauthTokenSelectHandler.d.ts +3 -2
- package/types/adapter/entry-points/handlers/OauthTokenSelectHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +1 -0
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSubAgentLivenessResolver.d.ts +1 -0
- package/types/adapter/repositories/FileSystemSubAgentLivenessResolver.d.ts.map +1 -1
- package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts +14 -1
- package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -1
- package/types/domain/usecases/LiveSessionOauthTokenSelectUseCase.d.ts.map +1 -1
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +4 -1
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OauthTokenSelectUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/OauthTokenSelectUseCase.ts"],"names":[],"mappings":";;;AAmBa,QAAA,wBAAwB,GAAG,CAAC,CAAC;AAEnC,MAAM,iBAAiB,GAAG,CAAC,SAA8B,EAAU,EAAE;IAC1E,MAAM,MAAM,GAAG,SAAS,CAAC,eAAe,IAAI,gCAAwB,CAAC;IACrE,OAAO,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACjC,CAAC,CAAC;AAHW,QAAA,iBAAiB,qBAG5B;AAgBF,MAAM,eAAe,GAAG,KAAK,CAAC;AAC9B,MAAM,qBAAqB,GAAG,CAAC,GAAG,eAAe,CAAC;AAErC,QAAA,wBAAwB,GAAG,IAAI,CAAC;AAChC,QAAA,wBAAwB,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"OauthTokenSelectUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/OauthTokenSelectUseCase.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/renovate.json
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"ignorePaths": ["**/generated/*", "**/_gen/*"],
|
|
18
18
|
"osvVulnerabilityAlerts": true,
|
|
19
19
|
"packageRules": [
|
|
20
|
+
{
|
|
21
|
+
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
|
22
|
+
"groupName": "all non-major dependencies",
|
|
23
|
+
"groupSlug": "all-non-major"
|
|
24
|
+
},
|
|
20
25
|
{
|
|
21
26
|
"matchPackageNames": ["eslint"],
|
|
22
27
|
"allowedVersions": "<9.0.0"
|
|
@@ -919,7 +919,7 @@ addServeWebOptions(program.command('serveConsole'))
|
|
|
919
919
|
program
|
|
920
920
|
.command('selectOauthToken')
|
|
921
921
|
.description(
|
|
922
|
-
'Print exactly one Claude Code OAuth token chosen by a rate-limit-aware filter.
|
|
922
|
+
'Print exactly one Claude Code OAuth token chosen by a rate-limit-aware filter. Among rate-limit-eligible tokens the choice is weighted-random, with each token weighted by its per-token selectionWeight (default 1) multiplied by how urgent its 7d window is (the free share of that window times 168 divided by the hours left before it resets, with those hours floored at 1), so a token holding unused allowance that resets soon is chosen more often. When every eligible weight is identical, the choice stays deterministic and no random draw is made. The token string is written to stdout (pipeable); the per-candidate decision trace is written to stderr. Exits non-zero when no token passes the filter.',
|
|
923
923
|
)
|
|
924
924
|
.option(
|
|
925
925
|
'--tokenListJsonPath <path>',
|
|
@@ -951,7 +951,7 @@ program
|
|
|
951
951
|
program
|
|
952
952
|
.command('selectLiveSessionOauthToken')
|
|
953
953
|
.description(
|
|
954
|
-
'Print exactly one Claude Code OAuth token chosen for a new live interactive session. Among rate-limit-eligible tokens
|
|
954
|
+
'Print exactly one Claude Code OAuth token chosen for a new live interactive session. Among rate-limit-eligible tokens the choice is weighted-random, with each token weighted by its per-token selectionWeight (default 1), multiplied by how urgent its 7d window is (the free share of that window times 168 divided by the hours left before it resets, with those hours floored at 1), and divided by one plus its current live session count (by distinct CLAUDE_CODE_SESSION_ID found in running Claude Code processes). A token whose 7d window still holds unused allowance and resets soon is therefore chosen more often, while occupancy keeps sessions spread across tokens. When every eligible weight is identical, the choice stays deterministic and no random draw is made. The token string is written to stdout (pipeable); the per-candidate decision trace is written to stderr. Exits non-zero when no token passes the filter.',
|
|
955
955
|
)
|
|
956
956
|
.option(
|
|
957
957
|
'--tokenListJsonPath <path>',
|
|
@@ -109,10 +109,12 @@ describe('LiveSessionOauthTokenSelectHandler', () => {
|
|
|
109
109
|
|
|
110
110
|
const buildHandler = (
|
|
111
111
|
sessions: ClaudeLiveSession[],
|
|
112
|
+
random: () => number = () => 0.5,
|
|
112
113
|
): LiveSessionOauthTokenSelectHandler =>
|
|
113
114
|
new LiveSessionOauthTokenSelectHandler(
|
|
114
115
|
new LiveSessionOauthTokenSelectUseCase(),
|
|
115
116
|
new FakeClaudeLiveSessionRepository(sessions),
|
|
117
|
+
random,
|
|
116
118
|
);
|
|
117
119
|
|
|
118
120
|
const writeFableRejectionCache = (token: string, resetsAt: number): void => {
|
|
@@ -163,7 +165,7 @@ describe('LiveSessionOauthTokenSelectHandler', () => {
|
|
|
163
165
|
);
|
|
164
166
|
});
|
|
165
167
|
|
|
166
|
-
it('selects the eligible token with
|
|
168
|
+
it('selects the eligible token with fewer live sessions for a draw inside its weight slice', () => {
|
|
167
169
|
writeTokenList([
|
|
168
170
|
{ name: 'busy', token: 'fake-busy' },
|
|
169
171
|
{ name: 'idle', token: 'fake-idle' },
|
|
@@ -181,9 +183,10 @@ describe('LiveSessionOauthTokenSelectHandler', () => {
|
|
|
181
183
|
sevenDayReset: NOW + 6 * DAY,
|
|
182
184
|
});
|
|
183
185
|
|
|
184
|
-
const handler = buildHandler(
|
|
185
|
-
{ token: 'fake-busy', sessionKey: 'session-a' },
|
|
186
|
-
|
|
186
|
+
const handler = buildHandler(
|
|
187
|
+
[{ token: 'fake-busy', sessionKey: 'session-a' }],
|
|
188
|
+
() => 0.9,
|
|
189
|
+
);
|
|
187
190
|
const output = handler.handle({
|
|
188
191
|
tokenListJsonPath: tokenListPath,
|
|
189
192
|
cacheDirectory,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
FIVE_HOUR_MIN_FREE_RATIO,
|
|
9
9
|
OauthTokenCandidate,
|
|
10
10
|
SEVEN_DAY_MIN_FREE_RATIO,
|
|
11
|
+
SelectionRandom,
|
|
11
12
|
} from '../../../domain/usecases/OauthTokenSelectUseCase';
|
|
12
13
|
import { ProcClaudeLiveSessionRepository } from '../../repositories/ProcClaudeLiveSessionRepository';
|
|
13
14
|
import { FABLE_LIMIT_TYPE, readRateLimit } from '../../proxy/RateLimitCache';
|
|
@@ -33,6 +34,7 @@ export class LiveSessionOauthTokenSelectHandler {
|
|
|
33
34
|
constructor(
|
|
34
35
|
private readonly useCase: LiveSessionOauthTokenSelectUseCase = new LiveSessionOauthTokenSelectUseCase(),
|
|
35
36
|
private readonly liveSessionRepository: ClaudeLiveSessionRepository = new ProcClaudeLiveSessionRepository(),
|
|
37
|
+
private readonly random: SelectionRandom = Math.random,
|
|
36
38
|
) {}
|
|
37
39
|
|
|
38
40
|
handle = (
|
|
@@ -96,6 +98,7 @@ export class LiveSessionOauthTokenSelectHandler {
|
|
|
96
98
|
candidates,
|
|
97
99
|
liveSessions,
|
|
98
100
|
input.nowEpochSeconds,
|
|
101
|
+
this.random,
|
|
99
102
|
);
|
|
100
103
|
|
|
101
104
|
return {
|
|
@@ -125,7 +128,7 @@ export class LiveSessionOauthTokenSelectHandler {
|
|
|
125
128
|
);
|
|
126
129
|
} else {
|
|
127
130
|
lines.push(
|
|
128
|
-
`Selected ${result.selected.name} (
|
|
131
|
+
`Selected ${result.selected.name} (weighted by how soon the 7d window resets, how much of it is free, and how few live sessions the token carries).`,
|
|
129
132
|
);
|
|
130
133
|
}
|
|
131
134
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { OauthTokenSelectUseCase } from '../../../domain/usecases/OauthTokenSelectUseCase';
|
|
4
5
|
import { FABLE_LIMIT_TYPE, hashToken } from '../../proxy/RateLimitCache';
|
|
5
6
|
import {
|
|
6
7
|
OauthTokenSelectHandler,
|
|
@@ -122,7 +123,7 @@ describe('OauthTokenSelectHandler', () => {
|
|
|
122
123
|
);
|
|
123
124
|
};
|
|
124
125
|
|
|
125
|
-
it('selects the
|
|
126
|
+
it('selects the token whose 7d window resets soonest for a draw inside its weight slice', () => {
|
|
126
127
|
writeTokenList([
|
|
127
128
|
{ name: 'far', token: 'fake-far' },
|
|
128
129
|
{ name: 'soon', token: 'fake-soon' },
|
|
@@ -140,7 +141,10 @@ describe('OauthTokenSelectHandler', () => {
|
|
|
140
141
|
sevenDayReset: NOW + 2 * DAY,
|
|
141
142
|
});
|
|
142
143
|
|
|
143
|
-
const handler = new OauthTokenSelectHandler(
|
|
144
|
+
const handler = new OauthTokenSelectHandler(
|
|
145
|
+
new OauthTokenSelectUseCase(),
|
|
146
|
+
() => 0.5,
|
|
147
|
+
);
|
|
144
148
|
const output = handler.handle({
|
|
145
149
|
tokenListJsonPath: tokenListPath,
|
|
146
150
|
cacheDirectory,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
OauthTokenCandidate,
|
|
4
4
|
OauthTokenSelectResult,
|
|
5
5
|
OauthTokenSelectUseCase,
|
|
6
|
+
SelectionRandom,
|
|
6
7
|
} from '../../../domain/usecases/OauthTokenSelectUseCase';
|
|
7
8
|
import {
|
|
8
9
|
FABLE_LIMIT_TYPE,
|
|
@@ -55,6 +56,7 @@ export const resolveCacheDirectory = (
|
|
|
55
56
|
export class OauthTokenSelectHandler {
|
|
56
57
|
constructor(
|
|
57
58
|
private readonly useCase: OauthTokenSelectUseCase = new OauthTokenSelectUseCase(),
|
|
59
|
+
private readonly random: SelectionRandom = Math.random,
|
|
58
60
|
) {}
|
|
59
61
|
|
|
60
62
|
handle = (
|
|
@@ -112,7 +114,11 @@ export class OauthTokenSelectHandler {
|
|
|
112
114
|
},
|
|
113
115
|
);
|
|
114
116
|
|
|
115
|
-
const result = this.useCase.run(
|
|
117
|
+
const result = this.useCase.run(
|
|
118
|
+
candidates,
|
|
119
|
+
input.nowEpochSeconds,
|
|
120
|
+
this.random,
|
|
121
|
+
);
|
|
116
122
|
|
|
117
123
|
return {
|
|
118
124
|
selectedToken: result.selected?.token ?? null,
|
|
@@ -139,7 +145,7 @@ export class OauthTokenSelectHandler {
|
|
|
139
145
|
lines.push('No eligible token passed the rate-limit filter.');
|
|
140
146
|
} else {
|
|
141
147
|
lines.push(
|
|
142
|
-
`Selected ${result.selected.name} (
|
|
148
|
+
`Selected ${result.selected.name} (weighted by how soon the 7d window resets and how much of it is free among eligible tokens).`,
|
|
143
149
|
);
|
|
144
150
|
}
|
|
145
151
|
|
|
@@ -147,6 +147,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
147
147
|
messageTemplates: EMPTY_TEMPLATES,
|
|
148
148
|
now: NOW,
|
|
149
149
|
...DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS,
|
|
150
|
+
submitPushOutWaitMilliseconds: 0,
|
|
150
151
|
});
|
|
151
152
|
|
|
152
153
|
it('sends a main stalled notification to a silent github-named live session that was already a candidate in the previous cycle', async () => {
|
|
@@ -18,7 +18,11 @@ import { LocalProcessLiveSessionProcessSnapshotProvider } from '../../repositori
|
|
|
18
18
|
import { ProcFsProcessEnvironReader } from '../../repositories/ProcFsProcessEnvironReader';
|
|
19
19
|
import { FileSystemInteractiveLiveSessionTranscriptResolver } from '../../repositories/FileSystemInteractiveLiveSessionTranscriptResolver';
|
|
20
20
|
import { FileSystemSessionOutputActivityRepository } from '../../repositories/FileSystemSessionOutputActivityRepository';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_SUBMIT_PUSH_OUT_ATTEMPT_LIMIT,
|
|
23
|
+
DEFAULT_SUBMIT_PUSH_OUT_WAIT_MILLISECONDS,
|
|
24
|
+
TmuxSilentSessionNotificationRepository,
|
|
25
|
+
} from '../../repositories/TmuxSilentSessionNotificationRepository';
|
|
22
26
|
import { NoUnansweredOwnerCallStatusProvider } from '../../repositories/NoUnansweredOwnerCallStatusProvider';
|
|
23
27
|
import { TranscriptOwnerCallStatusProvider } from '../../repositories/TranscriptOwnerCallStatusProvider';
|
|
24
28
|
import { TranscriptRefusalTailStatusProvider } from '../../repositories/TranscriptRefusalTailStatusProvider';
|
|
@@ -57,6 +61,7 @@ export type NotifySilentTmuxSessionsParams = {
|
|
|
57
61
|
hubTaskStatusCacheStateFilePath: string | null;
|
|
58
62
|
hubTaskStatusCacheTtlSeconds: number;
|
|
59
63
|
messageTemplates: SilentSessionMessageTemplates;
|
|
64
|
+
submitPushOutWaitMilliseconds?: number;
|
|
60
65
|
now: Date;
|
|
61
66
|
};
|
|
62
67
|
|
|
@@ -121,6 +126,7 @@ export const notifySilentTmuxSessions = async (
|
|
|
121
126
|
hubTaskStatusCacheStateFilePath,
|
|
122
127
|
hubTaskStatusCacheTtlSeconds,
|
|
123
128
|
messageTemplates,
|
|
129
|
+
submitPushOutWaitMilliseconds,
|
|
124
130
|
now,
|
|
125
131
|
} = params;
|
|
126
132
|
if (!enabled) {
|
|
@@ -149,7 +155,13 @@ export const notifySilentTmuxSessions = async (
|
|
|
149
155
|
now,
|
|
150
156
|
),
|
|
151
157
|
createOwnerCallStatusProvider(ownerCallMarker),
|
|
152
|
-
new TmuxSilentSessionNotificationRepository(
|
|
158
|
+
new TmuxSilentSessionNotificationRepository(
|
|
159
|
+
localCommandRunner,
|
|
160
|
+
new RealSleeper(),
|
|
161
|
+
DEFAULT_SUBMIT_PUSH_OUT_ATTEMPT_LIMIT,
|
|
162
|
+
submitPushOutWaitMilliseconds ??
|
|
163
|
+
DEFAULT_SUBMIT_PUSH_OUT_WAIT_MILLISECONDS,
|
|
164
|
+
),
|
|
153
165
|
candidateDebounceStateFilePath !== null
|
|
154
166
|
? new FileSystemSilentSessionCandidateStateRepository(
|
|
155
167
|
candidateDebounceStateFilePath,
|
|
@@ -46,7 +46,10 @@ export const resetDegeneratedTmuxSessions = async (
|
|
|
46
46
|
),
|
|
47
47
|
new FileSystemInteractiveLiveSessionTranscriptResolver(),
|
|
48
48
|
new FileSystemSessionAssistantTurnsRepository(),
|
|
49
|
-
new TmuxSilentSessionNotificationRepository(
|
|
49
|
+
new TmuxSilentSessionNotificationRepository(
|
|
50
|
+
localCommandRunner,
|
|
51
|
+
new RealSleeper(),
|
|
52
|
+
),
|
|
50
53
|
new NodeTmuxSessionRepository(localCommandRunner),
|
|
51
54
|
cooldownStateFilePath !== null
|
|
52
55
|
? new FileSystemSessionDegenerationCooldownStateRepository(
|
|
@@ -69,7 +69,7 @@ describe('FileSystemSubAgentLivenessResolver', () => {
|
|
|
69
69
|
expect(result).toEqual(new Set());
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it('returns
|
|
72
|
+
it('returns an empty set when the runtime root exists and the session has no running file', async () => {
|
|
73
73
|
const resolver = new FileSystemSubAgentLivenessResolver(runtimeRoot);
|
|
74
74
|
|
|
75
75
|
const result = await resolver.resolveLiveSubAgentIds({
|
|
@@ -77,6 +77,19 @@ describe('FileSystemSubAgentLivenessResolver', () => {
|
|
|
77
77
|
mainTranscriptPath,
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
expect(result).toEqual(new Set());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns null when the configured runtime root directory does not exist', async () => {
|
|
84
|
+
const resolver = new FileSystemSubAgentLivenessResolver(
|
|
85
|
+
path.join(runtimeRoot, 'absent-runtime-root'),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const result = await resolver.resolveLiveSubAgentIds({
|
|
89
|
+
sessionName,
|
|
90
|
+
mainTranscriptPath,
|
|
91
|
+
});
|
|
92
|
+
|
|
80
93
|
expect(result).toBeNull();
|
|
81
94
|
});
|
|
82
95
|
|
|
@@ -178,4 +191,39 @@ describe('running-subagents record agent id contract with TranscriptSessionSubAg
|
|
|
178
191
|
`agent-${liveAgentId}`,
|
|
179
192
|
]);
|
|
180
193
|
});
|
|
194
|
+
|
|
195
|
+
it('reports no sub-agent when the runtime root exists and the session has no running-subagents record', async () => {
|
|
196
|
+
const subAgentsDirectory = path.join(
|
|
197
|
+
transcriptRoot,
|
|
198
|
+
cwdSlug,
|
|
199
|
+
sessionUuid,
|
|
200
|
+
'subagents',
|
|
201
|
+
);
|
|
202
|
+
fs.mkdirSync(subAgentsDirectory, { recursive: true });
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
path.join(subAgentsDirectory, `agent-${liveAgentId}.jsonl`),
|
|
205
|
+
JSON.stringify({
|
|
206
|
+
type: 'assistant',
|
|
207
|
+
timestamp: '2026-07-20T11:00:00.000Z',
|
|
208
|
+
message: {
|
|
209
|
+
stop_reason: 'tool_use',
|
|
210
|
+
content: [{ type: 'tool_use', name: 'Read', input: {} }],
|
|
211
|
+
},
|
|
212
|
+
}),
|
|
213
|
+
'utf8',
|
|
214
|
+
);
|
|
215
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
216
|
+
new FileSystemSubAgentTranscriptDirectoryResolver(transcriptRoot),
|
|
217
|
+
emptyProcessLister,
|
|
218
|
+
now,
|
|
219
|
+
new FileSystemSubAgentLivenessResolver(runtimeRoot),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
223
|
+
[sessionName],
|
|
224
|
+
new Map([[sessionName, mainTranscriptPath]]),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(result.get(sessionName)).toBeUndefined();
|
|
228
|
+
});
|
|
181
229
|
});
|
|
@@ -4,6 +4,12 @@ import { SubAgentLivenessResolver } from '../../domain/usecases/adapter-interfac
|
|
|
4
4
|
|
|
5
5
|
const RUNNING_SUBAGENTS_FILE_NAME = 'running-subagents.txt';
|
|
6
6
|
|
|
7
|
+
const isMissingFileError = (error: unknown): boolean =>
|
|
8
|
+
typeof error === 'object' &&
|
|
9
|
+
error !== null &&
|
|
10
|
+
'code' in error &&
|
|
11
|
+
error.code === 'ENOENT';
|
|
12
|
+
|
|
7
13
|
export class FileSystemSubAgentLivenessResolver implements SubAgentLivenessResolver {
|
|
8
14
|
constructor(private readonly runtimeRootDirectory: string | null) {}
|
|
9
15
|
|
|
@@ -20,7 +26,10 @@ export class FileSystemSubAgentLivenessResolver implements SubAgentLivenessResol
|
|
|
20
26
|
let content: string;
|
|
21
27
|
try {
|
|
22
28
|
content = fs.readFileSync(runningFilePath, 'utf8');
|
|
23
|
-
} catch {
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (isMissingFileError(error) && this.hasReadableRuntimeRootDirectory()) {
|
|
31
|
+
return new Set();
|
|
32
|
+
}
|
|
24
33
|
return null;
|
|
25
34
|
}
|
|
26
35
|
const liveIds = new Set<string>();
|
|
@@ -33,6 +42,17 @@ export class FileSystemSubAgentLivenessResolver implements SubAgentLivenessResol
|
|
|
33
42
|
return liveIds;
|
|
34
43
|
};
|
|
35
44
|
|
|
45
|
+
private hasReadableRuntimeRootDirectory = (): boolean => {
|
|
46
|
+
if (this.runtimeRootDirectory === null) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return fs.statSync(this.runtimeRootDirectory).isDirectory();
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
36
56
|
private resolveRunningSubAgentsFilePath = (
|
|
37
57
|
mainTranscriptPath: string | null,
|
|
38
58
|
): string | null => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
2
|
+
import { Sleeper } from '../../domain/usecases/adapter-interfaces/Sleeper';
|
|
2
3
|
import { TmuxSilentSessionNotificationRepository } from './TmuxSilentSessionNotificationRepository';
|
|
3
4
|
|
|
4
5
|
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
@@ -11,14 +12,55 @@ const createMockRunner = (): Mocked<LocalCommandRunner> => ({
|
|
|
11
12
|
}),
|
|
12
13
|
});
|
|
13
14
|
|
|
15
|
+
const createMockSleeper = (): Mocked<Sleeper> => ({
|
|
16
|
+
sleep: jest.fn().mockResolvedValue(undefined),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
20
|
+
|
|
21
|
+
const paneWithInputBoxContent = (inputBoxContent: string): string =>
|
|
22
|
+
[
|
|
23
|
+
'● The previous assistant output line',
|
|
24
|
+
'',
|
|
25
|
+
'────────────────────────────────────────',
|
|
26
|
+
inputBoxContent,
|
|
27
|
+
'────────────────────────────────────────',
|
|
28
|
+
' {Opus 4.5} (dev1) CTX[██░░░░░░░░]16%',
|
|
29
|
+
].join('\n');
|
|
30
|
+
|
|
31
|
+
const createPaneAwareRunner = (
|
|
32
|
+
paneTexts: string[],
|
|
33
|
+
): Mocked<LocalCommandRunner> => {
|
|
34
|
+
let capturedPaneIndex = 0;
|
|
35
|
+
return {
|
|
36
|
+
runCommand: jest.fn().mockImplementation((_program, args: string[]) => {
|
|
37
|
+
if (args[0] !== 'capture-pane') {
|
|
38
|
+
return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 });
|
|
39
|
+
}
|
|
40
|
+
const paneText =
|
|
41
|
+
paneTexts[Math.min(capturedPaneIndex, paneTexts.length - 1)];
|
|
42
|
+
capturedPaneIndex += 1;
|
|
43
|
+
return Promise.resolve({ stdout: paneText, stderr: '', exitCode: 0 });
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const submitKeyCallCount = (runner: Mocked<LocalCommandRunner>): number =>
|
|
49
|
+
runner.runCommand.mock.calls.filter(
|
|
50
|
+
(call) => call[1][0] === 'send-keys' && call[1][3] === 'Enter',
|
|
51
|
+
).length;
|
|
52
|
+
|
|
14
53
|
describe('TmuxSilentSessionNotificationRepository', () => {
|
|
15
54
|
describe('sendSelfCheckNotification', () => {
|
|
16
55
|
it('wraps the message in bracketed-paste framing, sends it literally, then submits it with Enter', async () => {
|
|
17
56
|
const runner = createMockRunner();
|
|
18
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
57
|
+
const repository = new TmuxSilentSessionNotificationRepository(
|
|
58
|
+
runner,
|
|
59
|
+
createMockSleeper(),
|
|
60
|
+
);
|
|
19
61
|
|
|
20
62
|
await repository.sendSelfCheckNotification(
|
|
21
|
-
|
|
63
|
+
sessionName,
|
|
22
64
|
'self check message',
|
|
23
65
|
);
|
|
24
66
|
|
|
@@ -27,26 +69,26 @@ describe('TmuxSilentSessionNotificationRepository', () => {
|
|
|
27
69
|
[
|
|
28
70
|
'send-keys',
|
|
29
71
|
'-t',
|
|
30
|
-
|
|
72
|
+
sessionName,
|
|
31
73
|
'-l',
|
|
32
74
|
'\x1b[200~self check message\x1b[201~',
|
|
33
75
|
],
|
|
34
76
|
]);
|
|
35
77
|
expect(runner.runCommand.mock.calls[1]).toEqual([
|
|
36
78
|
'tmux',
|
|
37
|
-
['send-keys', '-t',
|
|
79
|
+
['send-keys', '-t', sessionName, 'Enter'],
|
|
38
80
|
]);
|
|
39
81
|
});
|
|
40
82
|
|
|
41
83
|
it('flattens a multi-line message into a single line so no newline reaches the input box', async () => {
|
|
42
84
|
const runner = createMockRunner();
|
|
43
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
85
|
+
const repository = new TmuxSilentSessionNotificationRepository(
|
|
86
|
+
runner,
|
|
87
|
+
createMockSleeper(),
|
|
88
|
+
);
|
|
44
89
|
const multiLineMessage = 'first line\nsecond line\n\nthird line';
|
|
45
90
|
|
|
46
|
-
await repository.sendSelfCheckNotification(
|
|
47
|
-
'https_//github_com/owner/repo/issues/9',
|
|
48
|
-
multiLineMessage,
|
|
49
|
-
);
|
|
91
|
+
await repository.sendSelfCheckNotification(sessionName, multiLineMessage);
|
|
50
92
|
|
|
51
93
|
const literalArgument = runner.runCommand.mock.calls[0][1][4];
|
|
52
94
|
expect(literalArgument).not.toContain('\n');
|
|
@@ -58,10 +100,13 @@ describe('TmuxSilentSessionNotificationRepository', () => {
|
|
|
58
100
|
|
|
59
101
|
it('keeps the bracketed-paste framing around the flattened message', async () => {
|
|
60
102
|
const runner = createMockRunner();
|
|
61
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
103
|
+
const repository = new TmuxSilentSessionNotificationRepository(
|
|
104
|
+
runner,
|
|
105
|
+
createMockSleeper(),
|
|
106
|
+
);
|
|
62
107
|
|
|
63
108
|
await repository.sendSelfCheckNotification(
|
|
64
|
-
|
|
109
|
+
sessionName,
|
|
65
110
|
'first line\nsecond line',
|
|
66
111
|
);
|
|
67
112
|
|
|
@@ -71,16 +116,19 @@ describe('TmuxSilentSessionNotificationRepository', () => {
|
|
|
71
116
|
expect(literalBytes.endsWith('1b5b3230317e')).toBe(true);
|
|
72
117
|
expect(runner.runCommand.mock.calls[1]).toEqual([
|
|
73
118
|
'tmux',
|
|
74
|
-
['send-keys', '-t',
|
|
119
|
+
['send-keys', '-t', sessionName, 'Enter'],
|
|
75
120
|
]);
|
|
76
121
|
});
|
|
77
122
|
|
|
78
123
|
it('collapses the run of spaces left by consecutive line breaks', async () => {
|
|
79
124
|
const runner = createMockRunner();
|
|
80
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
125
|
+
const repository = new TmuxSilentSessionNotificationRepository(
|
|
126
|
+
runner,
|
|
127
|
+
createMockSleeper(),
|
|
128
|
+
);
|
|
81
129
|
|
|
82
130
|
await repository.sendSelfCheckNotification(
|
|
83
|
-
|
|
131
|
+
sessionName,
|
|
84
132
|
'section one\n\n\nsection two',
|
|
85
133
|
);
|
|
86
134
|
|
|
@@ -97,7 +145,10 @@ describe('TmuxSilentSessionNotificationRepository', () => {
|
|
|
97
145
|
stderr: "can't find session",
|
|
98
146
|
exitCode: 1,
|
|
99
147
|
});
|
|
100
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
148
|
+
const repository = new TmuxSilentSessionNotificationRepository(
|
|
149
|
+
runner,
|
|
150
|
+
createMockSleeper(),
|
|
151
|
+
);
|
|
101
152
|
|
|
102
153
|
await expect(
|
|
103
154
|
repository.sendSelfCheckNotification('missing_session', 'message'),
|
|
@@ -105,5 +156,96 @@ describe('TmuxSilentSessionNotificationRepository', () => {
|
|
|
105
156
|
'Failed to send notification to tmux session "missing_session"',
|
|
106
157
|
);
|
|
107
158
|
});
|
|
159
|
+
|
|
160
|
+
it('pushes the message out with another Enter while it is still sitting in the input box', async () => {
|
|
161
|
+
const runner = createPaneAwareRunner([
|
|
162
|
+
paneWithInputBoxContent(
|
|
163
|
+
'❯ [TDPM_SILENT_SESSION_SELF_CHECK_REMINDER] report your state',
|
|
164
|
+
),
|
|
165
|
+
paneWithInputBoxContent('❯ '),
|
|
166
|
+
]);
|
|
167
|
+
const repository = new TmuxSilentSessionNotificationRepository(
|
|
168
|
+
runner,
|
|
169
|
+
createMockSleeper(),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await repository.sendSelfCheckNotification(
|
|
173
|
+
sessionName,
|
|
174
|
+
'[TDPM_SILENT_SESSION_SELF_CHECK_REMINDER] report your state',
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(submitKeyCallCount(runner)).toBe(2);
|
|
178
|
+
expect(runner.runCommand.mock.calls[2]).toEqual([
|
|
179
|
+
'tmux',
|
|
180
|
+
['capture-pane', '-p', '-t', sessionName],
|
|
181
|
+
]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('sends no further Enter once the input box no longer holds the message', async () => {
|
|
185
|
+
const runner = createPaneAwareRunner([paneWithInputBoxContent('❯ ')]);
|
|
186
|
+
const repository = new TmuxSilentSessionNotificationRepository(
|
|
187
|
+
runner,
|
|
188
|
+
createMockSleeper(),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
await repository.sendSelfCheckNotification(
|
|
192
|
+
sessionName,
|
|
193
|
+
'[TDPM_SILENT_SESSION_SELF_CHECK_REMINDER] report your state',
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(submitKeyCallCount(runner)).toBe(1);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('sends no further Enter while the message is held as a queued message', async () => {
|
|
200
|
+
const runner = createPaneAwareRunner([
|
|
201
|
+
paneWithInputBoxContent('❯ Press up to edit queued messages'),
|
|
202
|
+
]);
|
|
203
|
+
const repository = new TmuxSilentSessionNotificationRepository(
|
|
204
|
+
runner,
|
|
205
|
+
createMockSleeper(),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await repository.sendSelfCheckNotification(
|
|
209
|
+
sessionName,
|
|
210
|
+
'[TDPM_SILENT_SESSION_SELF_CHECK_REMINDER] report your state',
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(submitKeyCallCount(runner)).toBe(1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('stops pushing after the configured attempt limit so newlines cannot pile up', async () => {
|
|
217
|
+
const runner = createPaneAwareRunner([
|
|
218
|
+
paneWithInputBoxContent(
|
|
219
|
+
'❯ [TDPM_SILENT_SESSION_SELF_CHECK_REMINDER] report your state',
|
|
220
|
+
),
|
|
221
|
+
]);
|
|
222
|
+
const repository = new TmuxSilentSessionNotificationRepository(
|
|
223
|
+
runner,
|
|
224
|
+
createMockSleeper(),
|
|
225
|
+
2,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await repository.sendSelfCheckNotification(
|
|
229
|
+
sessionName,
|
|
230
|
+
'[TDPM_SILENT_SESSION_SELF_CHECK_REMINDER] report your state',
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(submitKeyCallCount(runner)).toBe(3);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('waits for the bracketed-paste parsing and rendering window before reading the input box back', async () => {
|
|
237
|
+
const runner = createPaneAwareRunner([paneWithInputBoxContent('❯ ')]);
|
|
238
|
+
const sleeper = createMockSleeper();
|
|
239
|
+
const repository = new TmuxSilentSessionNotificationRepository(
|
|
240
|
+
runner,
|
|
241
|
+
sleeper,
|
|
242
|
+
3,
|
|
243
|
+
2500,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
await repository.sendSelfCheckNotification(sessionName, 'self check');
|
|
247
|
+
|
|
248
|
+
expect(sleeper.sleep).toHaveBeenCalledWith(2500);
|
|
249
|
+
});
|
|
108
250
|
});
|
|
109
251
|
});
|