github-issue-tower-defence-management 1.140.2 → 1.141.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 +7 -0
- package/README.md +7 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +43 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.js +32 -0
- package/bin/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.js.map +1 -0
- package/bin/adapter/repositories/FileSystemSessionAssistantTurnsRepository.js +135 -0
- package/bin/adapter/repositories/FileSystemSessionAssistantTurnsRepository.js.map +1 -0
- package/bin/adapter/repositories/FileSystemSessionDegenerationCooldownStateRepository.js +120 -0
- package/bin/adapter/repositories/FileSystemSessionDegenerationCooldownStateRepository.js.map +1 -0
- package/bin/domain/entities/OutputDegeneration.js +3 -0
- package/bin/domain/entities/OutputDegeneration.js.map +1 -0
- package/bin/domain/usecases/OutputDegenerationDetector.js +150 -0
- package/bin/domain/usecases/OutputDegenerationDetector.js.map +1 -0
- package/bin/domain/usecases/SessionOutputDegenerationRecoveryUseCase.js +84 -0
- package/bin/domain/usecases/SessionOutputDegenerationRecoveryUseCase.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SessionAssistantTurnsRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SessionAssistantTurnsRepository.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SessionDegenerationCooldownStateRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SessionDegenerationCooldownStateRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +44 -0
- package/src/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.test.ts +69 -0
- package/src/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.ts +71 -0
- package/src/adapter/repositories/FileSystemSessionAssistantTurnsRepository.test.ts +138 -0
- package/src/adapter/repositories/FileSystemSessionAssistantTurnsRepository.ts +113 -0
- package/src/adapter/repositories/FileSystemSessionDegenerationCooldownStateRepository.test.ts +85 -0
- package/src/adapter/repositories/FileSystemSessionDegenerationCooldownStateRepository.ts +105 -0
- package/src/domain/entities/OutputDegeneration.ts +10 -0
- package/src/domain/usecases/OutputDegenerationDetector.test.ts +167 -0
- package/src/domain/usecases/OutputDegenerationDetector.ts +169 -0
- package/src/domain/usecases/SessionOutputDegenerationRecoveryUseCase.test.ts +224 -0
- package/src/domain/usecases/SessionOutputDegenerationRecoveryUseCase.ts +142 -0
- package/src/domain/usecases/adapter-interfaces/SessionAssistantTurnsRepository.ts +6 -0
- package/src/domain/usecases/adapter-interfaces/SessionDegenerationCooldownStateRepository.ts +4 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.d.ts +19 -0
- package/types/adapter/entry-points/handlers/resetDegeneratedTmuxSessions.d.ts.map +1 -0
- package/types/adapter/repositories/FileSystemSessionAssistantTurnsRepository.d.ts +10 -0
- package/types/adapter/repositories/FileSystemSessionAssistantTurnsRepository.d.ts.map +1 -0
- package/types/adapter/repositories/FileSystemSessionDegenerationCooldownStateRepository.d.ts +15 -0
- package/types/adapter/repositories/FileSystemSessionDegenerationCooldownStateRepository.d.ts.map +1 -0
- package/types/domain/entities/OutputDegeneration.d.ts +10 -0
- package/types/domain/entities/OutputDegeneration.d.ts.map +1 -0
- package/types/domain/usecases/OutputDegenerationDetector.d.ts +32 -0
- package/types/domain/usecases/OutputDegenerationDetector.d.ts.map +1 -0
- package/types/domain/usecases/SessionOutputDegenerationRecoveryUseCase.d.ts +31 -0
- package/types/domain/usecases/SessionOutputDegenerationRecoveryUseCase.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SessionAssistantTurnsRepository.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/SessionAssistantTurnsRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SessionDegenerationCooldownStateRepository.d.ts +8 -0
- package/types/domain/usecases/adapter-interfaces/SessionDegenerationCooldownStateRepository.d.ts.map +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OutputDegenerationDetector = exports.OUTPUT_DEGENERATION_CROSS_TURN_MIN_TURNS = exports.OUTPUT_DEGENERATION_CROSS_TURN_WINDOW = exports.OUTPUT_DEGENERATION_REP4_MIN_TOKENS = exports.OUTPUT_DEGENERATION_REP4_THRESHOLD = exports.OUTPUT_DEGENERATION_ABSOLUTE_RUN_THRESHOLD = exports.OUTPUT_DEGENERATION_MAX_TOKEN_LENGTH = exports.OUTPUT_DEGENERATION_DOMINATION_FRACTION = exports.OUTPUT_DEGENERATION_REPEAT_THRESHOLD = void 0;
|
|
4
|
+
exports.OUTPUT_DEGENERATION_REPEAT_THRESHOLD = 5;
|
|
5
|
+
exports.OUTPUT_DEGENERATION_DOMINATION_FRACTION = 0.8;
|
|
6
|
+
exports.OUTPUT_DEGENERATION_MAX_TOKEN_LENGTH = 32;
|
|
7
|
+
exports.OUTPUT_DEGENERATION_ABSOLUTE_RUN_THRESHOLD = 15;
|
|
8
|
+
exports.OUTPUT_DEGENERATION_REP4_THRESHOLD = 0.3;
|
|
9
|
+
exports.OUTPUT_DEGENERATION_REP4_MIN_TOKENS = 40;
|
|
10
|
+
exports.OUTPUT_DEGENERATION_CROSS_TURN_WINDOW = 10;
|
|
11
|
+
exports.OUTPUT_DEGENERATION_CROSS_TURN_MIN_TURNS = 4;
|
|
12
|
+
const LIST_ITEM_LINE_PATTERN = /^\s*(?:[-*+]\s|\d+[.)]\s)/;
|
|
13
|
+
const TRAILING_ALPHA_TOKEN_PATTERN = /^[A-Za-z]+$/;
|
|
14
|
+
const splitTokens = (text) => text.split(/\s+/).filter((token) => token.length > 0);
|
|
15
|
+
const splitLines = (text) => text.split(/\r\n|\r|\n/);
|
|
16
|
+
class OutputDegenerationDetector {
|
|
17
|
+
constructor(repeatThreshold = exports.OUTPUT_DEGENERATION_REPEAT_THRESHOLD, dominationFraction = exports.OUTPUT_DEGENERATION_DOMINATION_FRACTION, maxTokenLength = exports.OUTPUT_DEGENERATION_MAX_TOKEN_LENGTH, absoluteRunThreshold = exports.OUTPUT_DEGENERATION_ABSOLUTE_RUN_THRESHOLD, rep4Threshold = exports.OUTPUT_DEGENERATION_REP4_THRESHOLD, rep4MinTokens = exports.OUTPUT_DEGENERATION_REP4_MIN_TOKENS, crossTurnWindow = exports.OUTPUT_DEGENERATION_CROSS_TURN_WINDOW, crossTurnMinTurns = exports.OUTPUT_DEGENERATION_CROSS_TURN_MIN_TURNS) {
|
|
18
|
+
this.repeatThreshold = repeatThreshold;
|
|
19
|
+
this.dominationFraction = dominationFraction;
|
|
20
|
+
this.maxTokenLength = maxTokenLength;
|
|
21
|
+
this.absoluteRunThreshold = absoluteRunThreshold;
|
|
22
|
+
this.rep4Threshold = rep4Threshold;
|
|
23
|
+
this.rep4MinTokens = rep4MinTokens;
|
|
24
|
+
this.crossTurnWindow = crossTurnWindow;
|
|
25
|
+
this.crossTurnMinTurns = crossTurnMinTurns;
|
|
26
|
+
this.maxConsecutiveShortTokenRun = (text) => {
|
|
27
|
+
let bestToken = null;
|
|
28
|
+
let bestRun = 0;
|
|
29
|
+
let totalTokens = 0;
|
|
30
|
+
let currentToken = null;
|
|
31
|
+
let currentRun = 0;
|
|
32
|
+
for (const token of splitTokens(text)) {
|
|
33
|
+
totalTokens += 1;
|
|
34
|
+
if (token.length > this.maxTokenLength) {
|
|
35
|
+
currentToken = null;
|
|
36
|
+
currentRun = 0;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (token === currentToken) {
|
|
40
|
+
currentRun += 1;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
currentToken = token;
|
|
44
|
+
currentRun = 1;
|
|
45
|
+
}
|
|
46
|
+
if (currentRun > bestRun) {
|
|
47
|
+
bestRun = currentRun;
|
|
48
|
+
bestToken = currentToken;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { token: bestToken, run: bestRun, total: totalTokens };
|
|
52
|
+
};
|
|
53
|
+
this.isIntraTurnDegeneration = (text) => {
|
|
54
|
+
const { run, total } = this.maxConsecutiveShortTokenRun(text);
|
|
55
|
+
if (total === 0) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (run >= this.repeatThreshold && run >= this.dominationFraction * total) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
const filtered = this.stripRepetitionExemptSpans(text);
|
|
62
|
+
const filteredRun = this.maxConsecutiveShortTokenRun(filtered);
|
|
63
|
+
if (filteredRun.run >= this.absoluteRunThreshold) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (filteredRun.total >= this.rep4MinTokens &&
|
|
67
|
+
this.duplicateNgramFraction(filtered, 4) >= this.rep4Threshold) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
};
|
|
72
|
+
this.detectCrossTurnDegeneration = (texts) => {
|
|
73
|
+
const counts = new Map();
|
|
74
|
+
for (const text of texts.slice(0, this.crossTurnWindow)) {
|
|
75
|
+
const token = this.trailingSpuriousToken(text);
|
|
76
|
+
if (token === null) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
80
|
+
}
|
|
81
|
+
let bestToken = null;
|
|
82
|
+
let bestCount = 0;
|
|
83
|
+
for (const [token, count] of counts) {
|
|
84
|
+
if (count > bestCount) {
|
|
85
|
+
bestCount = count;
|
|
86
|
+
bestToken = token;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (bestToken !== null && bestCount >= this.crossTurnMinTurns) {
|
|
90
|
+
return { token: bestToken, turnCount: bestCount };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
this.stripRepetitionExemptSpans = (text) => {
|
|
95
|
+
const keptLines = [];
|
|
96
|
+
let inFence = false;
|
|
97
|
+
for (const line of splitLines(text)) {
|
|
98
|
+
const stripped = line.trim();
|
|
99
|
+
if (stripped.startsWith('```') || stripped.startsWith('~~~')) {
|
|
100
|
+
inFence = !inFence;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (inFence) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (line.includes('|')) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (LIST_ITEM_LINE_PATTERN.test(line)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
keptLines.push(line);
|
|
113
|
+
}
|
|
114
|
+
return keptLines.join('\n');
|
|
115
|
+
};
|
|
116
|
+
this.duplicateNgramFraction = (text, n) => {
|
|
117
|
+
const tokens = splitTokens(text).filter((token) => token.length <= this.maxTokenLength);
|
|
118
|
+
if (tokens.length < n + 1) {
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
const ngrams = [];
|
|
122
|
+
for (let index = 0; index <= tokens.length - n; index += 1) {
|
|
123
|
+
ngrams.push(tokens.slice(index, index + n).join('\u0000'));
|
|
124
|
+
}
|
|
125
|
+
const total = ngrams.length;
|
|
126
|
+
if (total < 2) {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
const unique = new Set(ngrams).size;
|
|
130
|
+
return 1 - unique / total;
|
|
131
|
+
};
|
|
132
|
+
this.trailingSpuriousToken = (text) => {
|
|
133
|
+
const stripped = text.replace(/\s+$/, '');
|
|
134
|
+
if (stripped.length === 0) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const lines = splitLines(stripped);
|
|
138
|
+
const lastLine = lines[lines.length - 1].trim();
|
|
139
|
+
if (lastLine.length === 0 || lastLine.length > this.maxTokenLength) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (!TRAILING_ALPHA_TOKEN_PATTERN.test(lastLine)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return lastLine;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.OutputDegenerationDetector = OutputDegenerationDetector;
|
|
150
|
+
//# sourceMappingURL=OutputDegenerationDetector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OutputDegenerationDetector.js","sourceRoot":"","sources":["../../../src/domain/usecases/OutputDegenerationDetector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionOutputDegenerationRecoveryUseCase = exports.DEFAULT_OUTPUT_DEGENERATION_WARNING_MESSAGE = exports.DEFAULT_OUTPUT_DEGENERATION_COOLDOWN_SECONDS = exports.DEFAULT_OUTPUT_DEGENERATION_GRACE_SECONDS = void 0;
|
|
4
|
+
const OutputDegenerationDetector_1 = require("./OutputDegenerationDetector");
|
|
5
|
+
const ResolveInteractiveLiveSessionsUseCase_1 = require("./ResolveInteractiveLiveSessionsUseCase");
|
|
6
|
+
const NotifySilentLiveSessionsUseCase_1 = require("./NotifySilentLiveSessionsUseCase");
|
|
7
|
+
exports.DEFAULT_OUTPUT_DEGENERATION_GRACE_SECONDS = 5;
|
|
8
|
+
exports.DEFAULT_OUTPUT_DEGENERATION_COOLDOWN_SECONDS = 300;
|
|
9
|
+
exports.DEFAULT_OUTPUT_DEGENERATION_WARNING_MESSAGE = 'OUTPUT DEGENERATION DETECTED: your session is about to be reset. Immediately write a checkpoint to your assigned task issue (single concrete next action to resume, working directory and branch, in-flight sub-agent branch/PR URLs).';
|
|
10
|
+
class SessionOutputDegenerationRecoveryUseCase {
|
|
11
|
+
constructor(liveSessionProcessSnapshotProvider, interactiveLiveSessionTranscriptResolver, assistantTurnsRepository, notificationRepository, tmuxSessionRepository, cooldownStateRepository, sleeper, detector = new OutputDegenerationDetector_1.OutputDegenerationDetector()) {
|
|
12
|
+
this.liveSessionProcessSnapshotProvider = liveSessionProcessSnapshotProvider;
|
|
13
|
+
this.interactiveLiveSessionTranscriptResolver = interactiveLiveSessionTranscriptResolver;
|
|
14
|
+
this.assistantTurnsRepository = assistantTurnsRepository;
|
|
15
|
+
this.notificationRepository = notificationRepository;
|
|
16
|
+
this.tmuxSessionRepository = tmuxSessionRepository;
|
|
17
|
+
this.cooldownStateRepository = cooldownStateRepository;
|
|
18
|
+
this.sleeper = sleeper;
|
|
19
|
+
this.detector = detector;
|
|
20
|
+
this.resolveInteractiveLiveSessions = new ResolveInteractiveLiveSessionsUseCase_1.ResolveInteractiveLiveSessionsUseCase();
|
|
21
|
+
this.run = async (params) => {
|
|
22
|
+
const snapshot = await this.liveSessionProcessSnapshotProvider.getSnapshot();
|
|
23
|
+
const allInteractiveSessions = this.resolveInteractiveLiveSessions.resolve(snapshot);
|
|
24
|
+
const interactiveSessions = allInteractiveSessions.filter((session) => (0, NotifySilentLiveSessionsUseCase_1.isGitHubIssueOrPullRequestSessionName)(session.sessionName));
|
|
25
|
+
const transcriptPathBySessionName = this.interactiveLiveSessionTranscriptResolver.resolveTranscriptPaths(interactiveSessions);
|
|
26
|
+
const turnsBySessionName = await this.assistantTurnsRepository.listRecentAssistantTurnsBySessionName(transcriptPathBySessionName, OutputDegenerationDetector_1.OUTPUT_DEGENERATION_CROSS_TURN_WINDOW);
|
|
27
|
+
const lastResetBySessionName = await this.cooldownStateRepository.loadLastResetEpochSecondsBySessionName();
|
|
28
|
+
const nowEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
29
|
+
let detectedCount = 0;
|
|
30
|
+
let resetCount = 0;
|
|
31
|
+
let dryRunCount = 0;
|
|
32
|
+
let cooldownSkippedCount = 0;
|
|
33
|
+
for (const session of interactiveSessions) {
|
|
34
|
+
const sessionName = session.sessionName;
|
|
35
|
+
const lastResetEpochSeconds = lastResetBySessionName.get(sessionName);
|
|
36
|
+
if (lastResetEpochSeconds !== undefined &&
|
|
37
|
+
nowEpochSeconds - lastResetEpochSeconds < params.cooldownSeconds) {
|
|
38
|
+
cooldownSkippedCount += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const turns = turnsBySessionName.get(sessionName) ?? [];
|
|
42
|
+
const newestTurn = turns.length > 0 ? turns[0] : null;
|
|
43
|
+
const intraTurn = newestTurn !== null &&
|
|
44
|
+
this.detector.isIntraTurnDegeneration(newestTurn);
|
|
45
|
+
const crossTurn = this.detector.detectCrossTurnDegeneration(turns);
|
|
46
|
+
if (!intraTurn && crossTurn === null) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
detectedCount += 1;
|
|
50
|
+
if (intraTurn && newestTurn !== null) {
|
|
51
|
+
const { token, run, total } = this.detector.maxConsecutiveShortTokenRun(newestTurn);
|
|
52
|
+
console.log(`Output degeneration detected (intra-turn) session=${sessionName} token=${JSON.stringify(token)} run=${run} turnTokens=${total} enabled=${params.enabled}`);
|
|
53
|
+
}
|
|
54
|
+
if (crossTurn !== null) {
|
|
55
|
+
console.log(`Output degeneration detected (cross-turn) session=${sessionName} token=${JSON.stringify(crossTurn.token)} turns=${crossTurn.turnCount}/${Math.min(turns.length, OutputDegenerationDetector_1.OUTPUT_DEGENERATION_CROSS_TURN_WINDOW)} enabled=${params.enabled}`);
|
|
56
|
+
}
|
|
57
|
+
if (params.enabled) {
|
|
58
|
+
try {
|
|
59
|
+
await this.notificationRepository.sendSelfCheckNotification(sessionName, params.warningMessage);
|
|
60
|
+
await this.sleeper.sleep(params.graceSeconds * 1000);
|
|
61
|
+
await this.tmuxSessionRepository.killSession(sessionName);
|
|
62
|
+
resetCount += 1;
|
|
63
|
+
console.log(`Output degeneration reset session=${sessionName} graceSeconds=${params.graceSeconds}`);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`Failed to reset degenerated session ${sessionName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
dryRunCount += 1;
|
|
72
|
+
console.log(`Output degeneration dry-run: would warn and reset session=${sessionName}`);
|
|
73
|
+
}
|
|
74
|
+
await this.cooldownStateRepository.recordReset({
|
|
75
|
+
sessionName,
|
|
76
|
+
now: params.now,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
console.log(`Output degeneration recovery: detected ${detectedCount} degenerated session(s) of ${interactiveSessions.length} interactive session(s); reset ${resetCount}, dry-run would-reset ${dryRunCount}, cooldown-skipped ${cooldownSkippedCount}.`);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.SessionOutputDegenerationRecoveryUseCase = SessionOutputDegenerationRecoveryUseCase;
|
|
84
|
+
//# sourceMappingURL=SessionOutputDegenerationRecoveryUseCase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionOutputDegenerationRecoveryUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/SessionOutputDegenerationRecoveryUseCase.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionAssistantTurnsRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/SessionAssistantTurnsRepository.ts"],"names":[],"mappings":""}
|
package/bin/domain/usecases/adapter-interfaces/SessionDegenerationCooldownStateRepository.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionDegenerationCooldownStateRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/SessionDegenerationCooldownStateRepository.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
notifySilentTmuxSessions,
|
|
17
17
|
DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS,
|
|
18
18
|
} from './notifySilentTmuxSessions';
|
|
19
|
+
import {
|
|
20
|
+
resetDegeneratedTmuxSessions,
|
|
21
|
+
DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS,
|
|
22
|
+
} from './resetDegeneratedTmuxSessions';
|
|
19
23
|
import { writeRotationOrderFile } from './rotationOrderFileWriter';
|
|
20
24
|
import {
|
|
21
25
|
fetchProjectReadme,
|
|
@@ -145,6 +149,11 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
145
149
|
silentSubAgentIdleMessageFooter?: string;
|
|
146
150
|
silentSubAgentLongRunningMessageHeader?: string;
|
|
147
151
|
silentSubAgentLongRunningMessageFooter?: string;
|
|
152
|
+
outputDegenerationResetEnabled?: boolean;
|
|
153
|
+
outputDegenerationWarningMessage?: string;
|
|
154
|
+
outputDegenerationGraceSeconds?: number;
|
|
155
|
+
outputDegenerationCooldownSeconds?: number;
|
|
156
|
+
outputDegenerationCooldownStateFilePath?: string;
|
|
148
157
|
credentials: {
|
|
149
158
|
manager: {
|
|
150
159
|
github: {
|
|
@@ -740,6 +749,41 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
740
749
|
}`,
|
|
741
750
|
);
|
|
742
751
|
}
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
const outputDegenerationResetEnabled =
|
|
755
|
+
mergedInput.outputDegenerationResetEnabled ??
|
|
756
|
+
process.env.TDPM_OUTPUT_DEGENERATION_RESET_ENABLED === 'true';
|
|
757
|
+
await resetDegeneratedTmuxSessions({
|
|
758
|
+
enabled: outputDegenerationResetEnabled,
|
|
759
|
+
localCommandRunner: nodeLocalCommandRunner,
|
|
760
|
+
warningMessage:
|
|
761
|
+
mergedInput.outputDegenerationWarningMessage ??
|
|
762
|
+
process.env.TDPM_OUTPUT_DEGENERATION_WARNING_MESSAGE ??
|
|
763
|
+
DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS.warningMessage,
|
|
764
|
+
graceSeconds: readSilentSeconds(
|
|
765
|
+
mergedInput.outputDegenerationGraceSeconds,
|
|
766
|
+
process.env.TDPM_OUTPUT_DEGENERATION_GRACE_SECONDS,
|
|
767
|
+
DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS.graceSeconds,
|
|
768
|
+
),
|
|
769
|
+
cooldownSeconds: readSilentSeconds(
|
|
770
|
+
mergedInput.outputDegenerationCooldownSeconds,
|
|
771
|
+
process.env.TDPM_OUTPUT_DEGENERATION_COOLDOWN_SECONDS,
|
|
772
|
+
DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS.cooldownSeconds,
|
|
773
|
+
),
|
|
774
|
+
cooldownStateFilePath:
|
|
775
|
+
mergedInput.outputDegenerationCooldownStateFilePath ??
|
|
776
|
+
process.env.TDPM_OUTPUT_DEGENERATION_COOLDOWN_STATE_FILE_PATH ??
|
|
777
|
+
null,
|
|
778
|
+
now: inTmuxNow,
|
|
779
|
+
});
|
|
780
|
+
} catch (error) {
|
|
781
|
+
console.error(
|
|
782
|
+
`Failed to reset degenerated tmux sessions: ${
|
|
783
|
+
error instanceof Error ? error.message : String(error)
|
|
784
|
+
}`,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
743
787
|
}
|
|
744
788
|
return result;
|
|
745
789
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
resetDegeneratedTmuxSessions,
|
|
6
|
+
DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS,
|
|
7
|
+
} from './resetDegeneratedTmuxSessions';
|
|
8
|
+
import { LocalCommandRunner } from '../../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
9
|
+
|
|
10
|
+
describe('resetDegeneratedTmuxSessions', () => {
|
|
11
|
+
let temporaryDirectory: string;
|
|
12
|
+
let cooldownStateFilePath: string;
|
|
13
|
+
let loggedMessages: string[];
|
|
14
|
+
let consoleLogSpy: jest.SpiedFunction<typeof console.log>;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
temporaryDirectory = fs.mkdtempSync(
|
|
18
|
+
path.join(os.tmpdir(), 'reset-degenerated-'),
|
|
19
|
+
);
|
|
20
|
+
cooldownStateFilePath = path.join(temporaryDirectory, 'cooldown.json');
|
|
21
|
+
loggedMessages = [];
|
|
22
|
+
consoleLogSpy = jest
|
|
23
|
+
.spyOn(console, 'log')
|
|
24
|
+
.mockImplementation((...args: unknown[]): void => {
|
|
25
|
+
loggedMessages.push(args.map((arg) => String(arg)).join(' '));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
consoleLogSpy.mockRestore();
|
|
31
|
+
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('wires the real adapters and runs end to end without acting when no sessions exist', async () => {
|
|
35
|
+
const runCommand: jest.MockedFunction<LocalCommandRunner['runCommand']> =
|
|
36
|
+
jest.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 });
|
|
37
|
+
const commandRunner: LocalCommandRunner = { runCommand };
|
|
38
|
+
|
|
39
|
+
await resetDegeneratedTmuxSessions({
|
|
40
|
+
enabled: false,
|
|
41
|
+
localCommandRunner: commandRunner,
|
|
42
|
+
warningMessage:
|
|
43
|
+
DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS.warningMessage,
|
|
44
|
+
graceSeconds: DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS.graceSeconds,
|
|
45
|
+
cooldownSeconds:
|
|
46
|
+
DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS.cooldownSeconds,
|
|
47
|
+
cooldownStateFilePath,
|
|
48
|
+
now: new Date('2026-07-26T00:00:00Z'),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const summaryLogged = loggedMessages.some((message) =>
|
|
52
|
+
message.includes(
|
|
53
|
+
'Output degeneration recovery: detected 0 degenerated session(s) of 0 interactive session(s)',
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
expect(summaryLogged).toBe(true);
|
|
57
|
+
const killInvoked = runCommand.mock.calls.some((callArguments) =>
|
|
58
|
+
callArguments[1].includes('kill-session'),
|
|
59
|
+
);
|
|
60
|
+
expect(killInvoked).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('exposes safe defaults with a five second grace and 300 second cooldown', () => {
|
|
64
|
+
expect(DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS.graceSeconds).toBe(5);
|
|
65
|
+
expect(DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS.cooldownSeconds).toBe(
|
|
66
|
+
300,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LocalCommandRunner } from '../../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
2
|
+
import { ProcessEnvironReader } from '../../../domain/usecases/adapter-interfaces/ProcessEnvironReader';
|
|
3
|
+
import {
|
|
4
|
+
SessionOutputDegenerationRecoveryUseCase,
|
|
5
|
+
DEFAULT_OUTPUT_DEGENERATION_GRACE_SECONDS,
|
|
6
|
+
DEFAULT_OUTPUT_DEGENERATION_COOLDOWN_SECONDS,
|
|
7
|
+
DEFAULT_OUTPUT_DEGENERATION_WARNING_MESSAGE,
|
|
8
|
+
} from '../../../domain/usecases/SessionOutputDegenerationRecoveryUseCase';
|
|
9
|
+
import { LocalProcessLiveSessionProcessSnapshotProvider } from '../../repositories/LocalProcessLiveSessionProcessSnapshotProvider';
|
|
10
|
+
import { ProcFsProcessEnvironReader } from '../../repositories/ProcFsProcessEnvironReader';
|
|
11
|
+
import { FileSystemInteractiveLiveSessionTranscriptResolver } from '../../repositories/FileSystemInteractiveLiveSessionTranscriptResolver';
|
|
12
|
+
import { FileSystemSessionAssistantTurnsRepository } from '../../repositories/FileSystemSessionAssistantTurnsRepository';
|
|
13
|
+
import { FileSystemSessionDegenerationCooldownStateRepository } from '../../repositories/FileSystemSessionDegenerationCooldownStateRepository';
|
|
14
|
+
import { TmuxSilentSessionNotificationRepository } from '../../repositories/TmuxSilentSessionNotificationRepository';
|
|
15
|
+
import { NodeTmuxSessionRepository } from '../../repositories/NodeTmuxSessionRepository';
|
|
16
|
+
import { RealSleeper } from '../../repositories/RealSleeper';
|
|
17
|
+
|
|
18
|
+
export type ResetDegeneratedTmuxSessionsParams = {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
localCommandRunner: LocalCommandRunner;
|
|
21
|
+
processEnvironReader?: ProcessEnvironReader;
|
|
22
|
+
warningMessage: string;
|
|
23
|
+
graceSeconds: number;
|
|
24
|
+
cooldownSeconds: number;
|
|
25
|
+
cooldownStateFilePath: string | null;
|
|
26
|
+
now: Date;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const resetDegeneratedTmuxSessions = async (
|
|
30
|
+
params: ResetDegeneratedTmuxSessionsParams,
|
|
31
|
+
): Promise<void> => {
|
|
32
|
+
const {
|
|
33
|
+
enabled,
|
|
34
|
+
localCommandRunner,
|
|
35
|
+
processEnvironReader,
|
|
36
|
+
warningMessage,
|
|
37
|
+
graceSeconds,
|
|
38
|
+
cooldownSeconds,
|
|
39
|
+
cooldownStateFilePath,
|
|
40
|
+
now,
|
|
41
|
+
} = params;
|
|
42
|
+
const useCase = new SessionOutputDegenerationRecoveryUseCase(
|
|
43
|
+
new LocalProcessLiveSessionProcessSnapshotProvider(
|
|
44
|
+
localCommandRunner,
|
|
45
|
+
processEnvironReader ?? new ProcFsProcessEnvironReader(),
|
|
46
|
+
),
|
|
47
|
+
new FileSystemInteractiveLiveSessionTranscriptResolver(),
|
|
48
|
+
new FileSystemSessionAssistantTurnsRepository(),
|
|
49
|
+
new TmuxSilentSessionNotificationRepository(localCommandRunner),
|
|
50
|
+
new NodeTmuxSessionRepository(localCommandRunner),
|
|
51
|
+
cooldownStateFilePath !== null
|
|
52
|
+
? new FileSystemSessionDegenerationCooldownStateRepository(
|
|
53
|
+
cooldownStateFilePath,
|
|
54
|
+
)
|
|
55
|
+
: new FileSystemSessionDegenerationCooldownStateRepository(),
|
|
56
|
+
new RealSleeper(),
|
|
57
|
+
);
|
|
58
|
+
await useCase.run({
|
|
59
|
+
enabled,
|
|
60
|
+
warningMessage,
|
|
61
|
+
graceSeconds,
|
|
62
|
+
cooldownSeconds,
|
|
63
|
+
now,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS = {
|
|
68
|
+
warningMessage: DEFAULT_OUTPUT_DEGENERATION_WARNING_MESSAGE,
|
|
69
|
+
graceSeconds: DEFAULT_OUTPUT_DEGENERATION_GRACE_SECONDS,
|
|
70
|
+
cooldownSeconds: DEFAULT_OUTPUT_DEGENERATION_COOLDOWN_SECONDS,
|
|
71
|
+
} as const;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { FileSystemSessionAssistantTurnsRepository } from './FileSystemSessionAssistantTurnsRepository';
|
|
5
|
+
|
|
6
|
+
describe('FileSystemSessionAssistantTurnsRepository', () => {
|
|
7
|
+
let temporaryDirectory: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
temporaryDirectory = fs.mkdtempSync(
|
|
11
|
+
path.join(os.tmpdir(), 'assistant-turns-'),
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const writeTranscript = (fileName: string, lines: string[]): string => {
|
|
20
|
+
const filePath = path.join(temporaryDirectory, fileName);
|
|
21
|
+
fs.writeFileSync(filePath, `${lines.join('\n')}\n`);
|
|
22
|
+
return filePath;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
it('returns recent assistant turn texts newest first, ignoring non-assistant lines', async () => {
|
|
26
|
+
const filePath = writeTranscript('transcript.jsonl', [
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
type: 'assistant',
|
|
29
|
+
message: { content: [{ type: 'text', text: 'first turn' }] },
|
|
30
|
+
}),
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
type: 'user',
|
|
33
|
+
message: { content: [{ type: 'text', text: 'a user turn' }] },
|
|
34
|
+
}),
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
type: 'assistant',
|
|
37
|
+
message: {
|
|
38
|
+
content: [
|
|
39
|
+
{ type: 'text', text: 'second turn' },
|
|
40
|
+
{ type: 'tool_use', id: 'tool-1', name: 'Bash' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
type: 'assistant',
|
|
46
|
+
message: { content: [{ type: 'text', text: 'third turn' }] },
|
|
47
|
+
}),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const repository = new FileSystemSessionAssistantTurnsRepository();
|
|
51
|
+
const result = await repository.listRecentAssistantTurnsBySessionName(
|
|
52
|
+
new Map([['session', filePath]]),
|
|
53
|
+
10,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(result.get('session')).toEqual([
|
|
57
|
+
'third turn',
|
|
58
|
+
'second turn',
|
|
59
|
+
'first turn',
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('limits the number of turns returned to maxTurnsPerSession', async () => {
|
|
64
|
+
const filePath = writeTranscript('transcript.jsonl', [
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
type: 'assistant',
|
|
67
|
+
message: { content: [{ type: 'text', text: 'oldest' }] },
|
|
68
|
+
}),
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
type: 'assistant',
|
|
71
|
+
message: { content: [{ type: 'text', text: 'middle' }] },
|
|
72
|
+
}),
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
type: 'assistant',
|
|
75
|
+
message: { content: [{ type: 'text', text: 'newest' }] },
|
|
76
|
+
}),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const repository = new FileSystemSessionAssistantTurnsRepository();
|
|
80
|
+
const result = await repository.listRecentAssistantTurnsBySessionName(
|
|
81
|
+
new Map([['session', filePath]]),
|
|
82
|
+
2,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(result.get('session')).toEqual(['newest', 'middle']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('reads a string content assistant message', async () => {
|
|
89
|
+
const filePath = writeTranscript('transcript.jsonl', [
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
type: 'assistant',
|
|
92
|
+
message: { content: 'plain string turn' },
|
|
93
|
+
}),
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const repository = new FileSystemSessionAssistantTurnsRepository();
|
|
97
|
+
const result = await repository.listRecentAssistantTurnsBySessionName(
|
|
98
|
+
new Map([['session', filePath]]),
|
|
99
|
+
10,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(result.get('session')).toEqual(['plain string turn']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('omits a session whose transcript is missing', async () => {
|
|
106
|
+
const repository = new FileSystemSessionAssistantTurnsRepository();
|
|
107
|
+
const result = await repository.listRecentAssistantTurnsBySessionName(
|
|
108
|
+
new Map([['session', path.join(temporaryDirectory, 'absent.jsonl')]]),
|
|
109
|
+
10,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(result.has('session')).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('drops the first partial line when reading only the transcript tail', async () => {
|
|
116
|
+
const firstLine = JSON.stringify({
|
|
117
|
+
type: 'assistant',
|
|
118
|
+
message: { content: [{ type: 'text', text: 'partial old turn' }] },
|
|
119
|
+
});
|
|
120
|
+
const secondLine = JSON.stringify({
|
|
121
|
+
type: 'assistant',
|
|
122
|
+
message: { content: [{ type: 'text', text: 'complete new turn' }] },
|
|
123
|
+
});
|
|
124
|
+
const filePath = writeTranscript('transcript.jsonl', [
|
|
125
|
+
firstLine,
|
|
126
|
+
secondLine,
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const tailBytes = Buffer.byteLength(`${secondLine}\n`) + 5;
|
|
130
|
+
const repository = new FileSystemSessionAssistantTurnsRepository(tailBytes);
|
|
131
|
+
const result = await repository.listRecentAssistantTurnsBySessionName(
|
|
132
|
+
new Map([['session', filePath]]),
|
|
133
|
+
10,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(result.get('session')).toEqual(['complete new turn']);
|
|
137
|
+
});
|
|
138
|
+
});
|