github-issue-tower-defence-management 1.114.1 → 1.114.2

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 CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.114.2](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.114.1...v1.114.2) (2026-06-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **silent-notification:** count any transcript entry and resolve non-resume transcripts ([#1061](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1061)) ([dbb8bb4](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/dbb8bb4171eb4ccd61a9e702a685ce7f9642e43a))
7
+
1
8
  ## [1.114.1](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.114.0...v1.114.1) (2026-06-28)
2
9
 
3
10
 
package/README.md CHANGED
@@ -556,9 +556,9 @@ For each `In Tmux by human` issue with no live session, the reconciler launches
556
556
 
557
557
  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.
558
558
 
559
- Sessions are selected purely from the live process tree, independently of session naming or any GitHub issue linkage: 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. Both issue-url-named sessions and plain-named sessions (for example `workbench`) are selected equally. 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. 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, 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) followed by the launch id from the `CLAUDE_CODE_SESSION_ID` environment variable. 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 most recently modified match is used when several exist. A session that has no issue URL resolves just as reliably as one that does.
559
+ Sessions are selected purely from the live process tree, independently of session naming or any GitHub issue linkage: 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. Both issue-url-named sessions and plain-named sessions (for example `workbench`) are selected equally. 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. 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 files are matched across both roots by existence; the most recently modified match is used only as a tiebreak when several candidate files exist. 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.
560
560
 
561
- 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 `assistant` entry in the session's resolved transcript rather than from the transcript file modification time, so a transcript that is only updated by tool results or owner replies still counts as silent. 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.
561
+ 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.
562
562
 
563
563
  The sub-process section is sent, regardless of main output, when at least one sub-process of the session has been output-idle for `subAgentSilentThresholdSeconds` or has been running for `subAgentRunningThresholdSeconds`; the message lists each matched sub-process with its idle and running minutes. Sub-processes are discovered in one of two ways: when `subAgentTranscriptRootDirectory` is configured, each session's `subagents/agent-<id>.jsonl` transcripts are scanned, finished sub-agents (latest transcript entry `stop_reason` equal to `end_turn` or `stop_sequence`) are skipped, and idle time is taken from the transcript file modification time (following symlinks) while running time is taken from its first entry timestamp; otherwise sub-processes are discovered by matching `subAgentProcessMatchPattern` against process command lines.
564
564
 
@@ -48,25 +48,28 @@ const parseEpochMilliseconds = (timestamp) => {
48
48
  return Number.isNaN(parsed) ? null : parsed;
49
49
  };
50
50
  /**
51
- * Reads the last main-session output time for each live session from its
51
+ * Reads the last main-session activity time for each live session from its
52
52
  * already-resolved transcript path. Idle time is computed from the timestamp of
53
- * the latest `assistant` entry rather than from the transcript file modification
54
- * time, so a transcript touched only by tool results or owner replies still
55
- * counts as silent.
53
+ * the latest entry of any kind (assistant text, owner replies, tool results, or
54
+ * any other entry type) rather than from the transcript file modification time.
55
+ * Because a session that is actively running tool calls keeps appending entries
56
+ * such as `user` and `tool_result` even while it emits no assistant text, every
57
+ * entry with a parseable timestamp counts as activity, so a working session is
58
+ * not mistaken for a silent one.
56
59
  */
57
60
  class FileSystemSessionOutputActivityRepository {
58
61
  constructor() {
59
62
  this.listSessionOutputActivities = async (transcriptPathBySessionName) => {
60
63
  const activities = [];
61
64
  for (const [sessionName, transcriptPath] of transcriptPathBySessionName) {
62
- const lastOutputEpochSeconds = this.readLastAssistantOutputEpochSeconds(transcriptPath);
65
+ const lastOutputEpochSeconds = this.readLastActivityEpochSeconds(transcriptPath);
63
66
  if (lastOutputEpochSeconds !== null) {
64
67
  activities.push({ sessionName, lastOutputEpochSeconds });
65
68
  }
66
69
  }
67
70
  return activities;
68
71
  };
69
- this.readLastAssistantOutputEpochSeconds = (transcriptPath) => {
72
+ this.readLastActivityEpochSeconds = (transcriptPath) => {
70
73
  let content;
71
74
  try {
72
75
  content = fs.readFileSync(transcriptPath, 'utf8');
@@ -74,7 +77,7 @@ class FileSystemSessionOutputActivityRepository {
74
77
  catch {
75
78
  return null;
76
79
  }
77
- let lastAssistantEpochMs = null;
80
+ let lastActivityEpochMs = null;
78
81
  for (const line of content.split('\n')) {
79
82
  const trimmed = line.trim();
80
83
  if (trimmed.length === 0) {
@@ -90,21 +93,18 @@ class FileSystemSessionOutputActivityRepository {
90
93
  if (!isRecord(parsed)) {
91
94
  continue;
92
95
  }
93
- if (readString(parsed, 'type') !== 'assistant') {
94
- continue;
95
- }
96
96
  const epochMs = parseEpochMilliseconds(readString(parsed, 'timestamp'));
97
97
  if (epochMs === null) {
98
98
  continue;
99
99
  }
100
- if (lastAssistantEpochMs === null || epochMs > lastAssistantEpochMs) {
101
- lastAssistantEpochMs = epochMs;
100
+ if (lastActivityEpochMs === null || epochMs > lastActivityEpochMs) {
101
+ lastActivityEpochMs = epochMs;
102
102
  }
103
103
  }
104
- if (lastAssistantEpochMs === null) {
104
+ if (lastActivityEpochMs === null) {
105
105
  return null;
106
106
  }
107
- return Math.floor(lastAssistantEpochMs / 1000);
107
+ return Math.floor(lastActivityEpochMs / 1000);
108
108
  };
109
109
  }
110
110
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FileSystemSessionOutputActivityRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FileSystemSessionOutputActivityRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAyB;AAIzB,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAoC,EAAE,CACpE,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AAE9C,MAAM,UAAU,GAAG,CACjB,KAA8B,EAC9B,GAAW,EACI,EAAE;IACjB,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,OAAO,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1D,CAAC,CAAC;AAEF,MAAM,sBAAsB,GAAG,CAAC,SAAwB,EAAiB,EAAE;IACzE,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;AAC9C,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAa,yCAAyC;IAAtD;QACE,gCAA2B,GAAG,KAAK,EACjC,2BAAgD,EACV,EAAE;YACxC,MAAM,UAAU,GAAgC,EAAE,CAAC;YACnD,KAAK,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,IAAI,2BAA2B,EAAE,CAAC;gBACxE,MAAM,sBAAsB,GAC1B,IAAI,CAAC,4BAA4B,CAAC,cAAc,CAAC,CAAC;gBACpD,IAAI,sBAAsB,KAAK,IAAI,EAAE,CAAC;oBACpC,UAAU,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,sBAAsB,EAAE,CAAC,CAAC;gBAC3D,CAAC;YACH,CAAC;YACD,OAAO,UAAU,CAAC;QACpB,CAAC,CAAC;QAEM,iCAA4B,GAAG,CACrC,cAAsB,EACP,EAAE;YACjB,IAAI,OAAe,CAAC;YACpB,IAAI,CAAC;gBACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;YACpD,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;YACD,IAAI,mBAAmB,GAAkB,IAAI,CAAC;YAC9C,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;gBAC5B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACzB,SAAS;gBACX,CAAC;gBACD,IAAI,MAAe,CAAC;gBACpB,IAAI,CAAC;oBACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC/B,CAAC;gBAAC,MAAM,CAAC;oBACP,SAAS;gBACX,CAAC;gBACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;oBACtB,SAAS;gBACX,CAAC;gBACD,MAAM,OAAO,GAAG,sBAAsB,CAAC,UAAU,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;gBACxE,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;oBACrB,SAAS;gBACX,CAAC;gBACD,IAAI,mBAAmB,KAAK,IAAI,IAAI,OAAO,GAAG,mBAAmB,EAAE,CAAC;oBAClE,mBAAmB,GAAG,OAAO,CAAC;gBAChC,CAAC;YACH,CAAC;YACD,IAAI,mBAAmB,KAAK,IAAI,EAAE,CAAC;gBACjC,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,IAAI,CAAC,KAAK,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;QAChD,CAAC,CAAC;IACJ,CAAC;CAAA;AApDD,8FAoDC"}
@@ -52,26 +52,54 @@ class ResolveInteractiveLiveSessionsUseCase {
52
52
  interactiveProcess.configDir === null) {
53
53
  continue;
54
54
  }
55
+ const candidateSessionIds = this.collectCandidateSessionIds(interactiveProcess, childrenByPpid);
55
56
  sessions.push({
56
57
  sessionName: session.sessionName,
57
58
  sessionId: interactiveProcess.sessionId,
58
- candidateSessionIds: this.collectCandidateSessionIds(interactiveProcess),
59
+ candidateSessionIds,
59
60
  configDir: interactiveProcess.configDir,
60
61
  });
61
62
  }
62
63
  return sessions;
63
64
  };
64
- this.collectCandidateSessionIds = (interactiveProcess) => {
65
+ /**
66
+ * Collects the distinct session ids that may name the actively-written
67
+ * transcript on disk, in priority order. The rotated current session id
68
+ * recorded for the interactive process is first, because for a resumed or
69
+ * compacted session the id rotates and the live transcript is named by the
70
+ * current id. The interactive process's own launch id follows, then the
71
+ * distinct ids propagated to its descendant processes. For a `--resume`
72
+ * session these ids coincide, yielding a single id; for a non-resume session
73
+ * the own (launch) id names no transcript and the live transcript is named by
74
+ * the descendant-propagated id, which is included here so the resolver can
75
+ * find the actively-written file.
76
+ */
77
+ this.collectCandidateSessionIds = (interactiveProcess, childrenByPpid) => {
65
78
  const candidateSessionIds = [];
66
79
  const seenSessionIds = new Set();
67
- for (const sessionId of [
68
- interactiveProcess.currentSessionId,
69
- interactiveProcess.sessionId,
70
- ]) {
80
+ const pushSessionId = (sessionId) => {
71
81
  if (sessionId !== null && !seenSessionIds.has(sessionId)) {
72
82
  seenSessionIds.add(sessionId);
73
83
  candidateSessionIds.push(sessionId);
74
84
  }
85
+ };
86
+ pushSessionId(interactiveProcess.currentSessionId);
87
+ const visitedPids = new Set();
88
+ const queue = [interactiveProcess];
89
+ let head = 0;
90
+ while (head < queue.length) {
91
+ const process = queue[head];
92
+ head += 1;
93
+ if (visitedPids.has(process.pid)) {
94
+ continue;
95
+ }
96
+ visitedPids.add(process.pid);
97
+ pushSessionId(process.sessionId);
98
+ for (const child of childrenByPpid.get(process.pid) ?? []) {
99
+ if (!visitedPids.has(child.pid)) {
100
+ queue.push(child);
101
+ }
102
+ }
75
103
  }
76
104
  return candidateSessionIds;
77
105
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ResolveInteractiveLiveSessionsUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.ts"],"names":[],"mappings":";;;AAMA,8EAA8E;AAC9E,4EAA4E;AAC5E,gFAAgF;AAChF,+EAA+E;AAC/E,iEAAiE;AACpD,QAAA,6BAA6B,GAAG,mBAAmB,CAAC;AAEjE,8EAA8E;AAC9E,MAAM,kCAAkC,GAAG,QAAQ,CAAC;AAEpD,MAAM,sBAAsB,GAAG,CAAC,OAA+B,EAAW,EAAE;IAC1E,IAAI,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,qCAA6B,CAAC,EAAE,CAAC;QAChE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,kCAAkC,CAAC,EAAE,CAAC;QACtE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,OAAO,CAAC,SAAS,KAAK,IAAI,IAAI,OAAO,CAAC,SAAS,KAAK,IAAI,CAAC;AAClE,CAAC,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAa,qCAAqC;IAAlD;QACE,YAAO,GAAG,CACR,QAAoC,EACV,EAAE;YAC5B,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkC,CAAC;YAC/D,MAAM,cAAc,GAAG,IAAI,GAAG,EAAoC,CAAC;YACnE,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;gBACzC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;gBACvC,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACxD,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACvB,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC7C,CAAC;YAED,MAAM,QAAQ,GAA6B,EAAE,CAAC;YAC9C,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACxC,MAAM,kBAAkB,GAAG,IAAI,CAAC,sBAAsB,CACpD,OAAO,CAAC,QAAQ,EAChB,YAAY,EACZ,cAAc,CACf,CAAC;gBACF,IACE,kBAAkB,KAAK,IAAI;oBAC3B,kBAAkB,CAAC,SAAS,KAAK,IAAI;oBACrC,kBAAkB,CAAC,SAAS,KAAK,IAAI,EACrC,CAAC;oBACD,SAAS;gBACX,CAAC;gBACD,QAAQ,CAAC,IAAI,CAAC;oBACZ,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,SAAS,EAAE,kBAAkB,CAAC,SAAS;oBACvC,mBAAmB,EACjB,IAAI,CAAC,0BAA0B,CAAC,kBAAkB,CAAC;oBACrD,SAAS,EAAE,kBAAkB,CAAC,SAAS;iBACxC,CAAC,CAAC;YACL,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC;QAEM,+BAA0B,GAAG,CACnC,kBAA0C,EAChC,EAAE;YACZ,MAAM,mBAAmB,GAAa,EAAE,CAAC;YACzC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;YACzC,KAAK,MAAM,SAAS,IAAI;gBACtB,kBAAkB,CAAC,gBAAgB;gBACnC,kBAAkB,CAAC,SAAS;aAC7B,EAAE,CAAC;gBACF,IAAI,SAAS,KAAK,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;oBACzD,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;oBAC9B,mBAAmB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACtC,CAAC;YACH,CAAC;YACD,OAAO,mBAAmB,CAAC;QAC7B,CAAC,CAAC;QAEM,2BAAsB,GAAG,CAC/B,QAAkB,EAClB,YAAiD,EACjD,cAAqD,EACtB,EAAE;YACjC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;YAClC,MAAM,KAAK,GAAa,CAAC,GAAG,QAAQ,CAAC,CAAC;YACtC,IAAI,IAAI,GAAG,CAAC,CAAC;YACb,OAAO,IAAI,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;gBAC3B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;gBACxB,IAAI,IAAI,CAAC,CAAC;gBACV,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;oBACrB,SAAS;gBACX,CAAC;gBACD,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACjB,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACtC,IAAI,OAAO,KAAK,SAAS,IAAI,sBAAsB,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC7D,OAAO,OAAO,CAAC;gBACjB,CAAC;gBACD,KAAK,MAAM,KAAK,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;oBAClD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;wBAC5B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;oBACxB,CAAC;gBACH,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;IACJ,CAAC;CAAA;AAlFD,sFAkFC"}
1
+ {"version":3,"file":"ResolveInteractiveLiveSessionsUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.114.1",
3
+ "version": "1.114.2",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -87,6 +87,52 @@ describe('FileSystemInteractiveLiveSessionTranscriptResolver', () => {
87
87
  expect(result.get('workbench')).toBe(rotatedPath);
88
88
  });
89
89
 
90
+ it('resolves a non-resume session to the descendant id file when the own launch id file is absent', () => {
91
+ const configDir = path.join(configRoot, 'non-resume');
92
+ const harnessPath = writeTranscript({
93
+ projectsDirectory: path.join(configDir, 'projects'),
94
+ cwdSlug: '-home-user',
95
+ sessionId: 'harness-id',
96
+ });
97
+ const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver(
98
+ sharedProjectsDirectory,
99
+ );
100
+
101
+ const result = resolver.resolveTranscriptPaths([
102
+ {
103
+ sessionName: 'non-resume',
104
+ sessionId: 'launch-id',
105
+ candidateSessionIds: ['launch-id', 'harness-id'],
106
+ configDir,
107
+ },
108
+ ]);
109
+
110
+ expect(result.get('non-resume')).toBe(harnessPath);
111
+ });
112
+
113
+ it('resolves a resume session to the own id file when descendants share the own id', () => {
114
+ const configDir = path.join(configRoot, 'resume');
115
+ const ownPath = writeTranscript({
116
+ projectsDirectory: path.join(configDir, 'projects'),
117
+ cwdSlug: '-home-user',
118
+ sessionId: 'own-id',
119
+ });
120
+ const resolver = new FileSystemInteractiveLiveSessionTranscriptResolver(
121
+ sharedProjectsDirectory,
122
+ );
123
+
124
+ const result = resolver.resolveTranscriptPaths([
125
+ {
126
+ sessionName: 'resume',
127
+ sessionId: 'own-id',
128
+ candidateSessionIds: ['own-id', 'own-id'],
129
+ configDir,
130
+ },
131
+ ]);
132
+
133
+ expect(result.get('resume')).toBe(ownPath);
134
+ });
135
+
90
136
  it('resolves a transcript that lives under the shared projects directory', () => {
91
137
  const configDir = path.join(configRoot, 'workbench');
92
138
  const sharedPath = writeTranscript({
@@ -31,6 +31,17 @@ describe('FileSystemSessionOutputActivityRepository', () => {
31
31
  message: { role: 'user', content: 'go ahead' },
32
32
  });
33
33
 
34
+ const toolResultEntry = (timestamp: string): object => ({
35
+ type: 'tool_result',
36
+ timestamp,
37
+ toolUseResult: { stdout: 'done', stderr: '' },
38
+ });
39
+
40
+ const untimestampedEntry = (): object => ({
41
+ type: 'summary',
42
+ summary: 'no timestamp on this entry',
43
+ });
44
+
34
45
  const writeTranscript = (fileName: string, lines: object[]): string => {
35
46
  const filePath = path.join(rootDirectory, fileName);
36
47
  fs.writeFileSync(
@@ -41,7 +52,7 @@ describe('FileSystemSessionOutputActivityRepository', () => {
41
52
  return filePath;
42
53
  };
43
54
 
44
- it('returns the latest assistant entry timestamp as the last output epoch', async () => {
55
+ it('returns the latest entry timestamp of any type as the last activity epoch', async () => {
45
56
  const transcriptPath = writeTranscript('workbench.jsonl', [
46
57
  assistantEntry('2026-06-27T10:00:00.000Z'),
47
58
  userEntry('2026-06-27T10:30:00.000Z'),
@@ -57,13 +68,13 @@ describe('FileSystemSessionOutputActivityRepository', () => {
57
68
  {
58
69
  sessionName: 'workbench',
59
70
  lastOutputEpochSeconds: Math.floor(
60
- Date.parse('2026-06-27T10:05:00.000Z') / 1000,
71
+ Date.parse('2026-06-27T10:30:00.000Z') / 1000,
61
72
  ),
62
73
  },
63
74
  ]);
64
75
  });
65
76
 
66
- it('ignores user entries when computing the last output epoch', async () => {
77
+ it('advances the last activity time when a later user entry follows an assistant entry', async () => {
67
78
  const transcriptPath = writeTranscript('workbench.jsonl', [
68
79
  assistantEntry('2026-06-27T10:00:00.000Z'),
69
80
  userEntry('2026-06-27T11:00:00.000Z'),
@@ -78,7 +89,28 @@ describe('FileSystemSessionOutputActivityRepository', () => {
78
89
  {
79
90
  sessionName: 'workbench',
80
91
  lastOutputEpochSeconds: Math.floor(
81
- Date.parse('2026-06-27T10:00:00.000Z') / 1000,
92
+ Date.parse('2026-06-27T11:00:00.000Z') / 1000,
93
+ ),
94
+ },
95
+ ]);
96
+ });
97
+
98
+ it('advances the last activity time when a tool_result entry is the latest entry', async () => {
99
+ const transcriptPath = writeTranscript('workbench.jsonl', [
100
+ assistantEntry('2026-06-27T10:00:00.000Z'),
101
+ toolResultEntry('2026-06-27T10:45:00.000Z'),
102
+ ]);
103
+ const repository = new FileSystemSessionOutputActivityRepository();
104
+
105
+ const result = await repository.listSessionOutputActivities(
106
+ new Map([['workbench', transcriptPath]]),
107
+ );
108
+
109
+ expect(result).toEqual([
110
+ {
111
+ sessionName: 'workbench',
112
+ lastOutputEpochSeconds: Math.floor(
113
+ Date.parse('2026-06-27T10:45:00.000Z') / 1000,
82
114
  ),
83
115
  },
84
116
  ]);
@@ -107,7 +139,7 @@ describe('FileSystemSessionOutputActivityRepository', () => {
107
139
  ]);
108
140
  });
109
141
 
110
- it('omits sessions whose transcript has no assistant entry', async () => {
142
+ it('resolves a transcript whose only entry is a non-assistant entry', async () => {
111
143
  const transcriptPath = writeTranscript('workbench.jsonl', [
112
144
  userEntry('2026-06-27T10:00:00.000Z'),
113
145
  ]);
@@ -117,6 +149,26 @@ describe('FileSystemSessionOutputActivityRepository', () => {
117
149
  new Map([['workbench', transcriptPath]]),
118
150
  );
119
151
 
152
+ expect(result).toEqual([
153
+ {
154
+ sessionName: 'workbench',
155
+ lastOutputEpochSeconds: Math.floor(
156
+ Date.parse('2026-06-27T10:00:00.000Z') / 1000,
157
+ ),
158
+ },
159
+ ]);
160
+ });
161
+
162
+ it('omits sessions whose transcript has no parseable timestamp', async () => {
163
+ const transcriptPath = writeTranscript('workbench.jsonl', [
164
+ untimestampedEntry(),
165
+ ]);
166
+ const repository = new FileSystemSessionOutputActivityRepository();
167
+
168
+ const result = await repository.listSessionOutputActivities(
169
+ new Map([['workbench', transcriptPath]]),
170
+ );
171
+
120
172
  expect(result).toEqual([]);
121
173
  });
122
174
 
@@ -22,11 +22,14 @@ const parseEpochMilliseconds = (timestamp: string | null): number | null => {
22
22
  };
23
23
 
24
24
  /**
25
- * Reads the last main-session output time for each live session from its
25
+ * Reads the last main-session activity time for each live session from its
26
26
  * already-resolved transcript path. Idle time is computed from the timestamp of
27
- * the latest `assistant` entry rather than from the transcript file modification
28
- * time, so a transcript touched only by tool results or owner replies still
29
- * counts as silent.
27
+ * the latest entry of any kind (assistant text, owner replies, tool results, or
28
+ * any other entry type) rather than from the transcript file modification time.
29
+ * Because a session that is actively running tool calls keeps appending entries
30
+ * such as `user` and `tool_result` even while it emits no assistant text, every
31
+ * entry with a parseable timestamp counts as activity, so a working session is
32
+ * not mistaken for a silent one.
30
33
  */
31
34
  export class FileSystemSessionOutputActivityRepository implements SessionOutputActivityRepository {
32
35
  listSessionOutputActivities = async (
@@ -35,7 +38,7 @@ export class FileSystemSessionOutputActivityRepository implements SessionOutputA
35
38
  const activities: LiveSessionOutputActivity[] = [];
36
39
  for (const [sessionName, transcriptPath] of transcriptPathBySessionName) {
37
40
  const lastOutputEpochSeconds =
38
- this.readLastAssistantOutputEpochSeconds(transcriptPath);
41
+ this.readLastActivityEpochSeconds(transcriptPath);
39
42
  if (lastOutputEpochSeconds !== null) {
40
43
  activities.push({ sessionName, lastOutputEpochSeconds });
41
44
  }
@@ -43,7 +46,7 @@ export class FileSystemSessionOutputActivityRepository implements SessionOutputA
43
46
  return activities;
44
47
  };
45
48
 
46
- private readLastAssistantOutputEpochSeconds = (
49
+ private readLastActivityEpochSeconds = (
47
50
  transcriptPath: string,
48
51
  ): number | null => {
49
52
  let content: string;
@@ -52,7 +55,7 @@ export class FileSystemSessionOutputActivityRepository implements SessionOutputA
52
55
  } catch {
53
56
  return null;
54
57
  }
55
- let lastAssistantEpochMs: number | null = null;
58
+ let lastActivityEpochMs: number | null = null;
56
59
  for (const line of content.split('\n')) {
57
60
  const trimmed = line.trim();
58
61
  if (trimmed.length === 0) {
@@ -67,20 +70,17 @@ export class FileSystemSessionOutputActivityRepository implements SessionOutputA
67
70
  if (!isRecord(parsed)) {
68
71
  continue;
69
72
  }
70
- if (readString(parsed, 'type') !== 'assistant') {
71
- continue;
72
- }
73
73
  const epochMs = parseEpochMilliseconds(readString(parsed, 'timestamp'));
74
74
  if (epochMs === null) {
75
75
  continue;
76
76
  }
77
- if (lastAssistantEpochMs === null || epochMs > lastAssistantEpochMs) {
78
- lastAssistantEpochMs = epochMs;
77
+ if (lastActivityEpochMs === null || epochMs > lastActivityEpochMs) {
78
+ lastActivityEpochMs = epochMs;
79
79
  }
80
80
  }
81
- if (lastAssistantEpochMs === null) {
81
+ if (lastActivityEpochMs === null) {
82
82
  return null;
83
83
  }
84
- return Math.floor(lastAssistantEpochMs / 1000);
84
+ return Math.floor(lastActivityEpochMs / 1000);
85
85
  };
86
86
  }
@@ -219,6 +219,82 @@ describe('ResolveInteractiveLiveSessionsUseCase', () => {
219
219
  ]);
220
220
  });
221
221
 
222
+ it('collects the descendant-propagated session id after the own launch id for a non-resume session', () => {
223
+ const snapshot: LiveSessionProcessSnapshot = {
224
+ sessions: [{ sessionName: 'non-resume', panePids: [900] }],
225
+ processes: [
226
+ processInfo({ pid: 900, ppid: 1, commandLine: 'shell' }),
227
+ processInfo({
228
+ pid: 901,
229
+ ppid: 900,
230
+ commandLine: 'claude --model opus',
231
+ sessionId: 'launch-id',
232
+ configDir: '/config/non-resume',
233
+ }),
234
+ processInfo({
235
+ pid: 902,
236
+ ppid: 901,
237
+ commandLine: 'node tool worker',
238
+ sessionId: 'harness-id',
239
+ configDir: '/config/non-resume',
240
+ }),
241
+ processInfo({
242
+ pid: 903,
243
+ ppid: 902,
244
+ commandLine: 'node nested worker',
245
+ sessionId: 'harness-id',
246
+ configDir: '/config/non-resume',
247
+ }),
248
+ ],
249
+ };
250
+
251
+ const result = useCase.resolve(snapshot);
252
+
253
+ expect(result).toEqual([
254
+ {
255
+ sessionName: 'non-resume',
256
+ sessionId: 'launch-id',
257
+ candidateSessionIds: ['launch-id', 'harness-id'],
258
+ configDir: '/config/non-resume',
259
+ },
260
+ ]);
261
+ });
262
+
263
+ it('orders the rotated current id, then the launch id, then the descendant id', () => {
264
+ const snapshot: LiveSessionProcessSnapshot = {
265
+ sessions: [{ sessionName: 'non-resume', panePids: [910] }],
266
+ processes: [
267
+ processInfo({ pid: 910, ppid: 1, commandLine: 'shell' }),
268
+ processInfo({
269
+ pid: 911,
270
+ ppid: 910,
271
+ commandLine: 'claude --model opus',
272
+ sessionId: 'launch-id',
273
+ currentSessionId: 'rotated-id',
274
+ configDir: '/config/non-resume',
275
+ }),
276
+ processInfo({
277
+ pid: 912,
278
+ ppid: 911,
279
+ commandLine: 'node tool worker',
280
+ sessionId: 'harness-id',
281
+ configDir: '/config/non-resume',
282
+ }),
283
+ ],
284
+ };
285
+
286
+ const result = useCase.resolve(snapshot);
287
+
288
+ expect(result).toEqual([
289
+ {
290
+ sessionName: 'non-resume',
291
+ sessionId: 'launch-id',
292
+ candidateSessionIds: ['rotated-id', 'launch-id', 'harness-id'],
293
+ configDir: '/config/non-resume',
294
+ },
295
+ ]);
296
+ });
297
+
222
298
  it('returns an empty list when there are no sessions', () => {
223
299
  const result = useCase.resolve({ sessions: [], processes: [] });
224
300
 
@@ -66,30 +66,61 @@ export class ResolveInteractiveLiveSessionsUseCase {
66
66
  ) {
67
67
  continue;
68
68
  }
69
+ const candidateSessionIds = this.collectCandidateSessionIds(
70
+ interactiveProcess,
71
+ childrenByPpid,
72
+ );
69
73
  sessions.push({
70
74
  sessionName: session.sessionName,
71
75
  sessionId: interactiveProcess.sessionId,
72
- candidateSessionIds:
73
- this.collectCandidateSessionIds(interactiveProcess),
76
+ candidateSessionIds,
74
77
  configDir: interactiveProcess.configDir,
75
78
  });
76
79
  }
77
80
  return sessions;
78
81
  };
79
82
 
83
+ /**
84
+ * Collects the distinct session ids that may name the actively-written
85
+ * transcript on disk, in priority order. The rotated current session id
86
+ * recorded for the interactive process is first, because for a resumed or
87
+ * compacted session the id rotates and the live transcript is named by the
88
+ * current id. The interactive process's own launch id follows, then the
89
+ * distinct ids propagated to its descendant processes. For a `--resume`
90
+ * session these ids coincide, yielding a single id; for a non-resume session
91
+ * the own (launch) id names no transcript and the live transcript is named by
92
+ * the descendant-propagated id, which is included here so the resolver can
93
+ * find the actively-written file.
94
+ */
80
95
  private collectCandidateSessionIds = (
81
96
  interactiveProcess: LiveSessionProcessInfo,
97
+ childrenByPpid: Map<number, LiveSessionProcessInfo[]>,
82
98
  ): string[] => {
83
99
  const candidateSessionIds: string[] = [];
84
100
  const seenSessionIds = new Set<string>();
85
- for (const sessionId of [
86
- interactiveProcess.currentSessionId,
87
- interactiveProcess.sessionId,
88
- ]) {
101
+ const pushSessionId = (sessionId: string | null): void => {
89
102
  if (sessionId !== null && !seenSessionIds.has(sessionId)) {
90
103
  seenSessionIds.add(sessionId);
91
104
  candidateSessionIds.push(sessionId);
92
105
  }
106
+ };
107
+ pushSessionId(interactiveProcess.currentSessionId);
108
+ const visitedPids = new Set<number>();
109
+ const queue: LiveSessionProcessInfo[] = [interactiveProcess];
110
+ let head = 0;
111
+ while (head < queue.length) {
112
+ const process = queue[head];
113
+ head += 1;
114
+ if (visitedPids.has(process.pid)) {
115
+ continue;
116
+ }
117
+ visitedPids.add(process.pid);
118
+ pushSessionId(process.sessionId);
119
+ for (const child of childrenByPpid.get(process.pid) ?? []) {
120
+ if (!visitedPids.has(child.pid)) {
121
+ queue.push(child);
122
+ }
123
+ }
93
124
  }
94
125
  return candidateSessionIds;
95
126
  };
@@ -1,14 +1,17 @@
1
1
  import { LiveSessionOutputActivity } from '../../domain/entities/LiveSessionOutputActivity';
2
2
  import { SessionOutputActivityRepository } from '../../domain/usecases/adapter-interfaces/SessionOutputActivityRepository';
3
3
  /**
4
- * Reads the last main-session output time for each live session from its
4
+ * Reads the last main-session activity time for each live session from its
5
5
  * already-resolved transcript path. Idle time is computed from the timestamp of
6
- * the latest `assistant` entry rather than from the transcript file modification
7
- * time, so a transcript touched only by tool results or owner replies still
8
- * counts as silent.
6
+ * the latest entry of any kind (assistant text, owner replies, tool results, or
7
+ * any other entry type) rather than from the transcript file modification time.
8
+ * Because a session that is actively running tool calls keeps appending entries
9
+ * such as `user` and `tool_result` even while it emits no assistant text, every
10
+ * entry with a parseable timestamp counts as activity, so a working session is
11
+ * not mistaken for a silent one.
9
12
  */
10
13
  export declare class FileSystemSessionOutputActivityRepository implements SessionOutputActivityRepository {
11
14
  listSessionOutputActivities: (transcriptPathBySessionName: Map<string, string>) => Promise<LiveSessionOutputActivity[]>;
12
- private readLastAssistantOutputEpochSeconds;
15
+ private readLastActivityEpochSeconds;
13
16
  }
14
17
  //# sourceMappingURL=FileSystemSessionOutputActivityRepository.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FileSystemSessionOutputActivityRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC5F,OAAO,EAAE,+BAA+B,EAAE,MAAM,0EAA0E,CAAC;AAqB3H;;;;;;GAMG;AACH,qBAAa,yCAA0C,YAAW,+BAA+B;IAC/F,2BAA2B,GACzB,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAUrC;IAEF,OAAO,CAAC,mCAAmC,CAuCzC;CACH"}
1
+ {"version":3,"file":"FileSystemSessionOutputActivityRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC5F,OAAO,EAAE,+BAA+B,EAAE,MAAM,0EAA0E,CAAC;AAqB3H;;;;;;;;;GASG;AACH,qBAAa,yCAA0C,YAAW,+BAA+B;IAC/F,2BAA2B,GACzB,6BAA6B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/C,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAUrC;IAEF,OAAO,CAAC,4BAA4B,CAoClC;CACH"}
@@ -18,6 +18,18 @@ export declare const OWNER_HANDOVER_COMMAND_MARKER = "Take ownership of";
18
18
  */
19
19
  export declare class ResolveInteractiveLiveSessionsUseCase {
20
20
  resolve: (snapshot: LiveSessionProcessSnapshot) => InteractiveLiveSession[];
21
+ /**
22
+ * Collects the distinct session ids that may name the actively-written
23
+ * transcript on disk, in priority order. The rotated current session id
24
+ * recorded for the interactive process is first, because for a resumed or
25
+ * compacted session the id rotates and the live transcript is named by the
26
+ * current id. The interactive process's own launch id follows, then the
27
+ * distinct ids propagated to its descendant processes. For a `--resume`
28
+ * session these ids coincide, yielding a single id; for a non-resume session
29
+ * the own (launch) id names no transcript and the live transcript is named by
30
+ * the descendant-propagated id, which is included here so the resolver can
31
+ * find the actively-written file.
32
+ */
21
33
  private collectCandidateSessionIds;
22
34
  private findInteractiveProcess;
23
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ResolveInteractiveLiveSessionsUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAEL,0BAA0B,EAC3B,MAAM,wCAAwC,CAAC;AAOhD,eAAO,MAAM,6BAA6B,sBAAsB,CAAC;AAejE;;;;;;;;;;;;;;GAcG;AACH,qBAAa,qCAAqC;IAChD,OAAO,GACL,UAAU,0BAA0B,KACnC,sBAAsB,EAAE,CAiCzB;IAEF,OAAO,CAAC,0BAA0B,CAehC;IAEF,OAAO,CAAC,sBAAsB,CA0B5B;CACH"}
1
+ {"version":3,"file":"ResolveInteractiveLiveSessionsUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAEL,0BAA0B,EAC3B,MAAM,wCAAwC,CAAC;AAOhD,eAAO,MAAM,6BAA6B,sBAAsB,CAAC;AAejE;;;;;;;;;;;;;;GAcG;AACH,qBAAa,qCAAqC;IAChD,OAAO,GACL,UAAU,0BAA0B,KACnC,sBAAsB,EAAE,CAoCzB;IAEF;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,0BAA0B,CA+BhC;IAEF,OAAO,CAAC,sBAAsB,CA0B5B;CACH"}