github-issue-tower-defence-management 1.116.7 → 1.116.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/bin/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.js +19 -12
- package/bin/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +10 -2
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +30 -2
- package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.test.ts +240 -0
- package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts +29 -15
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +194 -33
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +19 -1
- package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts +1 -0
- package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +1 -0
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.116.9](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.116.8...v1.116.9) (2026-06-29)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **silent-notification:** monitor only github.com issue or pull-request named sessions ([#1094](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1094)) ([e7f0fca](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/e7f0fca786d93a3cb9dc1899ee0c40bd9f371a2a)), closes [#1093](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1093)
|
|
7
|
+
|
|
8
|
+
## [1.116.8](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.116.7...v1.116.8) (2026-06-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **silent-notification:** resolve the parent transcript by candidate-id priority ([#1092](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1092)) ([d3fcefb](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/d3fcefb2ba761e7ae4cf1c1688c970cda5de590d)), closes [#1090](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1090)
|
|
14
|
+
|
|
1
15
|
## [1.116.7](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.116.6...v1.116.7) (2026-06-29)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -555,7 +555,7 @@ For each `In Tmux by human` issue with no live session, the reconciler launches
|
|
|
555
555
|
|
|
556
556
|
Each schedule cycle, when `silentNotificationEnabled` is true, also inspects every live interactive Claude Code session and sends a short self-check reminder into a session via `tmux send-keys` when either of two independent conditions holds. The reminder is composed of only the sections whose condition is met, so a session receives one combined message containing one or both sections. When `silentNotificationEnabled` is not set, the entire step is a no-op even if the daemon picks up its configuration automatically.
|
|
557
557
|
|
|
558
|
-
|
|
558
|
+
Candidate sessions are first selected from the live process tree: every live tmux session is taken, the descendants of each session's pane processes are walked, and the first descendant that is an interactive Claude Code process (its command line contains `claude` and does not contain the owner-handover marker `Take ownership of`, and its environment exposes both `CLAUDE_CODE_SESSION_ID` and `CLAUDE_CONFIG_DIR`) is taken as that session's process. Because the walk is anchored on tmux panes, background owner-handover spawns, the preparation daemon, and monitor processes — none of which run inside an interactive tmux pane — are excluded; the owner-handover marker check is an additional guard. The monitor then acts only on sessions whose tmux name encodes a `github.com` issue or pull-request URL — that is, a name of the form `https_//github_com/{owner}/{repo}/issues/{N}` or `https_//github_com/{owner}/{repo}/pull/{N}` (the form tmux derives from the raw issue/PR URL by replacing only `.` and `:` with `_`; the raw `https://github.com/...` form is matched as well). Sessions whose name does not encode such a URL — for example an orchestrator session named with a plain word such as `workbench` — are left untouched: they receive neither a main-stall nor a sub-process notification, and the count of non-github-named sessions ignored each cycle is logged. Each selected session's transcript is then resolved by scanning the `projects/<cwd-slug>/` subdirectories of two roots — the per-session `<CLAUDE_CONFIG_DIR>/projects/` and the shared `~/.claude/projects/` — for a `<sessionId>.jsonl` file named by any of the session's candidate session ids. The candidate ids are derived from the process tree, in priority order: the current session id recorded in `<CLAUDE_CONFIG_DIR>/sessions/<pid>.json` (the session id rotates on resume or compaction, and this record holds the current value), then the interactive process's own launch id from the `CLAUDE_CODE_SESSION_ID` environment variable, then the distinct ids propagated to its descendant processes, all deduped. Searching the shared root and the rotated current id is required because, for a resumed or compacted session, the actively-written transcript is named by the current id and lives under the shared root rather than under the per-session config directory; the descendant-propagated ids are required because, for a session started without `--resume`, the own launch id names no transcript on disk and the live transcript is named by the descendant id. The candidate ids are tried strictly in this priority order, and the first candidate id that names an existing `<sessionId>.jsonl` file in either root is selected; the most recently modified match is used only as a tiebreak when that same id exists in both roots. Resolving by candidate-id priority rather than by the globally most recently modified file across all candidate ids is required because a session running a sub-agent has the sub-agent's own transcript on disk too, and that sub-agent transcript can be more recently modified than the parent session's transcript while not naming the parent. The parent session's own ids (the rotated current id and the launch id) come before any descendant-propagated id, so the parent's own transcript is selected ahead of a sub-agent transcript; a sub-agent transcript, which lives under `projects/<cwd-slug>/<parentSessionId>/subagents/agent-<id>.jsonl` rather than as a top-level `<sessionId>.jsonl` file, is never reached. This way a non-resume session and a session that has no issue URL both resolve just as reliably as a resume, issue-url-named one, and a session that is waiting on the owner while a sub-agent runs is evaluated against its own transcript rather than the sub-agent's.
|
|
559
559
|
|
|
560
560
|
The main-session stalled section is sent when the session's main output has been idle for at least `mainSilentThresholdSeconds`, unless the session is currently waiting on the owner (in that case the silence is expected and the section is suppressed). Idle time is computed from the timestamp of the latest entry of any kind in the session's resolved transcript rather than from the transcript file modification time, so a session that keeps appending tool results, owner replies, or any other entry type while emitting no assistant text still counts as active and is not mistaken for a silent one. Whether a session is waiting on the owner is determined through an injectable port; the built-in default reports no session as waiting, so the suppression is only applied when a host provides an implementation. When `ownerCallMarker` is configured, a transcript-based implementation is used: it reads each session's resolved transcript and treats the session as waiting on the owner when the latest entry containing `ownerCallMarker` is newer (by full timestamp) than the latest genuine owner reply. A self-check reminder that the monitor injects into a session via `tmux send-keys` itself lands in that session's transcript as a `user` text entry; every injected reminder carries a fixed sentinel tag (`[TDPM_SILENT_SESSION_SELF_CHECK_REMINDER]`), and a `user` entry whose text contains that sentinel is not counted as a genuine owner reply. This prevents the monitor's own reminder from being mistaken for the owner answering, which would otherwise make a genuinely waiting session stop being suppressed. The stalled section's owner-call self-check point also instructs the agent on the required owner-call notification format: the owner-call must be emitted as the configured owner-call marker tag (interpolated from `ownerCallMarker` when set) as a complete matching pair — opening marker, content, then closing marker — on a single line with no newline inside the tag, and the content between the markers must begin with the 🔴 emoji immediately, because the owner's app only surfaces the notification together with its content when the exact, well-formed tag with the leading 🔴 is present and a malformed tag results in only a red indicator with no readable content. The same self-check point also tells the agent when to fire the owner-call: if the agent has completed or answered a request from the owner in this session, it must fire the owner-call to report the result to the owner, because completing or answering an owner's requested action is itself a reason to fire the owner-call. Completing or answering an owner request without firing the owner-call means the owner is never notified — the owner's app only surfaces the session when the owner-call fires — so the task silently stalls; the point states the causal link explicitly, noting that if the self-check reminder keeps arriving it is likely because an owner request was completed or answered without firing the owner-call, so the agent should fire the owner-call to report the result to the owner.
|
|
561
561
|
|
|
@@ -62,20 +62,27 @@ class FileSystemInteractiveLiveSessionTranscriptResolver {
|
|
|
62
62
|
};
|
|
63
63
|
this.resolveTranscriptPath = (session) => {
|
|
64
64
|
const projectsDirectories = this.listProjectsDirectories(session.configDir);
|
|
65
|
+
for (const candidateSessionId of session.candidateSessionIds) {
|
|
66
|
+
const transcriptPath = this.resolveCandidateTranscriptPath(candidateSessionId, projectsDirectories);
|
|
67
|
+
if (transcriptPath !== null) {
|
|
68
|
+
return transcriptPath;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
this.resolveCandidateTranscriptPath = (candidateSessionId, projectsDirectories) => {
|
|
74
|
+
const fileName = `${candidateSessionId}.jsonl`;
|
|
65
75
|
let latestPath = null;
|
|
66
76
|
let latestEpochMs = -Infinity;
|
|
67
|
-
for (const
|
|
68
|
-
const fileName
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
latestEpochMs = epochMs;
|
|
77
|
-
latestPath = candidate;
|
|
78
|
-
}
|
|
77
|
+
for (const projectsDirectory of projectsDirectories) {
|
|
78
|
+
for (const candidate of this.listCandidatePaths(projectsDirectory, fileName)) {
|
|
79
|
+
const epochMs = modifiedEpochMs(candidate);
|
|
80
|
+
if (epochMs === null) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (epochMs > latestEpochMs) {
|
|
84
|
+
latestEpochMs = epochMs;
|
|
85
|
+
latestPath = candidate;
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileSystemInteractiveLiveSessionTranscriptResolver.js","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAyB;AACzB,uCAAyB;AACzB,2CAA6B;AAI7B,MAAM,eAAe,GAAG,CAAC,QAAgB,EAAiB,EAAE;IAC1D,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,8BAA8B,GAAG,GAAW,EAAE,CAClD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;AAEjD,MAAa,kDAAkD;IAC7D,YACmB,0BAAkC,8BAA8B,EAAE;QAAlE,4BAAuB,GAAvB,uBAAuB,CAA2C;QAGrF,2BAAsB,GAAG,CACvB,QAAkC,EACb,EAAE;YACvB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;YAC3C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;gBAC/B,MAAM,cAAc,GAAG,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC;gBAC3D,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;oBAC5B,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;gBACpD,CAAC;YACH,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC;QAEM,0BAAqB,GAAG,CAC9B,OAA+B,EAChB,EAAE;YACjB,MAAM,mBAAmB,GAAG,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAC5E,
|
|
1
|
+
{"version":3,"file":"FileSystemInteractiveLiveSessionTranscriptResolver.js","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NotifySilentLiveSessionsUseCase = exports.parseHubTaskIssueUrlFromSessionName = exports.DEFAULT_NOTIFICATION_STAGGER_SECONDS = exports.DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS = exports.DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS = exports.DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS = void 0;
|
|
3
|
+
exports.NotifySilentLiveSessionsUseCase = exports.isGitHubIssueOrPullRequestSessionName = exports.parseHubTaskIssueUrlFromSessionName = exports.DEFAULT_NOTIFICATION_STAGGER_SECONDS = exports.DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS = exports.DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS = exports.DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS = void 0;
|
|
4
4
|
const ResolveInteractiveLiveSessionsUseCase_1 = require("./ResolveInteractiveLiveSessionsUseCase");
|
|
5
5
|
exports.DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS = 10 * 60;
|
|
6
6
|
exports.DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS = 5 * 60;
|
|
@@ -11,6 +11,9 @@ const parseHubTaskIssueUrlFromSessionName = (sessionName) => {
|
|
|
11
11
|
return GITHUB_ISSUE_URL_PATTERN.test(sessionName) ? sessionName : null;
|
|
12
12
|
};
|
|
13
13
|
exports.parseHubTaskIssueUrlFromSessionName = parseHubTaskIssueUrlFromSessionName;
|
|
14
|
+
const GITHUB_ISSUE_OR_PULL_REQUEST_SESSION_NAME_PATTERN = /^https(:\/\/|_\/\/)github(\.com|_com)\/[^/]+\/[^/]+\/(issues|pull)\/\d+$/;
|
|
15
|
+
const isGitHubIssueOrPullRequestSessionName = (sessionName) => GITHUB_ISSUE_OR_PULL_REQUEST_SESSION_NAME_PATTERN.test(sessionName);
|
|
16
|
+
exports.isGitHubIssueOrPullRequestSessionName = isGitHubIssueOrPullRequestSessionName;
|
|
14
17
|
class NotifySilentLiveSessionsUseCase {
|
|
15
18
|
constructor(liveSessionProcessSnapshotProvider, interactiveLiveSessionTranscriptResolver, sessionOutputActivityRepository, subAgentActivityRepository, ownerCallStatusProvider, notificationRepository, messageComposer, sleeper, hubTaskStatusResolver = null) {
|
|
16
19
|
this.liveSessionProcessSnapshotProvider = liveSessionProcessSnapshotProvider;
|
|
@@ -25,7 +28,12 @@ class NotifySilentLiveSessionsUseCase {
|
|
|
25
28
|
this.resolveInteractiveLiveSessions = new ResolveInteractiveLiveSessionsUseCase_1.ResolveInteractiveLiveSessionsUseCase();
|
|
26
29
|
this.run = async (params) => {
|
|
27
30
|
const snapshot = await this.liveSessionProcessSnapshotProvider.getSnapshot();
|
|
28
|
-
const
|
|
31
|
+
const allInteractiveSessions = this.resolveInteractiveLiveSessions.resolve(snapshot);
|
|
32
|
+
const interactiveSessions = allInteractiveSessions.filter((session) => (0, exports.isGitHubIssueOrPullRequestSessionName)(session.sessionName));
|
|
33
|
+
const skippedNonGitHubSessionCount = allInteractiveSessions.length - interactiveSessions.length;
|
|
34
|
+
if (skippedNonGitHubSessionCount > 0) {
|
|
35
|
+
console.log(`Silent live session notification: ignoring ${skippedNonGitHubSessionCount} non-github-named interactive session(s); only sessions named after a github.com issue or pull-request URL are monitored.`);
|
|
36
|
+
}
|
|
29
37
|
const transcriptPathBySessionName = this.interactiveLiveSessionTranscriptResolver.resolveTranscriptPaths(interactiveSessions);
|
|
30
38
|
const snapshots = await this.collectSnapshots(interactiveSessions, transcriptPathBySessionName, params.now);
|
|
31
39
|
const candidates = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotifySilentLiveSessionsUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/NotifySilentLiveSessionsUseCase.ts"],"names":[],"mappings":";;;AAWA,mGAAgG;AAEnF,QAAA,qCAAqC,GAAG,EAAE,GAAG,EAAE,CAAC;AAChD,QAAA,yCAAyC,GAAG,CAAC,GAAG,EAAE,CAAC;AACnD,QAAA,0CAA0C,GAAG,EAAE,GAAG,EAAE,CAAC;AACrD,QAAA,oCAAoC,GAAG,EAAE,CAAC;AAEvD,MAAM,wBAAwB,GAC5B,oDAAoD,CAAC;AAEhD,MAAM,mCAAmC,GAAG,CACjD,WAAmB,EACJ,EAAE;IACjB,OAAO,wBAAwB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;AACzE,CAAC,CAAC;AAJW,QAAA,mCAAmC,uCAI9C;
|
|
1
|
+
{"version":3,"file":"NotifySilentLiveSessionsUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/NotifySilentLiveSessionsUseCase.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const NOW = new Date('2026-06-26T00:00:00.000Z');
|
|
15
15
|
const NOW_EPOCH_SECONDS = Math.floor(NOW.getTime() / 1000);
|
|
16
|
-
const SESSION_NAME = '
|
|
16
|
+
const SESSION_NAME = 'https_//github_com/HiromiShikata/repo/issues/9';
|
|
17
17
|
const SESSION_ID = 'wb-uuid';
|
|
18
18
|
const PANE_PID = 200;
|
|
19
19
|
const CLAUDE_PID = 201;
|
|
@@ -121,7 +121,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
121
121
|
...DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS,
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
it('sends a main stalled notification to a silent
|
|
124
|
+
it('sends a main stalled notification to a silent github-named live session', async () => {
|
|
125
125
|
silentAssistantTranscript();
|
|
126
126
|
const runner = liveSessionRunner();
|
|
127
127
|
|
|
@@ -134,6 +134,34 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
134
134
|
expect(sendCall?.[1][4]).toContain('You have produced no output for');
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
it('sends no notification to a silent non-github-named live session', async () => {
|
|
138
|
+
silentAssistantTranscript();
|
|
139
|
+
const runner = createMockRunner();
|
|
140
|
+
runner.runCommand.mockImplementation(async (program, args) => {
|
|
141
|
+
if (program === 'tmux' && args[0] === 'list-sessions') {
|
|
142
|
+
return { stdout: 'orchestrator\n', stderr: '', exitCode: 0 };
|
|
143
|
+
}
|
|
144
|
+
if (program === 'tmux' && args[0] === 'list-panes') {
|
|
145
|
+
return { stdout: `${PANE_PID}\n`, stderr: '', exitCode: 0 };
|
|
146
|
+
}
|
|
147
|
+
if (program === 'ps') {
|
|
148
|
+
return {
|
|
149
|
+
stdout: ` ${PANE_PID} 1 shell\n ${CLAUDE_PID} ${PANE_PID} claude --name orchestrator\n`,
|
|
150
|
+
stderr: '',
|
|
151
|
+
exitCode: 0,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await notifySilentTmuxSessions(baseParams(runner));
|
|
158
|
+
|
|
159
|
+
const sendCall = runner.runCommand.mock.calls.find(
|
|
160
|
+
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
161
|
+
);
|
|
162
|
+
expect(sendCall).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
137
165
|
it('does nothing when the step is not enabled', async () => {
|
|
138
166
|
silentAssistantTranscript();
|
|
139
167
|
const runner = liveSessionRunner();
|
|
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { FileSystemInteractiveLiveSessionTranscriptResolver } from './FileSystemInteractiveLiveSessionTranscriptResolver';
|
|
5
|
+
import { TranscriptOwnerCallStatusProvider } from './TranscriptOwnerCallStatusProvider';
|
|
5
6
|
|
|
6
7
|
describe('FileSystemInteractiveLiveSessionTranscriptResolver', () => {
|
|
7
8
|
let configRoot: string;
|
|
@@ -41,6 +42,35 @@ describe('FileSystemInteractiveLiveSessionTranscriptResolver', () => {
|
|
|
41
42
|
return filePath;
|
|
42
43
|
};
|
|
43
44
|
|
|
45
|
+
const writeSubagentTranscript = (params: {
|
|
46
|
+
projectsDirectory: string;
|
|
47
|
+
cwdSlug: string;
|
|
48
|
+
parentSessionId: string;
|
|
49
|
+
agentId: string;
|
|
50
|
+
mtimeEpochSeconds?: number;
|
|
51
|
+
}): string => {
|
|
52
|
+
const subagentDirectory = path.join(
|
|
53
|
+
params.projectsDirectory,
|
|
54
|
+
params.cwdSlug,
|
|
55
|
+
params.parentSessionId,
|
|
56
|
+
'subagents',
|
|
57
|
+
);
|
|
58
|
+
fs.mkdirSync(subagentDirectory, { recursive: true });
|
|
59
|
+
const filePath = path.join(
|
|
60
|
+
subagentDirectory,
|
|
61
|
+
`agent-${params.agentId}.jsonl`,
|
|
62
|
+
);
|
|
63
|
+
fs.writeFileSync(filePath, '{"type":"custom-title"}', 'utf8');
|
|
64
|
+
if (params.mtimeEpochSeconds !== undefined) {
|
|
65
|
+
fs.utimesSync(
|
|
66
|
+
filePath,
|
|
67
|
+
params.mtimeEpochSeconds,
|
|
68
|
+
params.mtimeEpochSeconds,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return filePath;
|
|
72
|
+
};
|
|
73
|
+
|
|
44
74
|
it('resolves a resume-case transcript by config dir and session id', () => {
|
|
45
75
|
const configDir = path.join(configRoot, 'workbench');
|
|
46
76
|
const filePath = writeTranscript({
|
|
@@ -186,6 +216,123 @@ describe('FileSystemInteractiveLiveSessionTranscriptResolver', () => {
|
|
|
186
216
|
expect(result.get('workbench')).toBe(newerPath);
|
|
187
217
|
});
|
|
188
218
|
|
|
219
|
+
it('selects the parent transcript over a newer descendant id transcript', () => {
|
|
220
|
+
const configDir = path.join(configRoot, 'with-subagent');
|
|
221
|
+
const parentPath = writeTranscript({
|
|
222
|
+
projectsDirectory: path.join(configDir, 'projects'),
|
|
223
|
+
cwdSlug: '-home-user',
|
|
224
|
+
sessionId: 'parent-id',
|
|
225
|
+
mtimeEpochSeconds: 1700000000,
|
|
226
|
+
});
|
|
227
|
+
writeTranscript({
|
|
228
|
+
projectsDirectory: path.join(configDir, 'projects'),
|
|
229
|
+
cwdSlug: '-home-user',
|
|
230
|
+
sessionId: 'subagent-id',
|
|
231
|
+
mtimeEpochSeconds: 1700000500,
|
|
232
|
+
});
|
|
233
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver(
|
|
234
|
+
sharedProjectsDirectory,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const result = resolver.resolveTranscriptPaths([
|
|
238
|
+
{
|
|
239
|
+
sessionName: 'with-subagent',
|
|
240
|
+
sessionId: 'parent-id',
|
|
241
|
+
candidateSessionIds: ['parent-id', 'subagent-id'],
|
|
242
|
+
configDir,
|
|
243
|
+
},
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
expect(result.get('with-subagent')).toBe(parentPath);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('selects the rotated parent transcript over a newer descendant id transcript', () => {
|
|
250
|
+
const configDir = path.join(configRoot, 'rotated-with-subagent');
|
|
251
|
+
const rotatedParentPath = writeTranscript({
|
|
252
|
+
projectsDirectory: path.join(configDir, 'projects'),
|
|
253
|
+
cwdSlug: '-home-user',
|
|
254
|
+
sessionId: 'rotated-id',
|
|
255
|
+
mtimeEpochSeconds: 1700000000,
|
|
256
|
+
});
|
|
257
|
+
writeTranscript({
|
|
258
|
+
projectsDirectory: path.join(configDir, 'projects'),
|
|
259
|
+
cwdSlug: '-home-user',
|
|
260
|
+
sessionId: 'subagent-id',
|
|
261
|
+
mtimeEpochSeconds: 1700000500,
|
|
262
|
+
});
|
|
263
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver(
|
|
264
|
+
sharedProjectsDirectory,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const result = resolver.resolveTranscriptPaths([
|
|
268
|
+
{
|
|
269
|
+
sessionName: 'rotated-with-subagent',
|
|
270
|
+
sessionId: 'launch-id',
|
|
271
|
+
candidateSessionIds: ['rotated-id', 'launch-id', 'subagent-id'],
|
|
272
|
+
configDir,
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
expect(result.get('rotated-with-subagent')).toBe(rotatedParentPath);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('selects the parent transcript and never a newer subagent transcript nested under subagents', () => {
|
|
280
|
+
const configDir = path.join(configRoot, 'subagent-nested');
|
|
281
|
+
const parentPath = writeTranscript({
|
|
282
|
+
projectsDirectory: path.join(configDir, 'projects'),
|
|
283
|
+
cwdSlug: '-home-user',
|
|
284
|
+
sessionId: 'parent-id',
|
|
285
|
+
mtimeEpochSeconds: 1700000000,
|
|
286
|
+
});
|
|
287
|
+
const subagentPath = writeSubagentTranscript({
|
|
288
|
+
projectsDirectory: path.join(configDir, 'projects'),
|
|
289
|
+
cwdSlug: '-home-user',
|
|
290
|
+
parentSessionId: 'parent-id',
|
|
291
|
+
agentId: 'worker',
|
|
292
|
+
mtimeEpochSeconds: 1700000500,
|
|
293
|
+
});
|
|
294
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver(
|
|
295
|
+
sharedProjectsDirectory,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const result = resolver.resolveTranscriptPaths([
|
|
299
|
+
{
|
|
300
|
+
sessionName: 'subagent-nested',
|
|
301
|
+
sessionId: 'parent-id',
|
|
302
|
+
candidateSessionIds: ['parent-id', 'subagent-id'],
|
|
303
|
+
configDir,
|
|
304
|
+
},
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
expect(result.get('subagent-nested')).toBe(parentPath);
|
|
308
|
+
expect(result.get('subagent-nested')).not.toBe(subagentPath);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('omits a session whose only transcript is nested under a subagents directory', () => {
|
|
312
|
+
const configDir = path.join(configRoot, 'subagent-only');
|
|
313
|
+
writeSubagentTranscript({
|
|
314
|
+
projectsDirectory: path.join(configDir, 'projects'),
|
|
315
|
+
cwdSlug: '-home-user',
|
|
316
|
+
parentSessionId: 'parent-id',
|
|
317
|
+
agentId: 'worker',
|
|
318
|
+
mtimeEpochSeconds: 1700000500,
|
|
319
|
+
});
|
|
320
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver(
|
|
321
|
+
sharedProjectsDirectory,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const result = resolver.resolveTranscriptPaths([
|
|
325
|
+
{
|
|
326
|
+
sessionName: 'subagent-only',
|
|
327
|
+
sessionId: 'parent-id',
|
|
328
|
+
candidateSessionIds: ['parent-id', 'subagent-id'],
|
|
329
|
+
configDir,
|
|
330
|
+
},
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
expect(result.has('subagent-only')).toBe(false);
|
|
334
|
+
});
|
|
335
|
+
|
|
189
336
|
it('resolves several sessions in one call', () => {
|
|
190
337
|
const workbenchConfig = path.join(configRoot, 'workbench');
|
|
191
338
|
const controlRoomConfig = path.join(configRoot, 'control-room');
|
|
@@ -272,3 +419,96 @@ describe('FileSystemInteractiveLiveSessionTranscriptResolver', () => {
|
|
|
272
419
|
expect(result.size).toBe(0);
|
|
273
420
|
});
|
|
274
421
|
});
|
|
422
|
+
|
|
423
|
+
describe('parent transcript resolution feeding owner-call detection', () => {
|
|
424
|
+
let configRoot: string;
|
|
425
|
+
let sharedProjectsDirectory: string;
|
|
426
|
+
|
|
427
|
+
beforeEach(() => {
|
|
428
|
+
configRoot = fs.mkdtempSync(
|
|
429
|
+
path.join(os.tmpdir(), 'interactive-transcript-ownercall-'),
|
|
430
|
+
);
|
|
431
|
+
sharedProjectsDirectory = path.join(configRoot, 'shared', 'projects');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
afterEach(() => {
|
|
435
|
+
fs.rmSync(configRoot, { force: true, recursive: true });
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const ownerCallMarker = '<<OWNER_CALL>>';
|
|
439
|
+
|
|
440
|
+
const writeJsonlTranscript = (
|
|
441
|
+
filePath: string,
|
|
442
|
+
lines: object[],
|
|
443
|
+
mtimeEpochSeconds: number,
|
|
444
|
+
): void => {
|
|
445
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
446
|
+
fs.writeFileSync(
|
|
447
|
+
filePath,
|
|
448
|
+
lines.map((line) => JSON.stringify(line)).join('\n'),
|
|
449
|
+
'utf8',
|
|
450
|
+
);
|
|
451
|
+
fs.utimesSync(filePath, mtimeEpochSeconds, mtimeEpochSeconds);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
it('identifies a session running a subagent as waiting on the owner', () => {
|
|
455
|
+
const configDir = path.join(configRoot, 'waiting');
|
|
456
|
+
const projectDirectory = path.join(configDir, 'projects', '-home-user');
|
|
457
|
+
writeJsonlTranscript(
|
|
458
|
+
path.join(projectDirectory, 'parent-id.jsonl'),
|
|
459
|
+
[
|
|
460
|
+
{
|
|
461
|
+
type: 'assistant',
|
|
462
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
463
|
+
message: {
|
|
464
|
+
role: 'assistant',
|
|
465
|
+
content: [
|
|
466
|
+
{ type: 'text', text: `${ownerCallMarker} please decide` },
|
|
467
|
+
],
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
1700000000,
|
|
472
|
+
);
|
|
473
|
+
writeJsonlTranscript(
|
|
474
|
+
path.join(
|
|
475
|
+
projectDirectory,
|
|
476
|
+
'parent-id',
|
|
477
|
+
'subagents',
|
|
478
|
+
'agent-worker.jsonl',
|
|
479
|
+
),
|
|
480
|
+
[
|
|
481
|
+
{
|
|
482
|
+
type: 'assistant',
|
|
483
|
+
timestamp: '2026-01-01T00:05:00.000Z',
|
|
484
|
+
message: {
|
|
485
|
+
role: 'assistant',
|
|
486
|
+
content: [{ type: 'text', text: 'subagent progress' }],
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
1700000500,
|
|
491
|
+
);
|
|
492
|
+
const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver(
|
|
493
|
+
sharedProjectsDirectory,
|
|
494
|
+
);
|
|
495
|
+
const ownerCallStatusProvider = new TranscriptOwnerCallStatusProvider(
|
|
496
|
+
ownerCallMarker,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
const transcriptPaths = resolver.resolveTranscriptPaths([
|
|
500
|
+
{
|
|
501
|
+
sessionName: 'waiting',
|
|
502
|
+
sessionId: 'parent-id',
|
|
503
|
+
candidateSessionIds: ['parent-id', 'subagent-id'],
|
|
504
|
+
configDir,
|
|
505
|
+
},
|
|
506
|
+
]);
|
|
507
|
+
|
|
508
|
+
return ownerCallStatusProvider
|
|
509
|
+
.listSessionNamesWithUnansweredOwnerCall(transcriptPaths)
|
|
510
|
+
.then((waiting) => {
|
|
511
|
+
expect(waiting.has('waiting')).toBe(true);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
});
|
|
@@ -38,23 +38,37 @@ export class FileSystemInteractiveLiveSessionTranscriptResolver implements Inter
|
|
|
38
38
|
session: InteractiveLiveSession,
|
|
39
39
|
): string | null => {
|
|
40
40
|
const projectsDirectories = this.listProjectsDirectories(session.configDir);
|
|
41
|
+
for (const candidateSessionId of session.candidateSessionIds) {
|
|
42
|
+
const transcriptPath = this.resolveCandidateTranscriptPath(
|
|
43
|
+
candidateSessionId,
|
|
44
|
+
projectsDirectories,
|
|
45
|
+
);
|
|
46
|
+
if (transcriptPath !== null) {
|
|
47
|
+
return transcriptPath;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
private resolveCandidateTranscriptPath = (
|
|
54
|
+
candidateSessionId: string,
|
|
55
|
+
projectsDirectories: string[],
|
|
56
|
+
): string | null => {
|
|
57
|
+
const fileName = `${candidateSessionId}.jsonl`;
|
|
41
58
|
let latestPath: string | null = null;
|
|
42
59
|
let latestEpochMs = -Infinity;
|
|
43
|
-
for (const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
latestEpochMs = epochMs;
|
|
56
|
-
latestPath = candidate;
|
|
57
|
-
}
|
|
60
|
+
for (const projectsDirectory of projectsDirectories) {
|
|
61
|
+
for (const candidate of this.listCandidatePaths(
|
|
62
|
+
projectsDirectory,
|
|
63
|
+
fileName,
|
|
64
|
+
)) {
|
|
65
|
+
const epochMs = modifiedEpochMs(candidate);
|
|
66
|
+
if (epochMs === null) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (epochMs > latestEpochMs) {
|
|
70
|
+
latestEpochMs = epochMs;
|
|
71
|
+
latestPath = candidate;
|
|
58
72
|
}
|
|
59
73
|
}
|
|
60
74
|
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
NotifySilentLiveSessionsUseCase,
|
|
3
3
|
HubTaskStatusResolver,
|
|
4
4
|
parseHubTaskIssueUrlFromSessionName,
|
|
5
|
+
isGitHubIssueOrPullRequestSessionName,
|
|
5
6
|
DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
6
7
|
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
7
8
|
DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
@@ -38,6 +39,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
38
39
|
let mockHubTaskStatusResolver: Mocked<HubTaskStatusResolver>;
|
|
39
40
|
const now = new Date('2026-06-26T00:00:00Z');
|
|
40
41
|
const nowEpochSeconds = Math.floor(now.getTime() / 1000);
|
|
42
|
+
const GITHUB_SESSION = 'https_//github_com/HiromiShikata/repo/issues/42';
|
|
43
|
+
const GITHUB_SESSION_ALPHA =
|
|
44
|
+
'https_//github_com/HiromiShikata/repo/issues/100';
|
|
45
|
+
const GITHUB_SESSION_BRAVO =
|
|
46
|
+
'https_//github_com/HiromiShikata/repo/issues/200';
|
|
41
47
|
|
|
42
48
|
const runParams = (
|
|
43
49
|
overrides?: Partial<{ activeHubTaskStatus: string | null }>,
|
|
@@ -208,12 +214,12 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
208
214
|
expect(DEFAULT_NOTIFICATION_STAGGER_SECONDS).toBe(25);
|
|
209
215
|
});
|
|
210
216
|
|
|
211
|
-
it('notifies a
|
|
212
|
-
setupLiveInteractiveSession(
|
|
217
|
+
it('notifies a session whose name encodes a github.com issue URL', async () => {
|
|
218
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
213
219
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
214
220
|
[
|
|
215
221
|
{
|
|
216
|
-
sessionName:
|
|
222
|
+
sessionName: GITHUB_SESSION,
|
|
217
223
|
lastOutputEpochSeconds:
|
|
218
224
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
219
225
|
},
|
|
@@ -227,15 +233,100 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
227
233
|
);
|
|
228
234
|
expect(
|
|
229
235
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
230
|
-
).toHaveBeenCalledWith(
|
|
236
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('notifies a session whose name encodes a github.com pull-request URL', async () => {
|
|
240
|
+
const pullRequestSession = 'https_//github_com/HiromiShikata/repo/pull/77';
|
|
241
|
+
setupLiveInteractiveSession(pullRequestSession);
|
|
242
|
+
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
243
|
+
[
|
|
244
|
+
{
|
|
245
|
+
sessionName: pullRequestSession,
|
|
246
|
+
lastOutputEpochSeconds:
|
|
247
|
+
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
await useCase.run(runParams());
|
|
253
|
+
|
|
254
|
+
expect(
|
|
255
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
256
|
+
).toHaveBeenCalledWith(pullRequestSession, MAIN_STALLED_SECTION);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('excludes a non-github-named session from selection so it is never notified', async () => {
|
|
260
|
+
mockSnapshotProvider.getSnapshot.mockResolvedValue(
|
|
261
|
+
snapshotWithSessions(['workbench']),
|
|
262
|
+
);
|
|
263
|
+
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
264
|
+
[
|
|
265
|
+
{
|
|
266
|
+
sessionName: 'workbench',
|
|
267
|
+
lastOutputEpochSeconds:
|
|
268
|
+
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
await useCase.run(runParams());
|
|
274
|
+
|
|
275
|
+
expect(mockTranscriptResolver.resolveTranscriptPaths).toHaveBeenCalledWith(
|
|
276
|
+
[],
|
|
277
|
+
);
|
|
278
|
+
expect(
|
|
279
|
+
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName,
|
|
280
|
+
).toHaveBeenCalledWith([]);
|
|
281
|
+
expect(
|
|
282
|
+
mockMessageComposer.composeMainStalledSection,
|
|
283
|
+
).not.toHaveBeenCalled();
|
|
284
|
+
expect(
|
|
285
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
286
|
+
).not.toHaveBeenCalled();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('monitors only the github-named session when mixed with a non-github-named session', async () => {
|
|
290
|
+
mockSnapshotProvider.getSnapshot.mockResolvedValue(
|
|
291
|
+
snapshotWithSessions([GITHUB_SESSION, 'orchestrator']),
|
|
292
|
+
);
|
|
293
|
+
mockTranscriptResolver.resolveTranscriptPaths.mockReturnValue(
|
|
294
|
+
transcriptMapFor([GITHUB_SESSION]),
|
|
295
|
+
);
|
|
296
|
+
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
297
|
+
[
|
|
298
|
+
{
|
|
299
|
+
sessionName: GITHUB_SESSION,
|
|
300
|
+
lastOutputEpochSeconds:
|
|
301
|
+
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
sessionName: 'orchestrator',
|
|
305
|
+
lastOutputEpochSeconds:
|
|
306
|
+
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
await useCase.run(runParams());
|
|
312
|
+
|
|
313
|
+
expect(mockTranscriptResolver.resolveTranscriptPaths).toHaveBeenCalledWith([
|
|
314
|
+
sessionFor(GITHUB_SESSION),
|
|
315
|
+
]);
|
|
316
|
+
expect(
|
|
317
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
318
|
+
).toHaveBeenCalledTimes(1);
|
|
319
|
+
expect(
|
|
320
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
321
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
|
|
231
322
|
});
|
|
232
323
|
|
|
233
324
|
it('passes the resolved transcript paths to the output and owner-call providers', async () => {
|
|
234
|
-
setupLiveInteractiveSession(
|
|
325
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
235
326
|
|
|
236
327
|
await useCase.run(runParams());
|
|
237
328
|
|
|
238
|
-
const expectedMap = transcriptMapFor([
|
|
329
|
+
const expectedMap = transcriptMapFor([GITHUB_SESSION]);
|
|
239
330
|
expect(
|
|
240
331
|
mockSessionOutputActivityRepository.listSessionOutputActivities,
|
|
241
332
|
).toHaveBeenCalledWith(expectedMap);
|
|
@@ -244,22 +335,22 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
244
335
|
).toHaveBeenCalledWith(expectedMap);
|
|
245
336
|
expect(
|
|
246
337
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName,
|
|
247
|
-
).toHaveBeenCalledWith([
|
|
338
|
+
).toHaveBeenCalledWith([GITHUB_SESSION]);
|
|
248
339
|
});
|
|
249
340
|
|
|
250
341
|
it('suppresses the stalled section and sends nothing when an owner call is pending past the threshold', async () => {
|
|
251
|
-
setupLiveInteractiveSession(
|
|
342
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
252
343
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
253
344
|
[
|
|
254
345
|
{
|
|
255
|
-
sessionName:
|
|
346
|
+
sessionName: GITHUB_SESSION,
|
|
256
347
|
lastOutputEpochSeconds:
|
|
257
348
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
258
349
|
},
|
|
259
350
|
],
|
|
260
351
|
);
|
|
261
352
|
mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
|
|
262
|
-
new Set([
|
|
353
|
+
new Set([GITHUB_SESSION]),
|
|
263
354
|
);
|
|
264
355
|
|
|
265
356
|
await useCase.run(runParams());
|
|
@@ -273,11 +364,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
273
364
|
});
|
|
274
365
|
|
|
275
366
|
it('sends the main stalled section when the session is silent past the threshold and not waiting on the owner', async () => {
|
|
276
|
-
setupLiveInteractiveSession(
|
|
367
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
277
368
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
278
369
|
[
|
|
279
370
|
{
|
|
280
|
-
sessionName:
|
|
371
|
+
sessionName: GITHUB_SESSION,
|
|
281
372
|
lastOutputEpochSeconds:
|
|
282
373
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
283
374
|
},
|
|
@@ -294,15 +385,15 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
294
385
|
);
|
|
295
386
|
expect(
|
|
296
387
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
297
|
-
).toHaveBeenCalledWith(
|
|
388
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
|
|
298
389
|
});
|
|
299
390
|
|
|
300
391
|
it('does not send the main stalled section when output is within the threshold', async () => {
|
|
301
|
-
setupLiveInteractiveSession(
|
|
392
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
302
393
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
303
394
|
[
|
|
304
395
|
{
|
|
305
|
-
sessionName:
|
|
396
|
+
sessionName: GITHUB_SESSION,
|
|
306
397
|
lastOutputEpochSeconds:
|
|
307
398
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS + 1,
|
|
308
399
|
},
|
|
@@ -317,7 +408,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
317
408
|
});
|
|
318
409
|
|
|
319
410
|
it('sends the sub-agent section when a sub-agent exceeds the silent threshold', async () => {
|
|
320
|
-
setupLiveInteractiveSession(
|
|
411
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
321
412
|
const subAgents: SubAgentActivity[] = [
|
|
322
413
|
{
|
|
323
414
|
label: 'sub-process-1',
|
|
@@ -326,7 +417,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
326
417
|
},
|
|
327
418
|
];
|
|
328
419
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
329
|
-
new Map([[
|
|
420
|
+
new Map([[GITHUB_SESSION, subAgents]]),
|
|
330
421
|
);
|
|
331
422
|
|
|
332
423
|
await useCase.run(runParams());
|
|
@@ -336,7 +427,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
336
427
|
);
|
|
337
428
|
expect(
|
|
338
429
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
339
|
-
).toHaveBeenCalledWith(
|
|
430
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
340
431
|
});
|
|
341
432
|
|
|
342
433
|
it('excludes an owner-handover spawn from selection so no notification is sent', async () => {
|
|
@@ -374,11 +465,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
374
465
|
});
|
|
375
466
|
|
|
376
467
|
it('re-notifies a session on every consecutive cycle while it remains a valid silent target', async () => {
|
|
377
|
-
setupLiveInteractiveSession(
|
|
468
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
378
469
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
379
470
|
[
|
|
380
471
|
{
|
|
381
|
-
sessionName:
|
|
472
|
+
sessionName: GITHUB_SESSION,
|
|
382
473
|
lastOutputEpochSeconds:
|
|
383
474
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
384
475
|
},
|
|
@@ -393,18 +484,18 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
393
484
|
).toHaveBeenCalledTimes(2);
|
|
394
485
|
expect(
|
|
395
486
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
396
|
-
).toHaveBeenNthCalledWith(1,
|
|
487
|
+
).toHaveBeenNthCalledWith(1, GITHUB_SESSION, MAIN_STALLED_SECTION);
|
|
397
488
|
expect(
|
|
398
489
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
399
|
-
).toHaveBeenNthCalledWith(2,
|
|
490
|
+
).toHaveBeenNthCalledWith(2, GITHUB_SESSION, MAIN_STALLED_SECTION);
|
|
400
491
|
});
|
|
401
492
|
|
|
402
493
|
it('notifies on every repeated cycle at the same instant without reading any cooldown state', async () => {
|
|
403
|
-
setupLiveInteractiveSession(
|
|
494
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
404
495
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
405
496
|
[
|
|
406
497
|
{
|
|
407
|
-
sessionName:
|
|
498
|
+
sessionName: GITHUB_SESSION,
|
|
408
499
|
lastOutputEpochSeconds:
|
|
409
500
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
410
501
|
},
|
|
@@ -426,20 +517,20 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
426
517
|
|
|
427
518
|
it('sends to multiple sessions sequentially with a stagger delay between sends', async () => {
|
|
428
519
|
mockSnapshotProvider.getSnapshot.mockResolvedValue(
|
|
429
|
-
snapshotWithSessions([
|
|
520
|
+
snapshotWithSessions([GITHUB_SESSION_ALPHA, GITHUB_SESSION_BRAVO]),
|
|
430
521
|
);
|
|
431
522
|
mockTranscriptResolver.resolveTranscriptPaths.mockReturnValue(
|
|
432
|
-
transcriptMapFor([
|
|
523
|
+
transcriptMapFor([GITHUB_SESSION_ALPHA, GITHUB_SESSION_BRAVO]),
|
|
433
524
|
);
|
|
434
525
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
435
526
|
[
|
|
436
527
|
{
|
|
437
|
-
sessionName:
|
|
528
|
+
sessionName: GITHUB_SESSION_ALPHA,
|
|
438
529
|
lastOutputEpochSeconds:
|
|
439
530
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
440
531
|
},
|
|
441
532
|
{
|
|
442
|
-
sessionName:
|
|
533
|
+
sessionName: GITHUB_SESSION_BRAVO,
|
|
443
534
|
lastOutputEpochSeconds:
|
|
444
535
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
445
536
|
},
|
|
@@ -458,7 +549,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
458
549
|
|
|
459
550
|
await useCase.run(runParams());
|
|
460
551
|
|
|
461
|
-
expect(callOrder).toEqual([
|
|
552
|
+
expect(callOrder).toEqual([
|
|
553
|
+
`send:${GITHUB_SESSION_ALPHA}`,
|
|
554
|
+
'sleep',
|
|
555
|
+
`send:${GITHUB_SESSION_BRAVO}`,
|
|
556
|
+
]);
|
|
462
557
|
expect(mockSleeper.sleep).toHaveBeenCalledTimes(1);
|
|
463
558
|
expect(mockSleeper.sleep).toHaveBeenCalledWith(
|
|
464
559
|
DEFAULT_NOTIFICATION_STAGGER_SECONDS * 1000,
|
|
@@ -466,11 +561,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
466
561
|
});
|
|
467
562
|
|
|
468
563
|
it('does not suppress errors raised while sending a notification', async () => {
|
|
469
|
-
setupLiveInteractiveSession(
|
|
564
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
470
565
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
471
566
|
[
|
|
472
567
|
{
|
|
473
|
-
sessionName:
|
|
568
|
+
sessionName: GITHUB_SESSION,
|
|
474
569
|
lastOutputEpochSeconds:
|
|
475
570
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
476
571
|
},
|
|
@@ -538,7 +633,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
538
633
|
).not.toHaveBeenCalled();
|
|
539
634
|
});
|
|
540
635
|
|
|
541
|
-
it('
|
|
636
|
+
it('excludes a non-github-named session before the hub-task gate so the resolver is never consulted', async () => {
|
|
542
637
|
setupSilentMainSession('workbench');
|
|
543
638
|
|
|
544
639
|
await useCase.run(runParams({ activeHubTaskStatus: ACTIVE_STATUS }));
|
|
@@ -546,7 +641,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
546
641
|
expect(mockHubTaskStatusResolver.getIssueByUrl).not.toHaveBeenCalled();
|
|
547
642
|
expect(
|
|
548
643
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
549
|
-
).
|
|
644
|
+
).not.toHaveBeenCalled();
|
|
550
645
|
});
|
|
551
646
|
|
|
552
647
|
it('does not call the resolver at all when the active status is unconfigured', async () => {
|
|
@@ -616,6 +711,72 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
616
711
|
});
|
|
617
712
|
});
|
|
618
713
|
|
|
714
|
+
describe('isGitHubIssueOrPullRequestSessionName', () => {
|
|
715
|
+
it('accepts the encoded form of a github.com issue URL session name', () => {
|
|
716
|
+
expect(
|
|
717
|
+
isGitHubIssueOrPullRequestSessionName(
|
|
718
|
+
'https_//github_com/HiromiShikata/repo/issues/42',
|
|
719
|
+
),
|
|
720
|
+
).toBe(true);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('accepts the encoded form of a github.com pull-request URL session name', () => {
|
|
724
|
+
expect(
|
|
725
|
+
isGitHubIssueOrPullRequestSessionName(
|
|
726
|
+
'https_//github_com/HiromiShikata/repo/pull/77',
|
|
727
|
+
),
|
|
728
|
+
).toBe(true);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('accepts the raw form of a github.com issue and pull-request URL session name', () => {
|
|
732
|
+
expect(
|
|
733
|
+
isGitHubIssueOrPullRequestSessionName(
|
|
734
|
+
'https://github.com/HiromiShikata/repo/issues/42',
|
|
735
|
+
),
|
|
736
|
+
).toBe(true);
|
|
737
|
+
expect(
|
|
738
|
+
isGitHubIssueOrPullRequestSessionName(
|
|
739
|
+
'https://github.com/HiromiShikata/repo/pull/77',
|
|
740
|
+
),
|
|
741
|
+
).toBe(true);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('rejects a plain orchestrator-style session name', () => {
|
|
745
|
+
expect(isGitHubIssueOrPullRequestSessionName('workbench')).toBe(false);
|
|
746
|
+
expect(isGitHubIssueOrPullRequestSessionName('orchestrator')).toBe(false);
|
|
747
|
+
expect(isGitHubIssueOrPullRequestSessionName('aw-host')).toBe(false);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('rejects a non-github host even in the encoded form', () => {
|
|
751
|
+
expect(
|
|
752
|
+
isGitHubIssueOrPullRequestSessionName(
|
|
753
|
+
'https_//example_com/HiromiShikata/repo/issues/42',
|
|
754
|
+
),
|
|
755
|
+
).toBe(false);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('rejects a github.com session name that is neither an issue nor a pull request', () => {
|
|
759
|
+
expect(
|
|
760
|
+
isGitHubIssueOrPullRequestSessionName(
|
|
761
|
+
'https_//github_com/HiromiShikata/repo',
|
|
762
|
+
),
|
|
763
|
+
).toBe(false);
|
|
764
|
+
expect(
|
|
765
|
+
isGitHubIssueOrPullRequestSessionName(
|
|
766
|
+
'https_//github_com/HiromiShikata/repo/discussions/42',
|
|
767
|
+
),
|
|
768
|
+
).toBe(false);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('rejects a github.com issue URL whose trailing segment is not a number', () => {
|
|
772
|
+
expect(
|
|
773
|
+
isGitHubIssueOrPullRequestSessionName(
|
|
774
|
+
'https_//github_com/HiromiShikata/repo/issues/new',
|
|
775
|
+
),
|
|
776
|
+
).toBe(false);
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
|
|
619
780
|
it('uses the session entity helper for type completeness', () => {
|
|
620
781
|
expect(sessionFor('workbench')).toEqual({
|
|
621
782
|
sessionName: 'workbench',
|
|
@@ -25,6 +25,14 @@ export const parseHubTaskIssueUrlFromSessionName = (
|
|
|
25
25
|
return GITHUB_ISSUE_URL_PATTERN.test(sessionName) ? sessionName : null;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
const GITHUB_ISSUE_OR_PULL_REQUEST_SESSION_NAME_PATTERN =
|
|
29
|
+
/^https(:\/\/|_\/\/)github(\.com|_com)\/[^/]+\/[^/]+\/(issues|pull)\/\d+$/;
|
|
30
|
+
|
|
31
|
+
export const isGitHubIssueOrPullRequestSessionName = (
|
|
32
|
+
sessionName: string,
|
|
33
|
+
): boolean =>
|
|
34
|
+
GITHUB_ISSUE_OR_PULL_REQUEST_SESSION_NAME_PATTERN.test(sessionName);
|
|
35
|
+
|
|
28
36
|
export type HubTaskStatusResolver = Pick<IssueRepository, 'getIssueByUrl'>;
|
|
29
37
|
|
|
30
38
|
type NotifyCandidate = {
|
|
@@ -58,8 +66,18 @@ export class NotifySilentLiveSessionsUseCase {
|
|
|
58
66
|
}): Promise<void> => {
|
|
59
67
|
const snapshot =
|
|
60
68
|
await this.liveSessionProcessSnapshotProvider.getSnapshot();
|
|
61
|
-
const
|
|
69
|
+
const allInteractiveSessions =
|
|
62
70
|
this.resolveInteractiveLiveSessions.resolve(snapshot);
|
|
71
|
+
const interactiveSessions = allInteractiveSessions.filter((session) =>
|
|
72
|
+
isGitHubIssueOrPullRequestSessionName(session.sessionName),
|
|
73
|
+
);
|
|
74
|
+
const skippedNonGitHubSessionCount =
|
|
75
|
+
allInteractiveSessions.length - interactiveSessions.length;
|
|
76
|
+
if (skippedNonGitHubSessionCount > 0) {
|
|
77
|
+
console.log(
|
|
78
|
+
`Silent live session notification: ignoring ${skippedNonGitHubSessionCount} non-github-named interactive session(s); only sessions named after a github.com issue or pull-request URL are monitored.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
63
81
|
const transcriptPathBySessionName =
|
|
64
82
|
this.interactiveLiveSessionTranscriptResolver.resolveTranscriptPaths(
|
|
65
83
|
interactiveSessions,
|
|
@@ -5,6 +5,7 @@ export declare class FileSystemInteractiveLiveSessionTranscriptResolver implemen
|
|
|
5
5
|
constructor(sharedProjectsDirectory?: string);
|
|
6
6
|
resolveTranscriptPaths: (sessions: InteractiveLiveSession[]) => Map<string, string>;
|
|
7
7
|
private resolveTranscriptPath;
|
|
8
|
+
private resolveCandidateTranscriptPath;
|
|
8
9
|
private listProjectsDirectories;
|
|
9
10
|
private listCandidatePaths;
|
|
10
11
|
}
|
package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileSystemInteractiveLiveSessionTranscriptResolver.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,sBAAsB,EAAE,MAAM,8CAA8C,CAAC;AACtF,OAAO,EAAE,wCAAwC,EAAE,MAAM,mFAAmF,CAAC;AAc7I,qBAAa,kDAAmD,YAAW,wCAAwC;IAE/G,OAAO,CAAC,QAAQ,CAAC,uBAAuB;gBAAvB,uBAAuB,GAAE,MAAyC;IAGrF,sBAAsB,GACpB,UAAU,sBAAsB,EAAE,KACjC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CASpB;IAEF,OAAO,CAAC,qBAAqB,
|
|
1
|
+
{"version":3,"file":"FileSystemInteractiveLiveSessionTranscriptResolver.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,sBAAsB,EAAE,MAAM,8CAA8C,CAAC;AACtF,OAAO,EAAE,wCAAwC,EAAE,MAAM,mFAAmF,CAAC;AAc7I,qBAAa,kDAAmD,YAAW,wCAAwC;IAE/G,OAAO,CAAC,QAAQ,CAAC,uBAAuB;gBAAvB,uBAAuB,GAAE,MAAyC;IAGrF,sBAAsB,GACpB,UAAU,sBAAsB,EAAE,KACjC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CASpB;IAEF,OAAO,CAAC,qBAAqB,CAc3B;IAEF,OAAO,CAAC,8BAA8B,CAuBpC;IAEF,OAAO,CAAC,uBAAuB,CAM7B;IAEF,OAAO,CAAC,kBAAkB,CAsBxB;CACH"}
|
|
@@ -12,6 +12,7 @@ export declare const DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS: number;
|
|
|
12
12
|
export declare const DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS: number;
|
|
13
13
|
export declare const DEFAULT_NOTIFICATION_STAGGER_SECONDS = 25;
|
|
14
14
|
export declare const parseHubTaskIssueUrlFromSessionName: (sessionName: string) => string | null;
|
|
15
|
+
export declare const isGitHubIssueOrPullRequestSessionName: (sessionName: string) => boolean;
|
|
15
16
|
export type HubTaskStatusResolver = Pick<IssueRepository, 'getIssueByUrl'>;
|
|
16
17
|
export declare class NotifySilentLiveSessionsUseCase {
|
|
17
18
|
private readonly liveSessionProcessSnapshotProvider;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotifySilentLiveSessionsUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/NotifySilentLiveSessionsUseCase.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kCAAkC,EAAE,MAAM,yDAAyD,CAAC;AAC7G,OAAO,EAAE,wCAAwC,EAAE,MAAM,+DAA+D,CAAC;AACzH,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,+BAA+B,EAAE,MAAM,sDAAsD,CAAC;AACvG,OAAO,EAAE,iCAAiC,EAAE,MAAM,wDAAwD,CAAC;AAC3G,OAAO,EAAE,4BAA4B,EAAE,MAAM,mDAAmD,CAAC;AACjG,OAAO,EAAE,mCAAmC,EAAE,MAAM,0DAA0D,CAAC;AAC/G,OAAO,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AAGvE,eAAO,MAAM,qCAAqC,QAAU,CAAC;AAC7D,eAAO,MAAM,yCAAyC,QAAS,CAAC;AAChE,eAAO,MAAM,0CAA0C,QAAU,CAAC;AAClE,eAAO,MAAM,oCAAoC,KAAK,CAAC;AAKvD,eAAO,MAAM,mCAAmC,GAC9C,aAAa,MAAM,KAClB,MAAM,GAAG,IAEX,CAAC;
|
|
1
|
+
{"version":3,"file":"NotifySilentLiveSessionsUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/NotifySilentLiveSessionsUseCase.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kCAAkC,EAAE,MAAM,yDAAyD,CAAC;AAC7G,OAAO,EAAE,wCAAwC,EAAE,MAAM,+DAA+D,CAAC;AACzH,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,+BAA+B,EAAE,MAAM,sDAAsD,CAAC;AACvG,OAAO,EAAE,iCAAiC,EAAE,MAAM,wDAAwD,CAAC;AAC3G,OAAO,EAAE,4BAA4B,EAAE,MAAM,mDAAmD,CAAC;AACjG,OAAO,EAAE,mCAAmC,EAAE,MAAM,0DAA0D,CAAC;AAC/G,OAAO,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AAGvE,eAAO,MAAM,qCAAqC,QAAU,CAAC;AAC7D,eAAO,MAAM,yCAAyC,QAAS,CAAC;AAChE,eAAO,MAAM,0CAA0C,QAAU,CAAC;AAClE,eAAO,MAAM,oCAAoC,KAAK,CAAC;AAKvD,eAAO,MAAM,mCAAmC,GAC9C,aAAa,MAAM,KAClB,MAAM,GAAG,IAEX,CAAC;AAKF,eAAO,MAAM,qCAAqC,GAChD,aAAa,MAAM,KAClB,OACkE,CAAC;AAEtE,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;AAO3E,qBAAa,+BAA+B;IAKxC,OAAO,CAAC,QAAQ,CAAC,kCAAkC;IACnD,OAAO,CAAC,QAAQ,CAAC,wCAAwC;IACzD,OAAO,CAAC,QAAQ,CAAC,+BAA+B;IAChD,OAAO,CAAC,QAAQ,CAAC,0BAA0B;IAC3C,OAAO,CAAC,QAAQ,CAAC,uBAAuB;IACxC,OAAO,CAAC,QAAQ,CAAC,sBAAsB;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,qBAAqB;IAZxC,OAAO,CAAC,QAAQ,CAAC,8BAA8B,CACD;gBAG3B,kCAAkC,EAAE,kCAAkC,EACtE,wCAAwC,EAAE,wCAAwC,EAClF,+BAA+B,EAAE,+BAA+B,EAChE,0BAA0B,EAAE,iCAAiC,EAC7D,uBAAuB,EAAE,uBAAuB,EAChD,sBAAsB,EAAE,mCAAmC,EAC3D,eAAe,EAAE,4BAA4B,EAC7C,OAAO,EAAE,OAAO,EAChB,qBAAqB,GAAE,qBAAqB,GAAG,IAAW;IAG7E,GAAG,GAAU,QAAQ;QACnB,0BAA0B,EAAE,MAAM,CAAC;QACnC,8BAA8B,EAAE,MAAM,CAAC;QACvC,+BAA+B,EAAE,MAAM,CAAC;QACxC,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;QACnC,GAAG,EAAE,IAAI,CAAC;KACX,KAAG,OAAO,CAAC,IAAI,CAAC,CAiEf;IAEF,OAAO,CAAC,eAAe,CAmCrB;IAEF,OAAO,CAAC,gBAAgB,CA8CtB;IAEF,OAAO,CAAC,cAAc,CAqCpB;CACH"}
|