github-issue-tower-defence-management 1.148.12 → 1.148.14
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/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +6 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +3 -3
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.js +64 -0
- package/bin/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.js.map +1 -0
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +71 -3
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +2 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js +20 -0
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +10 -2
- package/src/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.test.ts +55 -0
- package/src/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.ts +36 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.queuedReply.test.ts +171 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +90 -3
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +72 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +6 -1
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +119 -0
- package/src/domain/usecases/IssueRejectionEvaluator.ts +19 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +6 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.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/ownerReplyMarkerDirectoryResolve.d.ts +2 -0
- package/types/adapter/entry-points/handlers/ownerReplyMarkerDirectoryResolve.d.ts.map +1 -0
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +3 -1
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +2 -0
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotifyFinishedIssuePreparationUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"NotifyFinishedIssuePreparationUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
notifySilentTmuxSessions,
|
|
19
19
|
DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS,
|
|
20
20
|
} from './notifySilentTmuxSessions';
|
|
21
|
+
import { ownerReplyMarkerDirectoryResolve } from './ownerReplyMarkerDirectoryResolve';
|
|
21
22
|
import {
|
|
22
23
|
resetDegeneratedTmuxSessions,
|
|
23
24
|
DEFAULT_RESET_DEGENERATED_TMUX_SESSIONS_PARAMS,
|
|
@@ -132,6 +133,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
132
133
|
tokenExhaustionHandoverStateFilePath?: string;
|
|
133
134
|
silentNotificationEnabled?: boolean;
|
|
134
135
|
ownerCallMarker?: string;
|
|
136
|
+
ownerReplyMarkerDirectory?: string;
|
|
135
137
|
subAgentOutputRootDirectory?: string;
|
|
136
138
|
subAgentProcessMatchPattern?: string;
|
|
137
139
|
subAgentTranscriptRootDirectory?: string;
|
|
@@ -732,6 +734,14 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
732
734
|
process.env.TDPM_SUBAGENT_TRANSCRIPT_ROOT_DIRECTORY ??
|
|
733
735
|
null;
|
|
734
736
|
const getuid = process.getuid?.bind(process);
|
|
737
|
+
// The status line writes the reply time it renders here, and the owner sees only that
|
|
738
|
+
// status line, so reading the same file keeps the reminder decision and the owner's view
|
|
739
|
+
// of it from disagreeing.
|
|
740
|
+
const ownerReplyMarkerDirectory = ownerReplyMarkerDirectoryResolve(
|
|
741
|
+
mergedInput.ownerReplyMarkerDirectory ?? null,
|
|
742
|
+
process.env,
|
|
743
|
+
getuid === undefined ? null : getuid(),
|
|
744
|
+
);
|
|
735
745
|
const subAgentRuntimeRootDirectory =
|
|
736
746
|
mergedInput.subAgentRuntimeRootDirectory ??
|
|
737
747
|
process.env.TDPM_SUBAGENT_RUNTIME_ROOT_DIRECTORY ??
|
|
@@ -742,6 +752,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
742
752
|
enabled: silentNotificationEnabled,
|
|
743
753
|
localCommandRunner: nodeLocalCommandRunner,
|
|
744
754
|
ownerCallMarker,
|
|
755
|
+
ownerReplyMarkerDirectory,
|
|
745
756
|
subAgentOutputRootDirectory,
|
|
746
757
|
subAgentProcessMatchPattern,
|
|
747
758
|
subAgentTranscriptRootDirectory,
|
|
@@ -45,6 +45,7 @@ export type NotifySilentTmuxSessionsParams = {
|
|
|
45
45
|
localCommandRunner: LocalCommandRunner;
|
|
46
46
|
processEnvironReader?: ProcessEnvironReader;
|
|
47
47
|
ownerCallMarker: string | null;
|
|
48
|
+
ownerReplyMarkerDirectory?: string | null;
|
|
48
49
|
subAgentOutputRootDirectory: string | null;
|
|
49
50
|
subAgentProcessMatchPattern: string | null;
|
|
50
51
|
subAgentTranscriptRootDirectory: string | null;
|
|
@@ -67,9 +68,13 @@ export type NotifySilentTmuxSessionsParams = {
|
|
|
67
68
|
|
|
68
69
|
const createOwnerCallStatusProvider = (
|
|
69
70
|
ownerCallMarker: string | null,
|
|
71
|
+
ownerReplyMarkerDirectory: string | null,
|
|
70
72
|
): OwnerCallStatusProvider => {
|
|
71
73
|
if (ownerCallMarker !== null && ownerCallMarker.length > 0) {
|
|
72
|
-
return new TranscriptOwnerCallStatusProvider(
|
|
74
|
+
return new TranscriptOwnerCallStatusProvider(
|
|
75
|
+
ownerCallMarker,
|
|
76
|
+
ownerReplyMarkerDirectory,
|
|
77
|
+
);
|
|
73
78
|
}
|
|
74
79
|
return new NoUnansweredOwnerCallStatusProvider();
|
|
75
80
|
};
|
|
@@ -154,7 +159,10 @@ export const notifySilentTmuxSessions = async (
|
|
|
154
159
|
localCommandRunner,
|
|
155
160
|
now,
|
|
156
161
|
),
|
|
157
|
-
createOwnerCallStatusProvider(
|
|
162
|
+
createOwnerCallStatusProvider(
|
|
163
|
+
ownerCallMarker,
|
|
164
|
+
params.ownerReplyMarkerDirectory ?? null,
|
|
165
|
+
),
|
|
158
166
|
new TmuxSilentSessionNotificationRepository(
|
|
159
167
|
localCommandRunner,
|
|
160
168
|
new RealSleeper(),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ownerReplyMarkerDirectoryResolve } from './ownerReplyMarkerDirectoryResolve';
|
|
2
|
+
|
|
3
|
+
describe('ownerReplyMarkerDirectoryResolve', () => {
|
|
4
|
+
const userId = 1000;
|
|
5
|
+
|
|
6
|
+
it('returns the directory the configuration supplies', () => {
|
|
7
|
+
expect(
|
|
8
|
+
ownerReplyMarkerDirectoryResolve(
|
|
9
|
+
'/configured/markers',
|
|
10
|
+
{
|
|
11
|
+
TDPM_SILENT_OWNER_REPLY_MARKER_DIRECTORY: '/from/tdpm/environment',
|
|
12
|
+
CLAUDE_STATUSLINE_CALL_DIR: '/from/status/line/environment',
|
|
13
|
+
},
|
|
14
|
+
userId,
|
|
15
|
+
),
|
|
16
|
+
).toBe('/configured/markers');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns the directory the tdpm environment variable supplies when the configuration has none', () => {
|
|
20
|
+
expect(
|
|
21
|
+
ownerReplyMarkerDirectoryResolve(
|
|
22
|
+
null,
|
|
23
|
+
{
|
|
24
|
+
TDPM_SILENT_OWNER_REPLY_MARKER_DIRECTORY: '/from/tdpm/environment',
|
|
25
|
+
CLAUDE_STATUSLINE_CALL_DIR: '/from/status/line/environment',
|
|
26
|
+
},
|
|
27
|
+
userId,
|
|
28
|
+
),
|
|
29
|
+
).toBe('/from/tdpm/environment');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns the directory the status line override supplies, so both sides move together', () => {
|
|
33
|
+
expect(
|
|
34
|
+
ownerReplyMarkerDirectoryResolve(
|
|
35
|
+
null,
|
|
36
|
+
{ CLAUDE_STATUSLINE_CALL_DIR: '/from/status/line/environment' },
|
|
37
|
+
userId,
|
|
38
|
+
),
|
|
39
|
+
).toBe('/from/status/line/environment');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('resolves the directory the status line writes to and ignores a redirected temporary directory', () => {
|
|
43
|
+
expect(
|
|
44
|
+
ownerReplyMarkerDirectoryResolve(
|
|
45
|
+
null,
|
|
46
|
+
{ TMPDIR: '/mnt/storage/tmp' },
|
|
47
|
+
userId,
|
|
48
|
+
),
|
|
49
|
+
).toBe('/tmp/claude-statusline-call-1000');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns null when the user id is unknown, because the directory name carries it', () => {
|
|
53
|
+
expect(ownerReplyMarkerDirectoryResolve(null, {}, null)).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
|
|
3
|
+
// The status line writes each session's rendered reply time to
|
|
4
|
+
// `<STATUS_LINE_MARKER_ROOT_DIRECTORY>/claude-statusline-call-<uid>/<sessionId>.reply_ts`, and it
|
|
5
|
+
// resolves that root without consulting TMPDIR. The silent-session monitor runs from a daemon whose
|
|
6
|
+
// TMPDIR points at a separate volume, so resolving the same directory through the operating
|
|
7
|
+
// system temporary directory would send the reader to a path the writer never uses and the marker
|
|
8
|
+
// would silently never be found. CLAUDE_STATUSLINE_CALL_DIR is the single override both sides honour.
|
|
9
|
+
const STATUS_LINE_MARKER_ROOT_DIRECTORY = '/tmp';
|
|
10
|
+
const STATUS_LINE_MARKER_DIRECTORY_NAME_PREFIX = 'claude-statusline-call-';
|
|
11
|
+
|
|
12
|
+
export const ownerReplyMarkerDirectoryResolve = (
|
|
13
|
+
configuredDirectory: string | null,
|
|
14
|
+
environment: NodeJS.ProcessEnv,
|
|
15
|
+
userId: number | null,
|
|
16
|
+
): string | null => {
|
|
17
|
+
if (configuredDirectory !== null) {
|
|
18
|
+
return configuredDirectory;
|
|
19
|
+
}
|
|
20
|
+
const tdpmEnvironmentDirectory =
|
|
21
|
+
environment.TDPM_SILENT_OWNER_REPLY_MARKER_DIRECTORY;
|
|
22
|
+
if (tdpmEnvironmentDirectory !== undefined) {
|
|
23
|
+
return tdpmEnvironmentDirectory;
|
|
24
|
+
}
|
|
25
|
+
const statusLineEnvironmentDirectory = environment.CLAUDE_STATUSLINE_CALL_DIR;
|
|
26
|
+
if (statusLineEnvironmentDirectory !== undefined) {
|
|
27
|
+
return statusLineEnvironmentDirectory;
|
|
28
|
+
}
|
|
29
|
+
if (userId === null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return path.join(
|
|
33
|
+
STATUS_LINE_MARKER_ROOT_DIRECTORY,
|
|
34
|
+
`${STATUS_LINE_MARKER_DIRECTORY_NAME_PREFIX}${userId}`,
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { TranscriptOwnerCallStatusProvider } from './TranscriptOwnerCallStatusProvider';
|
|
5
|
+
import { SILENT_SESSION_REMINDER_SENTINEL } from '../../domain/usecases/silentSessionReminderSentinel';
|
|
6
|
+
|
|
7
|
+
describe('TranscriptOwnerCallStatusProvider owner reply typed mid-turn', () => {
|
|
8
|
+
let rootDirectory: string;
|
|
9
|
+
let markerDirectory: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
rootDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'queued-reply-'));
|
|
13
|
+
markerDirectory = path.join(rootDirectory, 'markers');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(rootDirectory, { force: true, recursive: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const sessionId = 'e7a58bf9-4d13-4e27-b446-52c3b017ad79';
|
|
21
|
+
const callTimestamp = '2026-08-10T11:04:26.331Z';
|
|
22
|
+
const earlierReplyTimestamp = '2026-08-10T11:02:40.286Z';
|
|
23
|
+
const laterReplyTimestamp = '2026-08-10T11:05:53.576Z';
|
|
24
|
+
|
|
25
|
+
const writeTranscript = (lines: object[]): string => {
|
|
26
|
+
const filePath = path.join(rootDirectory, `${sessionId}.jsonl`);
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
filePath,
|
|
29
|
+
lines.map((line) => JSON.stringify(line)).join('\n'),
|
|
30
|
+
'utf8',
|
|
31
|
+
);
|
|
32
|
+
return filePath;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const writeReplyMarker = (timestamp: string): void => {
|
|
36
|
+
fs.mkdirSync(markerDirectory, { recursive: true });
|
|
37
|
+
fs.writeFileSync(
|
|
38
|
+
path.join(markerDirectory, `${sessionId}.reply_ts`),
|
|
39
|
+
`${timestamp}\n`,
|
|
40
|
+
'utf8',
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ownerCall = (timestamp: string): object => ({
|
|
45
|
+
type: 'assistant',
|
|
46
|
+
timestamp,
|
|
47
|
+
message: {
|
|
48
|
+
role: 'assistant',
|
|
49
|
+
content: [{ type: 'text', text: 'Please decide <<OWNER_CALL>>' }],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const typedReply = (timestamp: string, text: string): object => ({
|
|
54
|
+
type: 'user',
|
|
55
|
+
timestamp,
|
|
56
|
+
promptSource: 'typed',
|
|
57
|
+
origin: { kind: 'human' },
|
|
58
|
+
message: { role: 'user', content: text },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const enqueued = (timestamp: string, content: string): object => ({
|
|
62
|
+
type: 'queue-operation',
|
|
63
|
+
operation: 'enqueue',
|
|
64
|
+
timestamp,
|
|
65
|
+
sessionId,
|
|
66
|
+
content,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const removed = (timestamp: string, content: string): object => ({
|
|
70
|
+
type: 'queue-operation',
|
|
71
|
+
operation: 'remove',
|
|
72
|
+
timestamp,
|
|
73
|
+
sessionId,
|
|
74
|
+
content,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const unansweredSecondsOf = async (
|
|
78
|
+
transcriptPath: string,
|
|
79
|
+
replyMarkerDirectory: string | null,
|
|
80
|
+
): Promise<number | undefined> => {
|
|
81
|
+
const provider = new TranscriptOwnerCallStatusProvider(
|
|
82
|
+
'<<OWNER_CALL>>',
|
|
83
|
+
replyMarkerDirectory,
|
|
84
|
+
);
|
|
85
|
+
const result =
|
|
86
|
+
await provider.listUnansweredOwnerCallEpochSecondsBySessionName(
|
|
87
|
+
new Map([['session', transcriptPath]]),
|
|
88
|
+
);
|
|
89
|
+
return result.get('session');
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
it('treats a reply enqueued while the agent was working as an answer to the call', async () => {
|
|
93
|
+
const transcriptPath = writeTranscript([
|
|
94
|
+
typedReply(earlierReplyTimestamp, 'an earlier answer'),
|
|
95
|
+
ownerCall(callTimestamp),
|
|
96
|
+
enqueued(laterReplyTimestamp, 'CI が通っていないのに許可はできません'),
|
|
97
|
+
removed(
|
|
98
|
+
'2026-08-10T11:06:07.885Z',
|
|
99
|
+
'CI が通っていないのに許可はできません',
|
|
100
|
+
),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
expect(await unansweredSecondsOf(transcriptPath, null)).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('keeps the call unanswered when the only later enqueue is a task notification', async () => {
|
|
107
|
+
const transcriptPath = writeTranscript([
|
|
108
|
+
typedReply(earlierReplyTimestamp, 'an earlier answer'),
|
|
109
|
+
ownerCall(callTimestamp),
|
|
110
|
+
enqueued(
|
|
111
|
+
laterReplyTimestamp,
|
|
112
|
+
'<task-notification><task-id>abc</task-id></task-notification>',
|
|
113
|
+
),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
expect(await unansweredSecondsOf(transcriptPath, null)).toBe(
|
|
117
|
+
Math.floor(Date.parse(callTimestamp) / 1000),
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('keeps the call unanswered when the only later enqueue is an injected reminder', async () => {
|
|
122
|
+
const transcriptPath = writeTranscript([
|
|
123
|
+
typedReply(earlierReplyTimestamp, 'an earlier answer'),
|
|
124
|
+
ownerCall(callTimestamp),
|
|
125
|
+
enqueued(
|
|
126
|
+
laterReplyTimestamp,
|
|
127
|
+
`${SILENT_SESSION_REMINDER_SENTINEL} check yourself`,
|
|
128
|
+
),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
expect(await unansweredSecondsOf(transcriptPath, null)).toBe(
|
|
132
|
+
Math.floor(Date.parse(callTimestamp) / 1000),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('treats the reply time recorded by the status line marker as an answer to the call', async () => {
|
|
137
|
+
const transcriptPath = writeTranscript([
|
|
138
|
+
typedReply(earlierReplyTimestamp, 'an earlier answer'),
|
|
139
|
+
ownerCall(callTimestamp),
|
|
140
|
+
]);
|
|
141
|
+
writeReplyMarker(laterReplyTimestamp);
|
|
142
|
+
|
|
143
|
+
expect(
|
|
144
|
+
await unansweredSecondsOf(transcriptPath, markerDirectory),
|
|
145
|
+
).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('keeps the call unanswered when the status line marker predates the call', async () => {
|
|
149
|
+
const transcriptPath = writeTranscript([
|
|
150
|
+
typedReply(earlierReplyTimestamp, 'an earlier answer'),
|
|
151
|
+
ownerCall(callTimestamp),
|
|
152
|
+
]);
|
|
153
|
+
writeReplyMarker(earlierReplyTimestamp);
|
|
154
|
+
|
|
155
|
+
expect(await unansweredSecondsOf(transcriptPath, markerDirectory)).toBe(
|
|
156
|
+
Math.floor(Date.parse(callTimestamp) / 1000),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('keeps the call unanswered when no marker file exists for the session', async () => {
|
|
161
|
+
const transcriptPath = writeTranscript([
|
|
162
|
+
typedReply(earlierReplyTimestamp, 'an earlier answer'),
|
|
163
|
+
ownerCall(callTimestamp),
|
|
164
|
+
]);
|
|
165
|
+
fs.mkdirSync(markerDirectory, { recursive: true });
|
|
166
|
+
|
|
167
|
+
expect(await unansweredSecondsOf(transcriptPath, markerDirectory)).toBe(
|
|
168
|
+
Math.floor(Date.parse(callTimestamp) / 1000),
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
2
3
|
import { OwnerCallStatusProvider } from '../../domain/usecases/adapter-interfaces/OwnerCallStatusProvider';
|
|
3
4
|
import { SILENT_SESSION_REMINDER_SENTINEL } from '../../domain/usecases/silentSessionReminderSentinel';
|
|
4
5
|
|
|
@@ -100,10 +101,49 @@ export const ownerCallMarkerFamilyResolve = (marker: string): string[] =>
|
|
|
100
101
|
]
|
|
101
102
|
: [marker];
|
|
102
103
|
|
|
104
|
+
// A reply the owner types while the agent is still working is NOT written as a `user` entry: the
|
|
105
|
+
// running turn consumes it from the queue, and the transcript keeps only
|
|
106
|
+
// {"type":"queue-operation","operation":"enqueue","content":"<the text>"} plus its `remove` twin.
|
|
107
|
+
// Reading `user` entries alone therefore misses every mid-turn reply, leaves the call outstanding
|
|
108
|
+
// for the rest of the session, and suppresses the stall reminder of a session the owner has
|
|
109
|
+
// already answered. The exclusions match the ones the status line applies to the same entries, so
|
|
110
|
+
// a system-injected enqueue never counts as an owner reply.
|
|
111
|
+
const INJECTED_ENQUEUE_CONTENT_MARKERS = [
|
|
112
|
+
SILENT_SESSION_REMINDER_SENTINEL,
|
|
113
|
+
'<system-reminder>',
|
|
114
|
+
'UserPromptSubmit hook',
|
|
115
|
+
'<task-notification>',
|
|
116
|
+
'SYSTEM NOTIFICATION',
|
|
117
|
+
'<local-command-stdout>',
|
|
118
|
+
'<local-command-caveat>',
|
|
119
|
+
'<command-name>',
|
|
120
|
+
'This session is being continued from a previous conversation',
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const isOwnerEnqueuedReply = (parsed: Record<string, unknown>): boolean => {
|
|
124
|
+
if (readString(parsed, 'operation') !== 'enqueue') {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const content = readString(parsed, 'content');
|
|
128
|
+
if (content === null || content.length === 0) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return !INJECTED_ENQUEUE_CONTENT_MARKERS.some((injectedMarker) =>
|
|
132
|
+
content.includes(injectedMarker),
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const TRANSCRIPT_FILE_EXTENSION = '.jsonl';
|
|
137
|
+
const OWNER_REPLY_MARKER_FILE_EXTENSION = '.reply_ts';
|
|
138
|
+
const UNSAFE_SESSION_ID_CHARACTER_PATTERN = /[^A-Za-z0-9._-]/g;
|
|
139
|
+
|
|
103
140
|
export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvider {
|
|
104
141
|
private readonly ownerCallMarkerFamily: string[];
|
|
105
142
|
|
|
106
|
-
constructor(
|
|
143
|
+
constructor(
|
|
144
|
+
ownerCallMarker: string | null,
|
|
145
|
+
private readonly ownerReplyMarkerDirectory: string | null = null,
|
|
146
|
+
) {
|
|
107
147
|
this.ownerCallMarkerFamily =
|
|
108
148
|
ownerCallMarker === null || ownerCallMarker.length === 0
|
|
109
149
|
? []
|
|
@@ -181,13 +221,60 @@ export class TranscriptOwnerCallStatusProvider implements OwnerCallStatusProvide
|
|
|
181
221
|
) {
|
|
182
222
|
lastOwnerReplyEpochMs = epochMs;
|
|
183
223
|
}
|
|
224
|
+
if (type === 'queue-operation' && isOwnerEnqueuedReply(parsed)) {
|
|
225
|
+
lastOwnerReplyEpochMs =
|
|
226
|
+
lastOwnerReplyEpochMs === null || epochMs > lastOwnerReplyEpochMs
|
|
227
|
+
? epochMs
|
|
228
|
+
: lastOwnerReplyEpochMs;
|
|
229
|
+
}
|
|
184
230
|
}
|
|
185
231
|
if (lastOwnerCallEpochMs === null) {
|
|
186
232
|
return null;
|
|
187
233
|
}
|
|
188
|
-
|
|
189
|
-
|
|
234
|
+
const markerReplyEpochMs = this.readOwnerReplyMarkerEpochMs(transcriptPath);
|
|
235
|
+
const resolvedReplyEpochMs =
|
|
236
|
+
markerReplyEpochMs !== null &&
|
|
237
|
+
(lastOwnerReplyEpochMs === null ||
|
|
238
|
+
markerReplyEpochMs > lastOwnerReplyEpochMs)
|
|
239
|
+
? markerReplyEpochMs
|
|
240
|
+
: lastOwnerReplyEpochMs;
|
|
241
|
+
return resolvedReplyEpochMs === null ||
|
|
242
|
+
lastOwnerCallEpochMs > resolvedReplyEpochMs
|
|
190
243
|
? lastOwnerCallEpochMs
|
|
191
244
|
: null;
|
|
192
245
|
};
|
|
246
|
+
|
|
247
|
+
// The owner sees only the status line, so the reply time it renders is the value the owner
|
|
248
|
+
// believes the fleet is acting on. The status line writes that value to a per-session marker
|
|
249
|
+
// file, and reading it here keeps this decision and the owner's own view of it from diverging.
|
|
250
|
+
// The transcript-derived value still stands on its own: an absent, unreadable, or older marker
|
|
251
|
+
// changes nothing, so a fresh host with no markers yet behaves exactly as before.
|
|
252
|
+
private readOwnerReplyMarkerEpochMs = (
|
|
253
|
+
transcriptPath: string,
|
|
254
|
+
): number | null => {
|
|
255
|
+
if (this.ownerReplyMarkerDirectory === null) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const fileName = path.basename(transcriptPath);
|
|
259
|
+
const sessionId = fileName.endsWith(TRANSCRIPT_FILE_EXTENSION)
|
|
260
|
+
? fileName.slice(0, -TRANSCRIPT_FILE_EXTENSION.length)
|
|
261
|
+
: fileName;
|
|
262
|
+
const safeSessionId = sessionId.replace(
|
|
263
|
+
UNSAFE_SESSION_ID_CHARACTER_PATTERN,
|
|
264
|
+
'_',
|
|
265
|
+
);
|
|
266
|
+
let markerContent: string;
|
|
267
|
+
try {
|
|
268
|
+
markerContent = fs.readFileSync(
|
|
269
|
+
path.join(
|
|
270
|
+
this.ownerReplyMarkerDirectory,
|
|
271
|
+
`${safeSessionId}${OWNER_REPLY_MARKER_FILE_EXTENSION}`,
|
|
272
|
+
),
|
|
273
|
+
'utf8',
|
|
274
|
+
);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
return parseEpochMilliseconds(markerContent.split('\n')[0].trim());
|
|
279
|
+
};
|
|
193
280
|
}
|
|
@@ -186,6 +186,78 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
186
186
|
expect(sentQuery).not.toContain('assignees(first: 20)');
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
+
it('requests body field in the Issue content fragment of the GraphQL query', async () => {
|
|
190
|
+
const repository = new GraphqlProjectItemRepository(
|
|
191
|
+
new LocalStorageRepository(),
|
|
192
|
+
'dummy-token',
|
|
193
|
+
);
|
|
194
|
+
mockPost.mockReturnValueOnce(makePageResponse(false, 'cursor-1', 1));
|
|
195
|
+
|
|
196
|
+
await repository.fetchProjectItems('test-project-id');
|
|
197
|
+
|
|
198
|
+
const sentQuery = extractRequestedQueryFromMockCall(
|
|
199
|
+
mockPost.mock.calls[0],
|
|
200
|
+
);
|
|
201
|
+
expect(sentQuery).toContain('body');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('maps body from Issue content node to ProjectItem.body', async () => {
|
|
205
|
+
const repository = new GraphqlProjectItemRepository(
|
|
206
|
+
new LocalStorageRepository(),
|
|
207
|
+
'dummy-token',
|
|
208
|
+
);
|
|
209
|
+
mockPost.mockReturnValueOnce(makePageResponse(false, 'cursor-1', 1));
|
|
210
|
+
|
|
211
|
+
const result = await repository.fetchProjectItems('test-project-id');
|
|
212
|
+
|
|
213
|
+
expect(result).toHaveLength(1);
|
|
214
|
+
expect(result[0].body).toBe('body');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('sets ProjectItem.body to null when content node has no body', async () => {
|
|
218
|
+
const repository = new GraphqlProjectItemRepository(
|
|
219
|
+
new LocalStorageRepository(),
|
|
220
|
+
'dummy-token',
|
|
221
|
+
);
|
|
222
|
+
mockPost.mockReturnValueOnce(
|
|
223
|
+
mockJsonResponse({
|
|
224
|
+
data: {
|
|
225
|
+
node: {
|
|
226
|
+
items: {
|
|
227
|
+
totalCount: 1,
|
|
228
|
+
pageInfo: {
|
|
229
|
+
endCursor: 'cursor-1',
|
|
230
|
+
startCursor: 'cursor-start',
|
|
231
|
+
hasNextPage: false,
|
|
232
|
+
},
|
|
233
|
+
nodes: [
|
|
234
|
+
{
|
|
235
|
+
id: 'item-1',
|
|
236
|
+
fieldValues: { nodes: [] },
|
|
237
|
+
content: {
|
|
238
|
+
repository: { nameWithOwner: 'owner/repo' },
|
|
239
|
+
number: 1,
|
|
240
|
+
title: 'No Body Issue',
|
|
241
|
+
state: 'OPEN',
|
|
242
|
+
url: 'https://github.com/owner/repo/issues/1',
|
|
243
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
244
|
+
labels: { nodes: [] },
|
|
245
|
+
assignees: { nodes: [] },
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const result = await repository.fetchProjectItems('test-project-id');
|
|
256
|
+
|
|
257
|
+
expect(result).toHaveLength(1);
|
|
258
|
+
expect(result[0].body).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
189
261
|
it('logs the rateLimit cost of the request in one line', async () => {
|
|
190
262
|
const repository = new GraphqlProjectItemRepository(
|
|
191
263
|
new LocalStorageRepository(),
|
|
@@ -43,6 +43,7 @@ type ProjectV2ItemContentNode = {
|
|
|
43
43
|
title: string;
|
|
44
44
|
state: string;
|
|
45
45
|
url: string;
|
|
46
|
+
body?: string | null;
|
|
46
47
|
createdAt: string;
|
|
47
48
|
updatedAt: string;
|
|
48
49
|
author: { login: string } | null;
|
|
@@ -120,6 +121,7 @@ const PROJECT_V2_ITEM_FIELD_VALUES_AND_CONTENT_SELECTION = `
|
|
|
120
121
|
title
|
|
121
122
|
state
|
|
122
123
|
url
|
|
124
|
+
body
|
|
123
125
|
createdAt
|
|
124
126
|
updatedAt
|
|
125
127
|
author {
|
|
@@ -396,6 +398,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
|
|
|
396
398
|
title: string;
|
|
397
399
|
state: string;
|
|
398
400
|
url: string;
|
|
401
|
+
body?: string | null;
|
|
399
402
|
createdAt: string;
|
|
400
403
|
updatedAt: string;
|
|
401
404
|
author: { login: string } | null;
|
|
@@ -445,6 +448,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
|
|
|
445
448
|
title: string;
|
|
446
449
|
state: string;
|
|
447
450
|
url: string;
|
|
451
|
+
body?: string | null;
|
|
448
452
|
createdAt: string;
|
|
449
453
|
updatedAt: string;
|
|
450
454
|
author: { login: string } | null;
|
|
@@ -507,6 +511,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
|
|
|
507
511
|
title: string;
|
|
508
512
|
state: string;
|
|
509
513
|
url: string;
|
|
514
|
+
body?: string | null;
|
|
510
515
|
createdAt: string;
|
|
511
516
|
updatedAt: string;
|
|
512
517
|
author: { login: string } | null;
|
|
@@ -612,7 +617,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
|
|
|
612
617
|
title: item.content.title,
|
|
613
618
|
state: this.convertStrToState(item.content.state),
|
|
614
619
|
url: item.content.url,
|
|
615
|
-
body: null,
|
|
620
|
+
body: item.content.body ?? null,
|
|
616
621
|
labels: item.content.labels?.nodes?.map((l) => l.name) || [],
|
|
617
622
|
assignees: item.content.assignees?.nodes?.map((a) => a.login) || [],
|
|
618
623
|
createdAt: item.content.createdAt || new Date().toISOString(),
|