crewly 1.20.67 → 1.20.69
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/config/skills/agent/core/drive-read/SKILL.md +1 -10
- package/config/skills/agent/core/drive-search/SKILL.md +0 -9
- package/config/skills/agent/core/drive-upload/SKILL.md +0 -9
- package/config/skills/agent/core/google-connect/SKILL.md +44 -0
- package/config/skills/agent/core/google-connect/execute.sh +49 -0
- package/dist/backend/backend/src/constants.d.ts +53 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +53 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/google/google-connect-card.d.ts +64 -0
- package/dist/backend/backend/src/controllers/google/google-connect-card.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/google/google-connect-card.js +122 -0
- package/dist/backend/backend/src/controllers/google/google-connect-card.js.map +1 -0
- package/dist/backend/backend/src/controllers/google/google.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.routes.js +4 -0
- package/dist/backend/backend/src/controllers/google/google.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.js +6 -19
- package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts +16 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +32 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +17 -0
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +57 -9
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts +23 -0
- package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/google/google-workspace-token.service.js +22 -0
- package/dist/backend/backend/src/services/google/google-workspace-token.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +23 -0
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +50 -0
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.d.ts +11 -0
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.js +13 -0
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.js +62 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts +17 -0
- package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.js +41 -0
- package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts +28 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.js +66 -30
- package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts +11 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.js +68 -18
- package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +53 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +53 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +17 -0
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +57 -9
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +23 -0
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +50 -0
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/cli/backend/src/services/monitoring/activity-monitor.service.d.ts +162 -0
- package/dist/cli/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/monitoring/activity-monitor.service.js +708 -0
- package/dist/cli/backend/src/services/monitoring/activity-monitor.service.js.map +1 -0
- package/dist/cli/backend/src/services/monitoring/runtime-token-parser.service.d.ts +73 -0
- package/dist/cli/backend/src/services/monitoring/runtime-token-parser.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/monitoring/runtime-token-parser.service.js +199 -0
- package/dist/cli/backend/src/services/monitoring/runtime-token-parser.service.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.d.ts +11 -0
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.js +13 -0
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts +17 -0
- package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.js +41 -0
- package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { StorageService } from '../core/storage.service.js';
|
|
2
|
+
import { getSessionBackendSync, createSessionBackend } from '../session/index.js';
|
|
3
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
4
|
+
import { AgentHeartbeatService } from '../agent/agent-heartbeat.service.js';
|
|
5
|
+
import { writeFile, readFile, rename, unlink } from 'fs/promises';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { getCrewlyHomePath } from '../core/crewly-home.utils.js';
|
|
9
|
+
import { CREWLY_CONSTANTS, CONTINUATION_CONSTANTS, AGENT_IDENTITY_CONSTANTS, PTY_CONSTANTS, ACTIVITY_MONITOR_CONSTANTS, RUNTIME_TYPES } from '../../constants.js';
|
|
10
|
+
import { stripAnsiCodes } from '../../utils/terminal-output.utils.js';
|
|
11
|
+
import { PtyActivityTrackerService } from '../agent/pty-activity-tracker.service.js';
|
|
12
|
+
import { TokenUsageService } from '../monitoring/token-usage.service.js';
|
|
13
|
+
import { parseRuntimeTokens } from '../monitoring/runtime-token-parser.service.js';
|
|
14
|
+
import { getSettingsService } from '../settings/settings.service.js';
|
|
15
|
+
/**
|
|
16
|
+
* Activity Monitor Service - NEW ARCHITECTURE
|
|
17
|
+
*
|
|
18
|
+
* Responsibilities:
|
|
19
|
+
* - Track ONLY workingStatus (idle vs in_progress) based on terminal activity
|
|
20
|
+
* - Create and manage teamWorkingStatus.json file
|
|
21
|
+
* - Integrate with AgentHeartbeatService for stale agent detection
|
|
22
|
+
* - Remove ALL agentStatus management (now owned by AgentHeartbeatService)
|
|
23
|
+
*
|
|
24
|
+
* Key Changes:
|
|
25
|
+
* - No more agentStatus updates to teams.json
|
|
26
|
+
* - Simple activity detection: output changed = in_progress, else = idle
|
|
27
|
+
* - Integration with stale detection (30-minute threshold)
|
|
28
|
+
* - Separate teamWorkingStatus.json file for working statuses
|
|
29
|
+
*/
|
|
30
|
+
export class ActivityMonitorService {
|
|
31
|
+
static instance;
|
|
32
|
+
logger;
|
|
33
|
+
storageService;
|
|
34
|
+
_sessionBackend = null;
|
|
35
|
+
agentHeartbeatService;
|
|
36
|
+
intervalId = null;
|
|
37
|
+
POLLING_INTERVAL = ACTIVITY_MONITOR_CONSTANTS.POLLING_INTERVAL_MS;
|
|
38
|
+
lastTerminalOutputs = new Map();
|
|
39
|
+
MAX_OUTPUT_SIZE = 512; // 512 bytes max per output
|
|
40
|
+
ACTIVITY_CHECK_TIMEOUT = 6000; // 6 second timeout per check
|
|
41
|
+
lastCleanup = Date.now();
|
|
42
|
+
crewlyHome;
|
|
43
|
+
teamWorkingStatusFile;
|
|
44
|
+
eventBusService = null;
|
|
45
|
+
/** Tracks when each session entered in_progress (epoch ms) */
|
|
46
|
+
busyTransitionTimestamps = new Map();
|
|
47
|
+
/** Tracks which sessions have had their agent:busy event emitted */
|
|
48
|
+
busyEventEmitted = new Set();
|
|
49
|
+
/** Tracks last recorded token counts per session to avoid duplicate recording */
|
|
50
|
+
lastRecordedTokens = new Map();
|
|
51
|
+
/** Cached tokenTracking setting, refreshed periodically to avoid async reads in hot path */
|
|
52
|
+
tokenTrackingEnabled = false;
|
|
53
|
+
tokenTrackingLastCheck = 0;
|
|
54
|
+
constructor() {
|
|
55
|
+
this.logger = LoggerService.getInstance().createComponentLogger('ActivityMonitor');
|
|
56
|
+
this.storageService = StorageService.getInstance();
|
|
57
|
+
this.agentHeartbeatService = AgentHeartbeatService.getInstance();
|
|
58
|
+
this.crewlyHome = getCrewlyHomePath();
|
|
59
|
+
this.teamWorkingStatusFile = join(this.crewlyHome, 'teamWorkingStatus.json');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get the session backend, lazily initializing if needed.
|
|
63
|
+
*/
|
|
64
|
+
async getBackend() {
|
|
65
|
+
if (!this._sessionBackend) {
|
|
66
|
+
this._sessionBackend = getSessionBackendSync();
|
|
67
|
+
if (!this._sessionBackend) {
|
|
68
|
+
this._sessionBackend = await createSessionBackend('pty');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return this._sessionBackend;
|
|
72
|
+
}
|
|
73
|
+
static getInstance() {
|
|
74
|
+
if (!ActivityMonitorService.instance) {
|
|
75
|
+
ActivityMonitorService.instance = new ActivityMonitorService();
|
|
76
|
+
}
|
|
77
|
+
return ActivityMonitorService.instance;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Inject the EventBusService for direct event publishing on status transitions.
|
|
81
|
+
*
|
|
82
|
+
* @param service - The EventBusService instance
|
|
83
|
+
*/
|
|
84
|
+
setEventBusService(service) {
|
|
85
|
+
this.eventBusService = service;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build an AgentEvent for a working status transition.
|
|
89
|
+
*
|
|
90
|
+
* @param type - The event type ('agent:busy' or 'agent:idle')
|
|
91
|
+
* @param timestamp - ISO timestamp for the event
|
|
92
|
+
* @param identity - Agent identity fields (team/member/session)
|
|
93
|
+
* @param previousValue - The previous working status
|
|
94
|
+
* @param newValue - The new working status
|
|
95
|
+
* @returns A fully-formed AgentEvent
|
|
96
|
+
*/
|
|
97
|
+
buildAgentEvent(type, timestamp, identity, previousValue, newValue) {
|
|
98
|
+
return {
|
|
99
|
+
id: crypto.randomUUID(),
|
|
100
|
+
type,
|
|
101
|
+
timestamp,
|
|
102
|
+
teamId: identity.teamId,
|
|
103
|
+
teamName: identity.teamName,
|
|
104
|
+
memberId: identity.memberId,
|
|
105
|
+
memberName: identity.memberName,
|
|
106
|
+
sessionName: identity.sessionName,
|
|
107
|
+
previousValue,
|
|
108
|
+
newValue,
|
|
109
|
+
changedField: 'workingStatus',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Handle duration-gated event emission for a session status transition.
|
|
114
|
+
* Suppresses spurious idle→busy→idle cycles by requiring a minimum
|
|
115
|
+
* busy duration (MIN_BUSY_DURATION_MS) before emitting events.
|
|
116
|
+
*
|
|
117
|
+
* @param key - Unique key for tracking (e.g. 'orchestrator' or session name)
|
|
118
|
+
* @param newStatus - The new working status
|
|
119
|
+
* @param previousStatus - The previous working status
|
|
120
|
+
* @param statusChanged - Whether the status actually changed this poll
|
|
121
|
+
* @param identity - Agent identity fields for building the event
|
|
122
|
+
* @param now - ISO timestamp for the event
|
|
123
|
+
*/
|
|
124
|
+
handleDurationGatedEmission(key, newStatus, previousStatus, statusChanged, identity, now) {
|
|
125
|
+
if (statusChanged && newStatus === 'in_progress') {
|
|
126
|
+
if (!this.busyTransitionTimestamps.has(key)) {
|
|
127
|
+
this.busyTransitionTimestamps.set(key, Date.now());
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (statusChanged) {
|
|
131
|
+
const busyStart = this.busyTransitionTimestamps.get(key);
|
|
132
|
+
const elapsed = busyStart != null ? Date.now() - busyStart : 0;
|
|
133
|
+
if (newStatus === 'in_progress') {
|
|
134
|
+
if (elapsed >= PTY_CONSTANTS.MIN_BUSY_DURATION_MS && !this.busyEventEmitted.has(key) && this.eventBusService) {
|
|
135
|
+
this.busyEventEmitted.add(key);
|
|
136
|
+
this.eventBusService.publish(this.buildAgentEvent('agent:busy', now, identity, previousStatus, newStatus));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
if (elapsed >= PTY_CONSTANTS.MIN_BUSY_DURATION_MS && this.busyEventEmitted.has(key) && this.eventBusService) {
|
|
141
|
+
// Generic event (back-compat for fleet dashboards / auto-claim / etc.)
|
|
142
|
+
this.eventBusService.publish(this.buildAgentEvent('agent:idle', now, identity, previousStatus, newStatus));
|
|
143
|
+
// 2026-05-20 follow-up: the in-process busy guard above (`busyEventEmitted`)
|
|
144
|
+
// guarantees we only reach this branch when the agent was actually
|
|
145
|
+
// observed busy first — so this transition is unambiguously
|
|
146
|
+
// post-task. Emit the precise variant so peer-watch triggers
|
|
147
|
+
// can filter on it without being woken by registration-idle.
|
|
148
|
+
this.eventBusService.publish(this.buildAgentEvent('agent:idle_after_task', now, identity, previousStatus, newStatus));
|
|
149
|
+
}
|
|
150
|
+
this.busyTransitionTimestamps.delete(key);
|
|
151
|
+
this.busyEventEmitted.delete(key);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (newStatus === 'in_progress' && !this.busyEventEmitted.has(key)) {
|
|
155
|
+
// Still in_progress across polls — seed timestamp if missing (service restart)
|
|
156
|
+
// and check if deferred busy event can now be emitted
|
|
157
|
+
if (!this.busyTransitionTimestamps.has(key)) {
|
|
158
|
+
this.busyTransitionTimestamps.set(key, Date.now());
|
|
159
|
+
}
|
|
160
|
+
const busyStart = this.busyTransitionTimestamps.get(key);
|
|
161
|
+
if (Date.now() - busyStart >= PTY_CONSTANTS.MIN_BUSY_DURATION_MS && this.eventBusService) {
|
|
162
|
+
this.busyEventEmitted.add(key);
|
|
163
|
+
this.eventBusService.publish(this.buildAgentEvent('agent:busy', now, identity, 'idle', 'in_progress'));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
startPolling() {
|
|
168
|
+
if (this.intervalId) {
|
|
169
|
+
this.logger.warn('Activity monitoring already running');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.logger.info(`Starting activity monitoring with ${this.POLLING_INTERVAL / 1000}s intervals (workingStatus only)`);
|
|
173
|
+
// Run immediately first
|
|
174
|
+
this.performActivityCheck();
|
|
175
|
+
// Set up recurring polling
|
|
176
|
+
this.intervalId = setInterval(() => {
|
|
177
|
+
this.performActivityCheck();
|
|
178
|
+
}, this.POLLING_INTERVAL);
|
|
179
|
+
}
|
|
180
|
+
stopPolling() {
|
|
181
|
+
if (this.intervalId) {
|
|
182
|
+
clearInterval(this.intervalId);
|
|
183
|
+
this.intervalId = null;
|
|
184
|
+
this.logger.info('Activity monitoring stopped');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async performActivityCheck() {
|
|
188
|
+
try {
|
|
189
|
+
// Add timeout protection for entire activity check
|
|
190
|
+
await Promise.race([
|
|
191
|
+
this.performActivityCheckInternal(),
|
|
192
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Activity check timeout')), this.ACTIVITY_CHECK_TIMEOUT))
|
|
193
|
+
]);
|
|
194
|
+
// Perform periodic cleanup
|
|
195
|
+
this.performPeriodicCleanup();
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.logger.error('Error during activity check', {
|
|
199
|
+
error: error instanceof Error ? error.message : String(error)
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* NEW ARCHITECTURE: Simplified activity checking
|
|
205
|
+
*
|
|
206
|
+
* This method now:
|
|
207
|
+
* 1. Detects stale agents via AgentHeartbeatService (30-minute threshold)
|
|
208
|
+
* 2. Checks terminal activity for working status (ONLY)
|
|
209
|
+
* 3. Updates teamWorkingStatus.json (separate from teams.json)
|
|
210
|
+
* 4. Removes ALL agentStatus logic (handled by AgentHeartbeatService)
|
|
211
|
+
*/
|
|
212
|
+
async performActivityCheckInternal() {
|
|
213
|
+
const now = new Date().toISOString();
|
|
214
|
+
try {
|
|
215
|
+
// Step 1: Detect stale agents and mark dead ones as inactive
|
|
216
|
+
const staleThreshold = CONTINUATION_CONSTANTS.DETECTION.STALE_THRESHOLD_MINUTES;
|
|
217
|
+
const staleAgents = await this.agentHeartbeatService.detectStaleAgents(staleThreshold);
|
|
218
|
+
if (staleAgents.length > 0) {
|
|
219
|
+
this.logger.debug('Detected stale agents for potential inactivity', {
|
|
220
|
+
staleAgents,
|
|
221
|
+
thresholdMinutes: staleThreshold
|
|
222
|
+
});
|
|
223
|
+
// Batch-check stale agents and mark dead ones as inactive.
|
|
224
|
+
// We read teams ONCE, update all dead members in memory, and save
|
|
225
|
+
// each modified team ONCE — instead of calling updateAgentStatus()
|
|
226
|
+
// per agent which does a full getTeams()+saveTeam() each time,
|
|
227
|
+
// flooding the libuv thread pool with I/O.
|
|
228
|
+
try {
|
|
229
|
+
const backend = await this.getBackend();
|
|
230
|
+
const heartbeats = await this.agentHeartbeatService.getAllAgentHeartbeats();
|
|
231
|
+
// Collect dead session names first
|
|
232
|
+
const deadSessions = new Set();
|
|
233
|
+
for (const agentId of staleAgents) {
|
|
234
|
+
let sessionName;
|
|
235
|
+
if (agentId === 'orchestrator') {
|
|
236
|
+
sessionName = heartbeats.orchestrator?.sessionName;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
sessionName = heartbeats.teamMembers[agentId]?.sessionName;
|
|
240
|
+
}
|
|
241
|
+
if (!sessionName)
|
|
242
|
+
continue;
|
|
243
|
+
// Skip orchestrator — its lifecycle is managed by OrchestratorHeartbeatMonitor,
|
|
244
|
+
// not the activity monitor. In-process runtimes (crewly-agent) have no PTY
|
|
245
|
+
// session, so sessionExists() would falsely return false.
|
|
246
|
+
if (agentId === AGENT_IDENTITY_CONSTANTS.ORCHESTRATOR.ID) {
|
|
247
|
+
this.logger.debug('Skipping orchestrator stale check (managed by OrchestratorHeartbeatMonitor)', {
|
|
248
|
+
sessionName,
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const sessionAlive = backend.sessionExists(sessionName);
|
|
253
|
+
if (!sessionAlive) {
|
|
254
|
+
this.logger.debug('Marking stale agent as inactive (session dead)', {
|
|
255
|
+
agentId,
|
|
256
|
+
sessionName
|
|
257
|
+
});
|
|
258
|
+
deadSessions.add(sessionName);
|
|
259
|
+
// Update heartbeat file so detectStaleAgents stops re-detecting this agent
|
|
260
|
+
await this.agentHeartbeatService.updateAgentHeartbeat(sessionName, agentId, CREWLY_CONSTANTS.AGENT_STATUSES.INACTIVE);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Batch-update team members: read teams once, save each modified team once
|
|
264
|
+
if (deadSessions.size > 0) {
|
|
265
|
+
const teams = await this.storageService.getTeams();
|
|
266
|
+
const modifiedTeams = new Set();
|
|
267
|
+
for (const team of teams) {
|
|
268
|
+
for (const member of team.members || []) {
|
|
269
|
+
if (deadSessions.has(member.sessionName)) {
|
|
270
|
+
member.agentStatus = CREWLY_CONSTANTS.AGENT_STATUSES.INACTIVE;
|
|
271
|
+
member.updatedAt = new Date().toISOString();
|
|
272
|
+
modifiedTeams.add(team.id);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Save each modified team once (sequentially to avoid lock contention)
|
|
277
|
+
for (const team of teams) {
|
|
278
|
+
if (modifiedTeams.has(team.id)) {
|
|
279
|
+
await this.storageService.saveTeam(team);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (cleanupError) {
|
|
285
|
+
this.logger.error('Error during stale agent cleanup', {
|
|
286
|
+
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Step 2: Load current working status file
|
|
291
|
+
const workingStatusData = await this.loadTeamWorkingStatusFile();
|
|
292
|
+
let hasChanges = false;
|
|
293
|
+
// Step 3: Check orchestrator working status
|
|
294
|
+
const backend = await this.getBackend();
|
|
295
|
+
const orchestratorRunning = await Promise.race([
|
|
296
|
+
Promise.resolve(backend.sessionExists(CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME)),
|
|
297
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Orchestrator check timeout')), 1000))
|
|
298
|
+
]).catch(() => false);
|
|
299
|
+
if (orchestratorRunning) {
|
|
300
|
+
const orchestratorOutput = await this.getTerminalOutput(CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME);
|
|
301
|
+
const previousOutput = this.lastTerminalOutputs.get('orchestrator') || '';
|
|
302
|
+
const outputChanged = orchestratorOutput !== previousOutput && orchestratorOutput.trim() !== '';
|
|
303
|
+
// Bridge to PtyActivityTracker so idle/heartbeat checks detect activity
|
|
304
|
+
// even when no WebSocket clients are connected
|
|
305
|
+
if (outputChanged) {
|
|
306
|
+
PtyActivityTrackerService.getInstance().recordActivity(CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME);
|
|
307
|
+
}
|
|
308
|
+
let newWorkingStatus = outputChanged ? 'in_progress' : 'idle';
|
|
309
|
+
const orchKey = 'orchestrator';
|
|
310
|
+
const previousStatus = workingStatusData.orchestrator.workingStatus;
|
|
311
|
+
// Auto-reset: if in_progress for longer than MAX_IN_PROGRESS_MS, force idle.
|
|
312
|
+
// Prevents workingStatus from getting stuck due to continuous terminal output
|
|
313
|
+
// (spinners, TUI re-renders, long-running tool calls).
|
|
314
|
+
if (newWorkingStatus === 'in_progress') {
|
|
315
|
+
let busySince = this.busyTransitionTimestamps.get(orchKey);
|
|
316
|
+
if (!busySince) {
|
|
317
|
+
// Seed timestamp when status is already in_progress but no transition was
|
|
318
|
+
// observed (e.g. service restart with pre-existing in_progress in the file).
|
|
319
|
+
// Use updatedAt from the file if available, otherwise fall back to now.
|
|
320
|
+
const fileUpdatedAt = Date.parse(workingStatusData.orchestrator.updatedAt);
|
|
321
|
+
busySince = (previousStatus === 'in_progress' && !isNaN(fileUpdatedAt)) ? fileUpdatedAt : Date.now();
|
|
322
|
+
this.busyTransitionTimestamps.set(orchKey, busySince);
|
|
323
|
+
}
|
|
324
|
+
if ((Date.now() - busySince) > ACTIVITY_MONITOR_CONSTANTS.MAX_IN_PROGRESS_MS) {
|
|
325
|
+
this.logger.info('Auto-resetting orchestrator workingStatus from in_progress to idle (exceeded max duration)', {
|
|
326
|
+
durationMs: Date.now() - busySince,
|
|
327
|
+
maxMs: ACTIVITY_MONITOR_CONSTANTS.MAX_IN_PROGRESS_MS,
|
|
328
|
+
});
|
|
329
|
+
newWorkingStatus = 'idle';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const statusChanged = previousStatus !== newWorkingStatus;
|
|
333
|
+
if (statusChanged) {
|
|
334
|
+
workingStatusData.orchestrator.workingStatus = newWorkingStatus;
|
|
335
|
+
workingStatusData.orchestrator.lastActivityCheck = now;
|
|
336
|
+
workingStatusData.orchestrator.updatedAt = now;
|
|
337
|
+
hasChanges = true;
|
|
338
|
+
this.logger.info('Orchestrator working status updated', {
|
|
339
|
+
sessionName: CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME,
|
|
340
|
+
newWorkingStatus,
|
|
341
|
+
outputChanged
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
this.handleDurationGatedEmission(orchKey, newWorkingStatus, previousStatus, statusChanged, {
|
|
345
|
+
teamId: 'orchestrator',
|
|
346
|
+
teamName: 'Orchestrator',
|
|
347
|
+
memberId: AGENT_IDENTITY_CONSTANTS.ORCHESTRATOR.ID,
|
|
348
|
+
memberName: 'Orchestrator',
|
|
349
|
+
sessionName: CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME,
|
|
350
|
+
}, now);
|
|
351
|
+
this.lastTerminalOutputs.set('orchestrator', orchestratorOutput);
|
|
352
|
+
// Track orchestrator PTY token usage (same as team members)
|
|
353
|
+
this.tryRecordPtyTokenUsage(CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME, orchestratorOutput, 'claude-code');
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
// Orchestrator not running, set to idle
|
|
357
|
+
if (workingStatusData.orchestrator.workingStatus !== 'idle') {
|
|
358
|
+
workingStatusData.orchestrator.workingStatus = 'idle';
|
|
359
|
+
workingStatusData.orchestrator.lastActivityCheck = now;
|
|
360
|
+
workingStatusData.orchestrator.updatedAt = now;
|
|
361
|
+
hasChanges = true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Step 4: Check team member working statuses
|
|
365
|
+
const teams = await this.storageService.getTeams();
|
|
366
|
+
for (const team of teams) {
|
|
367
|
+
for (const member of team.members) {
|
|
368
|
+
if (member.sessionName) {
|
|
369
|
+
try {
|
|
370
|
+
// Check if session exists
|
|
371
|
+
const sessionExists = await Promise.race([
|
|
372
|
+
Promise.resolve(backend.sessionExists(member.sessionName)),
|
|
373
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Session check timeout')), 500))
|
|
374
|
+
]).catch(() => false);
|
|
375
|
+
if (!sessionExists) {
|
|
376
|
+
// Session doesn't exist, set to idle
|
|
377
|
+
const memberKey = member.sessionName;
|
|
378
|
+
if (!workingStatusData.teamMembers[memberKey]) {
|
|
379
|
+
workingStatusData.teamMembers[memberKey] = {
|
|
380
|
+
sessionName: member.sessionName,
|
|
381
|
+
teamMemberId: member.id,
|
|
382
|
+
workingStatus: 'idle',
|
|
383
|
+
lastActivityCheck: now,
|
|
384
|
+
updatedAt: now
|
|
385
|
+
};
|
|
386
|
+
hasChanges = true;
|
|
387
|
+
}
|
|
388
|
+
else if (workingStatusData.teamMembers[memberKey].workingStatus !== 'idle') {
|
|
389
|
+
workingStatusData.teamMembers[memberKey].workingStatus = 'idle';
|
|
390
|
+
workingStatusData.teamMembers[memberKey].lastActivityCheck = now;
|
|
391
|
+
workingStatusData.teamMembers[memberKey].updatedAt = now;
|
|
392
|
+
hasChanges = true;
|
|
393
|
+
}
|
|
394
|
+
this.lastTerminalOutputs.delete(memberKey);
|
|
395
|
+
this.busyTransitionTimestamps.delete(memberKey);
|
|
396
|
+
this.busyEventEmitted.delete(memberKey);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
// Get terminal output and check for activity
|
|
400
|
+
const currentOutput = await this.getTerminalOutput(member.sessionName);
|
|
401
|
+
const previousOutput = this.lastTerminalOutputs.get(member.sessionName) || '';
|
|
402
|
+
let outputChanged = currentOutput !== previousOutput && currentOutput.trim() !== '';
|
|
403
|
+
// Gemini CLI TUI re-renders in place, so bottom-N line comparison
|
|
404
|
+
// often shows no change even while the agent is actively working.
|
|
405
|
+
// Fall back to raw PTY activity timestamp as an additional signal.
|
|
406
|
+
if (!outputChanged && member.runtimeType === RUNTIME_TYPES.GEMINI_CLI) {
|
|
407
|
+
const tracker = PtyActivityTrackerService.getInstance();
|
|
408
|
+
const idleMs = tracker.getIdleTimeMs(member.sessionName);
|
|
409
|
+
if (idleMs > 0 && idleMs < ACTIVITY_MONITOR_CONSTANTS.POLLING_INTERVAL_MS) {
|
|
410
|
+
outputChanged = true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
let newWorkingStatus = outputChanged ? 'in_progress' : 'idle';
|
|
414
|
+
// Auto-reset: if in_progress for longer than MAX_IN_PROGRESS_MS, force idle
|
|
415
|
+
const memberKey = member.sessionName;
|
|
416
|
+
if (newWorkingStatus === 'in_progress') {
|
|
417
|
+
let busySince = this.busyTransitionTimestamps.get(memberKey);
|
|
418
|
+
if (!busySince) {
|
|
419
|
+
// Seed timestamp when status is already in_progress but no transition was
|
|
420
|
+
// observed (e.g. service restart with pre-existing in_progress in the file).
|
|
421
|
+
const existingEntry = workingStatusData.teamMembers[memberKey];
|
|
422
|
+
const fileUpdatedAt = existingEntry ? Date.parse(existingEntry.updatedAt) : NaN;
|
|
423
|
+
const previousMemberStatus = existingEntry?.workingStatus;
|
|
424
|
+
busySince = (previousMemberStatus === 'in_progress' && !isNaN(fileUpdatedAt)) ? fileUpdatedAt : Date.now();
|
|
425
|
+
this.busyTransitionTimestamps.set(memberKey, busySince);
|
|
426
|
+
}
|
|
427
|
+
if ((Date.now() - busySince) > ACTIVITY_MONITOR_CONSTANTS.MAX_IN_PROGRESS_MS) {
|
|
428
|
+
this.logger.info('Auto-resetting member workingStatus from in_progress to idle (exceeded max duration)', {
|
|
429
|
+
sessionName: member.sessionName,
|
|
430
|
+
durationMs: Date.now() - busySince,
|
|
431
|
+
maxMs: ACTIVITY_MONITOR_CONSTANTS.MAX_IN_PROGRESS_MS,
|
|
432
|
+
});
|
|
433
|
+
newWorkingStatus = 'idle';
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Update working status if changed
|
|
437
|
+
if (!workingStatusData.teamMembers[memberKey]) {
|
|
438
|
+
workingStatusData.teamMembers[memberKey] = {
|
|
439
|
+
sessionName: member.sessionName,
|
|
440
|
+
teamMemberId: member.id,
|
|
441
|
+
workingStatus: newWorkingStatus,
|
|
442
|
+
lastActivityCheck: now,
|
|
443
|
+
updatedAt: now
|
|
444
|
+
};
|
|
445
|
+
hasChanges = true;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
const previousMemberStatus = workingStatusData.teamMembers[memberKey].workingStatus;
|
|
449
|
+
const memberStatusChanged = previousMemberStatus !== newWorkingStatus;
|
|
450
|
+
if (memberStatusChanged) {
|
|
451
|
+
workingStatusData.teamMembers[memberKey].workingStatus = newWorkingStatus;
|
|
452
|
+
workingStatusData.teamMembers[memberKey].lastActivityCheck = now;
|
|
453
|
+
workingStatusData.teamMembers[memberKey].updatedAt = now;
|
|
454
|
+
hasChanges = true;
|
|
455
|
+
this.logger.info('Team member working status updated', {
|
|
456
|
+
teamId: team.id,
|
|
457
|
+
memberId: member.id,
|
|
458
|
+
memberName: member.name,
|
|
459
|
+
sessionName: member.sessionName,
|
|
460
|
+
newWorkingStatus,
|
|
461
|
+
outputChanged
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
this.handleDurationGatedEmission(memberKey, newWorkingStatus, previousMemberStatus, memberStatusChanged, {
|
|
465
|
+
teamId: team.id,
|
|
466
|
+
teamName: team.name,
|
|
467
|
+
memberId: member.id,
|
|
468
|
+
memberName: member.name,
|
|
469
|
+
sessionName: member.sessionName,
|
|
470
|
+
}, now);
|
|
471
|
+
}
|
|
472
|
+
this.lastTerminalOutputs.set(member.sessionName, currentOutput);
|
|
473
|
+
// Token usage tracking: parse PTY output for token data from CLI runtimes
|
|
474
|
+
this.tryRecordPtyTokenUsage(member.sessionName, currentOutput, member.runtimeType);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
this.logger.error('Error checking member working status', {
|
|
478
|
+
teamId: team.id,
|
|
479
|
+
memberId: member.id,
|
|
480
|
+
memberName: member.name,
|
|
481
|
+
sessionName: member.sessionName,
|
|
482
|
+
error: error instanceof Error ? error.message : String(error)
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Step 5: Save changes if any
|
|
489
|
+
if (hasChanges) {
|
|
490
|
+
workingStatusData.metadata.lastUpdated = now;
|
|
491
|
+
await this.saveTeamWorkingStatusFile(workingStatusData);
|
|
492
|
+
this.logger.debug('Updated teamWorkingStatus.json with activity changes');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
this.logger.error('Error during activity check', {
|
|
497
|
+
error: error instanceof Error ? error.message : String(error)
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Load teamWorkingStatus.json file with proper initialization
|
|
503
|
+
*/
|
|
504
|
+
async loadTeamWorkingStatusFile() {
|
|
505
|
+
try {
|
|
506
|
+
if (!existsSync(this.teamWorkingStatusFile)) {
|
|
507
|
+
const defaultData = this.createDefaultTeamWorkingStatusFile();
|
|
508
|
+
await this.saveTeamWorkingStatusFile(defaultData);
|
|
509
|
+
this.logger.info('Created new teamWorkingStatus.json file');
|
|
510
|
+
return defaultData;
|
|
511
|
+
}
|
|
512
|
+
const content = await readFile(this.teamWorkingStatusFile, 'utf-8');
|
|
513
|
+
const data = JSON.parse(content);
|
|
514
|
+
// Validate structure
|
|
515
|
+
if (!data.orchestrator || !data.teamMembers || !data.metadata) {
|
|
516
|
+
this.logger.warn('Invalid teamWorkingStatus.json structure, reinitializing');
|
|
517
|
+
const defaultData = this.createDefaultTeamWorkingStatusFile();
|
|
518
|
+
await this.saveTeamWorkingStatusFile(defaultData);
|
|
519
|
+
return defaultData;
|
|
520
|
+
}
|
|
521
|
+
return data;
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
this.logger.error('Failed to load teamWorkingStatus.json, creating new file', {
|
|
525
|
+
error: error instanceof Error ? error.message : String(error)
|
|
526
|
+
});
|
|
527
|
+
const defaultData = this.createDefaultTeamWorkingStatusFile();
|
|
528
|
+
await this.saveTeamWorkingStatusFile(defaultData);
|
|
529
|
+
return defaultData;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Save teamWorkingStatus.json file with atomic write
|
|
534
|
+
*/
|
|
535
|
+
async saveTeamWorkingStatusFile(data) {
|
|
536
|
+
const content = JSON.stringify(data, null, 2);
|
|
537
|
+
const tempFile = `${this.teamWorkingStatusFile}.tmp`;
|
|
538
|
+
try {
|
|
539
|
+
// Write to temp file first, then rename (atomic operation)
|
|
540
|
+
await writeFile(tempFile, content, 'utf-8');
|
|
541
|
+
await rename(tempFile, this.teamWorkingStatusFile);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
// Clean up temp file if something went wrong
|
|
545
|
+
try {
|
|
546
|
+
await unlink(tempFile);
|
|
547
|
+
}
|
|
548
|
+
catch { }
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Create default teamWorkingStatus.json structure
|
|
554
|
+
*/
|
|
555
|
+
createDefaultTeamWorkingStatusFile() {
|
|
556
|
+
const now = new Date().toISOString();
|
|
557
|
+
return {
|
|
558
|
+
orchestrator: {
|
|
559
|
+
sessionName: CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME,
|
|
560
|
+
workingStatus: 'idle',
|
|
561
|
+
lastActivityCheck: now,
|
|
562
|
+
updatedAt: now
|
|
563
|
+
},
|
|
564
|
+
teamMembers: {},
|
|
565
|
+
metadata: {
|
|
566
|
+
lastUpdated: now,
|
|
567
|
+
version: '1.0.0'
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Cleanup old terminal outputs and perform garbage collection
|
|
573
|
+
*/
|
|
574
|
+
performPeriodicCleanup() {
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
// Clean up every 2 minutes
|
|
577
|
+
if (now - this.lastCleanup > 2 * 60 * 1000) {
|
|
578
|
+
// Limit the size of lastTerminalOutputs Map to prevent memory leaks
|
|
579
|
+
if (this.lastTerminalOutputs.size > 50) {
|
|
580
|
+
const entries = Array.from(this.lastTerminalOutputs.entries());
|
|
581
|
+
this.lastTerminalOutputs.clear();
|
|
582
|
+
// Keep only the most recent 25 entries
|
|
583
|
+
const recentEntries = entries.slice(-25);
|
|
584
|
+
for (const [key, value] of recentEntries) {
|
|
585
|
+
this.lastTerminalOutputs.set(key, value);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
this.lastCleanup = now;
|
|
589
|
+
// Force garbage collection if available
|
|
590
|
+
if (global.gc) {
|
|
591
|
+
global.gc();
|
|
592
|
+
this.logger.debug('Performed periodic cleanup with garbage collection', {
|
|
593
|
+
mapSize: this.lastTerminalOutputs.size
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
this.logger.debug('Performed periodic cleanup', {
|
|
598
|
+
mapSize: this.lastTerminalOutputs.size
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Attempt to parse and record token usage from PTY terminal output.
|
|
605
|
+
*
|
|
606
|
+
* Only records when token tracking is enabled in settings and when the
|
|
607
|
+
* parsed token counts differ from the last recorded values (to avoid
|
|
608
|
+
* duplicate recording from the same output being captured multiple times).
|
|
609
|
+
*
|
|
610
|
+
* @param sessionName - Agent session name
|
|
611
|
+
* @param output - Captured terminal output
|
|
612
|
+
* @param runtimeType - Runtime type hint for parser selection
|
|
613
|
+
*/
|
|
614
|
+
tryRecordPtyTokenUsage(sessionName, output, runtimeType) {
|
|
615
|
+
// Skip non-PTY runtimes (crewly-agent records via SDK, not PTY output)
|
|
616
|
+
if (!runtimeType || runtimeType === 'crewly-agent')
|
|
617
|
+
return;
|
|
618
|
+
try {
|
|
619
|
+
// Refresh cached tokenTracking setting every 60 seconds
|
|
620
|
+
const now = Date.now();
|
|
621
|
+
if (now - this.tokenTrackingLastCheck > 60_000) {
|
|
622
|
+
this.tokenTrackingLastCheck = now;
|
|
623
|
+
getSettingsService().getSettings().then(s => {
|
|
624
|
+
this.tokenTrackingEnabled = s.general.tokenTracking ?? false;
|
|
625
|
+
}).catch(() => { });
|
|
626
|
+
}
|
|
627
|
+
if (!this.tokenTrackingEnabled)
|
|
628
|
+
return;
|
|
629
|
+
const parsed = parseRuntimeTokens(output, runtimeType);
|
|
630
|
+
if (!parsed)
|
|
631
|
+
return;
|
|
632
|
+
// Dedup: skip if we already recorded these exact counts for this session
|
|
633
|
+
const last = this.lastRecordedTokens.get(sessionName);
|
|
634
|
+
if (last && last.input === parsed.inputTokens && last.output === parsed.outputTokens) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
// Record delta if we have a previous reading, otherwise record full amount
|
|
638
|
+
const deltaInput = last ? Math.max(0, parsed.inputTokens - last.input) : parsed.inputTokens;
|
|
639
|
+
const deltaOutput = last ? Math.max(0, parsed.outputTokens - last.output) : parsed.outputTokens;
|
|
640
|
+
if (deltaInput > 0 || deltaOutput > 0) {
|
|
641
|
+
const model = parsed.model ?? `${runtimeType}-default`;
|
|
642
|
+
TokenUsageService.getInstance().recordUsage(sessionName, sessionName, deltaInput, deltaOutput, model);
|
|
643
|
+
this.lastRecordedTokens.set(sessionName, {
|
|
644
|
+
input: parsed.inputTokens,
|
|
645
|
+
output: parsed.outputTokens,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
// Non-critical — don't let token parsing errors affect activity monitoring
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Get terminal output with size limits for activity detection
|
|
655
|
+
*/
|
|
656
|
+
async getTerminalOutput(sessionName) {
|
|
657
|
+
try {
|
|
658
|
+
const backend = await this.getBackend();
|
|
659
|
+
const output = await Promise.race([
|
|
660
|
+
Promise.resolve(backend.captureOutput(sessionName, 5)), // 5 lines only
|
|
661
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Capture timeout')), 500))
|
|
662
|
+
]);
|
|
663
|
+
// Strip ANSI escape codes so TUI re-renders (spinners, cursor
|
|
664
|
+
// repositioning) don't register as meaningful output changes.
|
|
665
|
+
const cleaned = stripAnsiCodes(output).trim();
|
|
666
|
+
// Limit output size to prevent memory issues
|
|
667
|
+
return cleaned.length > this.MAX_OUTPUT_SIZE
|
|
668
|
+
? cleaned.substring(cleaned.length - this.MAX_OUTPUT_SIZE)
|
|
669
|
+
: cleaned;
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
return '';
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
isRunning() {
|
|
676
|
+
return this.intervalId !== null;
|
|
677
|
+
}
|
|
678
|
+
getPollingInterval() {
|
|
679
|
+
return this.POLLING_INTERVAL;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Get current team working status data
|
|
683
|
+
* Useful for external services that need to check working statuses
|
|
684
|
+
*/
|
|
685
|
+
async getTeamWorkingStatus() {
|
|
686
|
+
return await this.loadTeamWorkingStatusFile();
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Get working status for a specific session
|
|
690
|
+
*/
|
|
691
|
+
async getWorkingStatusForSession(sessionName) {
|
|
692
|
+
try {
|
|
693
|
+
const data = await this.loadTeamWorkingStatusFile();
|
|
694
|
+
if (sessionName === CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME) {
|
|
695
|
+
return data.orchestrator.workingStatus;
|
|
696
|
+
}
|
|
697
|
+
return data.teamMembers[sessionName]?.workingStatus || null;
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
this.logger.error('Failed to get working status for session', {
|
|
701
|
+
sessionName,
|
|
702
|
+
error: error instanceof Error ? error.message : String(error)
|
|
703
|
+
});
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
//# sourceMappingURL=activity-monitor.service.js.map
|