crewly 1.4.80 → 1.4.82

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.
Files changed (52) hide show
  1. package/dist/backend/backend/src/constants.d.ts +24 -0
  2. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  3. package/dist/backend/backend/src/constants.js +24 -0
  4. package/dist/backend/backend/src/constants.js.map +1 -1
  5. package/dist/backend/backend/src/index.d.ts +1 -0
  6. package/dist/backend/backend/src/index.d.ts.map +1 -1
  7. package/dist/backend/backend/src/index.js +88 -1
  8. package/dist/backend/backend/src/index.js.map +1 -1
  9. package/dist/backend/backend/src/services/messaging/index.d.ts +1 -0
  10. package/dist/backend/backend/src/services/messaging/index.d.ts.map +1 -1
  11. package/dist/backend/backend/src/services/messaging/index.js +1 -0
  12. package/dist/backend/backend/src/services/messaging/index.js.map +1 -1
  13. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts +9 -0
  14. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
  15. package/dist/backend/backend/src/services/messaging/queue-processor.service.js +27 -0
  16. package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
  17. package/dist/backend/backend/src/services/messaging/response-router.service.d.ts +18 -0
  18. package/dist/backend/backend/src/services/messaging/response-router.service.d.ts.map +1 -1
  19. package/dist/backend/backend/src/services/messaging/response-router.service.js +62 -0
  20. package/dist/backend/backend/src/services/messaging/response-router.service.js.map +1 -1
  21. package/dist/backend/backend/src/services/messaging/thread-status-queue.service.d.ts +197 -0
  22. package/dist/backend/backend/src/services/messaging/thread-status-queue.service.d.ts.map +1 -0
  23. package/dist/backend/backend/src/services/messaging/thread-status-queue.service.js +458 -0
  24. package/dist/backend/backend/src/services/messaging/thread-status-queue.service.js.map +1 -0
  25. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +9 -0
  26. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  27. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +59 -1
  28. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  29. package/dist/backend/backend/src/types/index.d.ts +1 -0
  30. package/dist/backend/backend/src/types/index.d.ts.map +1 -1
  31. package/dist/backend/backend/src/types/index.js +2 -0
  32. package/dist/backend/backend/src/types/index.js.map +1 -1
  33. package/dist/backend/backend/src/types/thread-status.types.d.ts +165 -0
  34. package/dist/backend/backend/src/types/thread-status.types.d.ts.map +1 -0
  35. package/dist/backend/backend/src/types/thread-status.types.js +105 -0
  36. package/dist/backend/backend/src/types/thread-status.types.js.map +1 -0
  37. package/dist/cli/backend/src/constants.d.ts +24 -0
  38. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  39. package/dist/cli/backend/src/constants.js +24 -0
  40. package/dist/cli/backend/src/constants.js.map +1 -1
  41. package/dist/cli/backend/src/types/index.d.ts +1 -0
  42. package/dist/cli/backend/src/types/index.d.ts.map +1 -1
  43. package/dist/cli/backend/src/types/index.js +2 -0
  44. package/dist/cli/backend/src/types/index.js.map +1 -1
  45. package/dist/cli/backend/src/types/thread-status.types.d.ts +165 -0
  46. package/dist/cli/backend/src/types/thread-status.types.d.ts.map +1 -0
  47. package/dist/cli/backend/src/types/thread-status.types.js +105 -0
  48. package/dist/cli/backend/src/types/thread-status.types.js.map +1 -0
  49. package/frontend/dist/assets/{index-e2a673d6.css → index-975ccc95.css} +1 -1
  50. package/frontend/dist/assets/{index-e830dc67.js → index-d28d1135.js} +331 -331
  51. package/frontend/dist/index.html +2 -2
  52. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"response-router.service.js","sourceRoot":"","sources":["../../../../../../backend/src/services/messaging/response-router.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"response-router.service.js","sourceRoot":"","sources":["../../../../../../backend/src/services/messaging/response-router.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Thread Status Queue Service
3
+ *
4
+ * Tracks the lifecycle of every inbound message thread across platforms
5
+ * (Slack, GChat, Telegram, etc.). On orchestrator restart, non-terminal
6
+ * threads are recovered and re-enqueued so no user messages are silently dropped.
7
+ *
8
+ * Persistence is debounced and written to ~/.crewly/thread-status-queue.json
9
+ * using atomic file I/O to prevent corruption.
10
+ *
11
+ * @module services/messaging/thread-status-queue
12
+ */
13
+ import type { ThreadStatus, ThreadStatusEntry, TrackInboundInput, ThreadStatusStats, ReplyStatus } from '../../types/thread-status.types.js';
14
+ /**
15
+ * Service that maintains a persistent queue of thread statuses.
16
+ * Constructed with a crewly home directory path for disk persistence.
17
+ *
18
+ * Lifecycle: enqueued → delivered → replied_* (terminal or waiting_actions)
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const svc = new ThreadStatusQueueService('/home/user/.crewly');
23
+ * await svc.loadPersistedState();
24
+ *
25
+ * const entry = svc.trackInbound({
26
+ * source: 'slack',
27
+ * threadKey: 'C123:170743.001',
28
+ * conversationId: 'conv-1',
29
+ * messagePreview: 'Deploy the new feature',
30
+ * });
31
+ *
32
+ * svc.markDelivered('C123:170743.001');
33
+ * svc.markReplied('C123:170743.001', 'replied_completed');
34
+ * ```
35
+ */
36
+ export declare class ThreadStatusQueueService {
37
+ /** Singleton instance */
38
+ private static instance;
39
+ /** Logger for this service */
40
+ private logger;
41
+ /** All tracked thread entries, keyed by threadKey for O(1) lookup */
42
+ private entries;
43
+ /** Path to the persistence file, or null if init() has not been called */
44
+ private persistPath;
45
+ /** Debounce timer for batching persistence writes */
46
+ private persistTimer;
47
+ /** ISO timestamp of last cleanup run */
48
+ private lastCleanupAt;
49
+ /**
50
+ * Creates a new ThreadStatusQueueService.
51
+ * Use getInstance() for singleton access, or construct directly for testing.
52
+ *
53
+ * @param crewlyHome - Optional crewly home directory path for immediate initialization
54
+ */
55
+ constructor(crewlyHome?: string);
56
+ /**
57
+ * Returns the singleton instance of ThreadStatusQueueService.
58
+ * Call init() after this to set the persistence path.
59
+ *
60
+ * @returns The singleton instance
61
+ */
62
+ static getInstance(): ThreadStatusQueueService;
63
+ /**
64
+ * Resets the singleton instance. For testing only.
65
+ */
66
+ static resetInstance(): void;
67
+ /**
68
+ * Loads persisted state from disk.
69
+ * If the file doesn't exist or is corrupted, starts with an empty queue.
70
+ */
71
+ loadPersistedState(): Promise<void>;
72
+ /**
73
+ * Records a new inbound thread message with status `enqueued`.
74
+ * If a thread with the same threadKey already exists, the existing entry is returned.
75
+ *
76
+ * @param input - Inbound message details
77
+ * @returns The created (or existing) ThreadStatusEntry
78
+ */
79
+ trackInbound(input: TrackInboundInput): ThreadStatusEntry;
80
+ /**
81
+ * Transitions a thread from `enqueued` (or `error`) to `delivered`.
82
+ * No-op if thread is already in a terminal or later status.
83
+ *
84
+ * @param threadKey - Platform-specific thread identifier
85
+ * @throws Error if threadKey is not found
86
+ */
87
+ markDelivered(threadKey: string): void;
88
+ /**
89
+ * Transitions a thread to one of the replied_* statuses.
90
+ * Sets the repliedAt timestamp.
91
+ *
92
+ * @param threadKey - Platform-specific thread identifier
93
+ * @param status - One of: replied_completed, replied_waiting_actions, replied_to_follow_up
94
+ * @throws Error if threadKey is not found or status is not a valid reply status
95
+ */
96
+ markReplied(threadKey: string, status: ReplyStatus): void;
97
+ /**
98
+ * Records that an agent was delegated work from this thread.
99
+ *
100
+ * @param threadKey - Platform-specific thread identifier
101
+ * @param agentSession - Session name of the delegated agent
102
+ * @throws Error if threadKey is not found
103
+ */
104
+ addDelegatedAgent(threadKey: string, agentSession: string): void;
105
+ /**
106
+ * Transitions a `replied_waiting_actions` thread to `replied_completed`
107
+ * when all delegated agents have finished their work.
108
+ *
109
+ * @param threadKey - Platform-specific thread identifier
110
+ * @throws Error if threadKey is not found
111
+ */
112
+ markDelegationsComplete(threadKey: string): void;
113
+ /**
114
+ * Returns all entries not in a terminal status. Used for restart recovery.
115
+ *
116
+ * @returns Array of non-terminal ThreadStatusEntry objects
117
+ */
118
+ getPendingThreads(): ThreadStatusEntry[];
119
+ /**
120
+ * Returns all entries matching the given status.
121
+ *
122
+ * @param status - The ThreadStatus to filter by
123
+ * @returns Array of matching ThreadStatusEntry objects
124
+ */
125
+ getByStatus(status: ThreadStatus): ThreadStatusEntry[];
126
+ /**
127
+ * Retrieves a specific entry by threadKey.
128
+ *
129
+ * @param threadKey - Platform-specific thread identifier
130
+ * @returns The entry, or null if not found
131
+ */
132
+ get(threadKey: string): ThreadStatusEntry | null;
133
+ /**
134
+ * Expires entries that have been in a non-terminal status longer than maxAgeMinutes.
135
+ * Transitions them to `expired`.
136
+ *
137
+ * @param maxAgeMinutes - Maximum age in minutes (defaults to STALE_TIMEOUT_MINUTES)
138
+ * @returns Number of entries expired
139
+ */
140
+ expireStale(maxAgeMinutes?: number): number;
141
+ /**
142
+ * Removes terminal entries older than retentionHours.
143
+ * Also enforces MAX_ENTRIES by pruning oldest terminal entries first.
144
+ *
145
+ * @param retentionHours - Hours to retain terminal entries (defaults to CLEANUP_RETENTION_HOURS)
146
+ * @returns Number of entries removed
147
+ */
148
+ cleanup(retentionHours?: number): number;
149
+ /**
150
+ * Returns statistics for monitoring the thread status queue.
151
+ *
152
+ * @returns Stats snapshot including total, per-status counts, and oldest pending timestamp
153
+ */
154
+ getStats(): ThreadStatusStats;
155
+ /**
156
+ * Initializes the service with a crewly home directory for persistence.
157
+ * Must be called before loadPersistedState() when using getInstance().
158
+ *
159
+ * @param crewlyHome - Absolute path to the crewly home directory (e.g. ~/.crewly)
160
+ */
161
+ init(crewlyHome: string): void;
162
+ /**
163
+ * Public convenience method: optionally re-inits the persist path,
164
+ * then loads persisted state from disk.
165
+ *
166
+ * @param crewlyHome - Optional crewly home directory to re-initialize with
167
+ */
168
+ load(crewlyHome?: string): Promise<void>;
169
+ /**
170
+ * Immediately flushes the current state to disk. Call during graceful shutdown
171
+ * to ensure no in-memory mutations are lost.
172
+ */
173
+ persist(): Promise<void>;
174
+ /**
175
+ * Cleans up timers. Call on shutdown.
176
+ */
177
+ destroy(): void;
178
+ /**
179
+ * Retrieves an entry by threadKey or throws an error with a descriptive message.
180
+ *
181
+ * @param threadKey - Platform-specific thread identifier
182
+ * @param caller - Name of the calling method (for error messages)
183
+ * @returns The found entry
184
+ * @throws Error if entry is not found
185
+ */
186
+ private getOrThrow;
187
+ /**
188
+ * Schedules a debounced persistence write to disk.
189
+ * Multiple mutations within PERSIST_DEBOUNCE_MS are batched into a single write.
190
+ */
191
+ private schedulePersist;
192
+ /**
193
+ * Writes the current queue state to disk atomically.
194
+ */
195
+ private persistToDisk;
196
+ }
197
+ //# sourceMappingURL=thread-status-queue.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thread-status-queue.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/messaging/thread-status-queue.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAaH,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAEjB,WAAW,EACZ,MAAM,oCAAoC,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,wBAAwB;IACnC,yBAAyB;IACzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA2B;IAElD,8BAA8B;IAC9B,OAAO,CAAC,MAAM,CAAkG;IAEhH,qEAAqE;IACrE,OAAO,CAAC,OAAO,CAA6C;IAE5D,0EAA0E;IAC1E,OAAO,CAAC,WAAW,CAAuB;IAE1C,qDAAqD;IACrD,OAAO,CAAC,YAAY,CAA8C;IAElE,wCAAwC;IACxC,OAAO,CAAC,aAAa,CAAoC;IAEzD;;;;;OAKG;gBACS,UAAU,CAAC,EAAE,MAAM;IAO/B;;;;;OAKG;IACH,MAAM,CAAC,WAAW,IAAI,wBAAwB;IAO9C;;OAEG;IACH,MAAM,CAAC,aAAa,IAAI,IAAI;IAO5B;;;OAGG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCzC;;;;;;OAMG;IACH,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,iBAAiB;IAkCzD;;;;;;OAMG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAetC;;;;;;;OAOG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IAmBzD;;;;;;OAMG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAgBhE;;;;;;OAMG;IACH,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAahD;;;;OAIG;IACH,iBAAiB,IAAI,iBAAiB,EAAE;IAUxC;;;;;OAKG;IACH,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,iBAAiB,EAAE;IAUtD;;;;;OAKG;IACH,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAIhD;;;;;;OAMG;IACH,WAAW,CAAC,aAAa,GAAE,MAAsD,GAAG,MAAM;IAuB1F;;;;;;OAMG;IACH,OAAO,CAAC,cAAc,GAAE,MAAwD,GAAG,MAAM;IAqCzF;;;;OAIG;IACH,QAAQ,IAAI,iBAAiB;IA8B7B;;;;;OAKG;IACH,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAK9B;;;;;OAKG;IACG,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9C;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B;;OAEG;IACH,OAAO,IAAI,IAAI;IAWf;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU;IAQlB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAevB;;OAEG;YACW,aAAa;CAe5B"}
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Thread Status Queue Service
3
+ *
4
+ * Tracks the lifecycle of every inbound message thread across platforms
5
+ * (Slack, GChat, Telegram, etc.). On orchestrator restart, non-terminal
6
+ * threads are recovered and re-enqueued so no user messages are silently dropped.
7
+ *
8
+ * Persistence is debounced and written to ~/.crewly/thread-status-queue.json
9
+ * using atomic file I/O to prevent corruption.
10
+ *
11
+ * @module services/messaging/thread-status-queue
12
+ */
13
+ import { randomUUID } from 'crypto';
14
+ import path from 'path';
15
+ import { THREAD_STATUS_CONSTANTS } from '../../constants.js';
16
+ import { LoggerService } from '../core/logger.service.js';
17
+ import { atomicWriteFile, safeReadJson } from '../../utils/file-io.utils.js';
18
+ import { PERSISTED_THREAD_STATUS_VERSION, isPersistedThreadStatusState, isTerminalStatus, isReplyStatus, } from '../../types/thread-status.types.js';
19
+ /**
20
+ * Service that maintains a persistent queue of thread statuses.
21
+ * Constructed with a crewly home directory path for disk persistence.
22
+ *
23
+ * Lifecycle: enqueued → delivered → replied_* (terminal or waiting_actions)
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const svc = new ThreadStatusQueueService('/home/user/.crewly');
28
+ * await svc.loadPersistedState();
29
+ *
30
+ * const entry = svc.trackInbound({
31
+ * source: 'slack',
32
+ * threadKey: 'C123:170743.001',
33
+ * conversationId: 'conv-1',
34
+ * messagePreview: 'Deploy the new feature',
35
+ * });
36
+ *
37
+ * svc.markDelivered('C123:170743.001');
38
+ * svc.markReplied('C123:170743.001', 'replied_completed');
39
+ * ```
40
+ */
41
+ export class ThreadStatusQueueService {
42
+ /** Singleton instance */
43
+ static instance;
44
+ /** Logger for this service */
45
+ logger = LoggerService.getInstance().createComponentLogger('ThreadStatusQueueService');
46
+ /** All tracked thread entries, keyed by threadKey for O(1) lookup */
47
+ entries = new Map();
48
+ /** Path to the persistence file, or null if init() has not been called */
49
+ persistPath = null;
50
+ /** Debounce timer for batching persistence writes */
51
+ persistTimer = null;
52
+ /** ISO timestamp of last cleanup run */
53
+ lastCleanupAt = new Date().toISOString();
54
+ /**
55
+ * Creates a new ThreadStatusQueueService.
56
+ * Use getInstance() for singleton access, or construct directly for testing.
57
+ *
58
+ * @param crewlyHome - Optional crewly home directory path for immediate initialization
59
+ */
60
+ constructor(crewlyHome) {
61
+ if (crewlyHome) {
62
+ this.persistPath = path.join(crewlyHome, THREAD_STATUS_CONSTANTS.STORAGE_FILE);
63
+ this.logger.info('Initialized', { persistPath: this.persistPath });
64
+ }
65
+ }
66
+ /**
67
+ * Returns the singleton instance of ThreadStatusQueueService.
68
+ * Call init() after this to set the persistence path.
69
+ *
70
+ * @returns The singleton instance
71
+ */
72
+ static getInstance() {
73
+ if (!ThreadStatusQueueService.instance) {
74
+ ThreadStatusQueueService.instance = new ThreadStatusQueueService();
75
+ }
76
+ return ThreadStatusQueueService.instance;
77
+ }
78
+ /**
79
+ * Resets the singleton instance. For testing only.
80
+ */
81
+ static resetInstance() {
82
+ if (ThreadStatusQueueService.instance) {
83
+ ThreadStatusQueueService.instance.destroy();
84
+ }
85
+ ThreadStatusQueueService.instance = undefined;
86
+ }
87
+ /**
88
+ * Loads persisted state from disk.
89
+ * If the file doesn't exist or is corrupted, starts with an empty queue.
90
+ */
91
+ async loadPersistedState() {
92
+ if (!this.persistPath) {
93
+ this.logger.warn('loadPersistedState called before init — skipping');
94
+ return;
95
+ }
96
+ const defaultState = {
97
+ version: PERSISTED_THREAD_STATUS_VERSION,
98
+ entries: [],
99
+ lastCleanupAt: new Date().toISOString(),
100
+ };
101
+ const state = await safeReadJson(this.persistPath, defaultState, this.logger);
102
+ if (!isPersistedThreadStatusState(state)) {
103
+ this.logger.warn('Invalid persisted state, starting fresh');
104
+ return;
105
+ }
106
+ this.entries.clear();
107
+ for (const entry of state.entries) {
108
+ this.entries.set(entry.threadKey, entry);
109
+ }
110
+ this.lastCleanupAt = state.lastCleanupAt;
111
+ this.logger.info('Loaded persisted state', {
112
+ entryCount: this.entries.size,
113
+ lastCleanupAt: this.lastCleanupAt,
114
+ });
115
+ }
116
+ /**
117
+ * Records a new inbound thread message with status `enqueued`.
118
+ * If a thread with the same threadKey already exists, the existing entry is returned.
119
+ *
120
+ * @param input - Inbound message details
121
+ * @returns The created (or existing) ThreadStatusEntry
122
+ */
123
+ trackInbound(input) {
124
+ const existing = this.entries.get(input.threadKey);
125
+ if (existing) {
126
+ this.logger.debug('Thread already tracked, returning existing', { threadKey: input.threadKey });
127
+ return existing;
128
+ }
129
+ const now = new Date().toISOString();
130
+ const entry = {
131
+ id: randomUUID(),
132
+ source: input.source,
133
+ threadKey: input.threadKey,
134
+ conversationId: input.conversationId,
135
+ status: 'enqueued',
136
+ receivedAt: now,
137
+ updatedAt: now,
138
+ messagePreview: input.messagePreview.slice(0, THREAD_STATUS_CONSTANTS.MAX_PREVIEW_LENGTH),
139
+ retryCount: 0,
140
+ queueMessageId: input.queueMessageId,
141
+ sourceMetadata: input.sourceMetadata,
142
+ };
143
+ this.entries.set(input.threadKey, entry);
144
+ this.schedulePersist();
145
+ this.logger.info('Tracked inbound thread', {
146
+ threadKey: input.threadKey,
147
+ source: input.source,
148
+ conversationId: input.conversationId,
149
+ });
150
+ return entry;
151
+ }
152
+ /**
153
+ * Transitions a thread from `enqueued` (or `error`) to `delivered`.
154
+ * No-op if thread is already in a terminal or later status.
155
+ *
156
+ * @param threadKey - Platform-specific thread identifier
157
+ * @throws Error if threadKey is not found
158
+ */
159
+ markDelivered(threadKey) {
160
+ const entry = this.getOrThrow(threadKey, 'markDelivered');
161
+ if (isTerminalStatus(entry.status) || entry.status === 'delivered') {
162
+ return;
163
+ }
164
+ const now = new Date().toISOString();
165
+ entry.status = 'delivered';
166
+ entry.deliveredAt = now;
167
+ entry.updatedAt = now;
168
+ this.schedulePersist();
169
+ this.logger.info('Marked delivered', { threadKey });
170
+ }
171
+ /**
172
+ * Transitions a thread to one of the replied_* statuses.
173
+ * Sets the repliedAt timestamp.
174
+ *
175
+ * @param threadKey - Platform-specific thread identifier
176
+ * @param status - One of: replied_completed, replied_waiting_actions, replied_to_follow_up
177
+ * @throws Error if threadKey is not found or status is not a valid reply status
178
+ */
179
+ markReplied(threadKey, status) {
180
+ if (!isReplyStatus(status)) {
181
+ throw new Error(`Invalid reply status: ${status}`);
182
+ }
183
+ const entry = this.getOrThrow(threadKey, 'markReplied');
184
+ if (isTerminalStatus(entry.status)) {
185
+ return;
186
+ }
187
+ const now = new Date().toISOString();
188
+ entry.status = status;
189
+ entry.repliedAt = now;
190
+ entry.updatedAt = now;
191
+ this.schedulePersist();
192
+ this.logger.info('Marked replied', { threadKey, status });
193
+ }
194
+ /**
195
+ * Records that an agent was delegated work from this thread.
196
+ *
197
+ * @param threadKey - Platform-specific thread identifier
198
+ * @param agentSession - Session name of the delegated agent
199
+ * @throws Error if threadKey is not found
200
+ */
201
+ addDelegatedAgent(threadKey, agentSession) {
202
+ const entry = this.getOrThrow(threadKey, 'addDelegatedAgent');
203
+ if (!entry.delegatedAgents) {
204
+ entry.delegatedAgents = [];
205
+ }
206
+ if (!entry.delegatedAgents.includes(agentSession)) {
207
+ entry.delegatedAgents.push(agentSession);
208
+ entry.updatedAt = new Date().toISOString();
209
+ this.schedulePersist();
210
+ this.logger.info('Added delegated agent', { threadKey, agentSession });
211
+ }
212
+ }
213
+ /**
214
+ * Transitions a `replied_waiting_actions` thread to `replied_completed`
215
+ * when all delegated agents have finished their work.
216
+ *
217
+ * @param threadKey - Platform-specific thread identifier
218
+ * @throws Error if threadKey is not found
219
+ */
220
+ markDelegationsComplete(threadKey) {
221
+ const entry = this.getOrThrow(threadKey, 'markDelegationsComplete');
222
+ if (entry.status !== 'replied_waiting_actions') {
223
+ return;
224
+ }
225
+ entry.status = 'replied_completed';
226
+ entry.updatedAt = new Date().toISOString();
227
+ this.schedulePersist();
228
+ this.logger.info('Marked delegations complete', { threadKey });
229
+ }
230
+ /**
231
+ * Returns all entries not in a terminal status. Used for restart recovery.
232
+ *
233
+ * @returns Array of non-terminal ThreadStatusEntry objects
234
+ */
235
+ getPendingThreads() {
236
+ const pending = [];
237
+ for (const entry of this.entries.values()) {
238
+ if (!isTerminalStatus(entry.status)) {
239
+ pending.push(entry);
240
+ }
241
+ }
242
+ return pending;
243
+ }
244
+ /**
245
+ * Returns all entries matching the given status.
246
+ *
247
+ * @param status - The ThreadStatus to filter by
248
+ * @returns Array of matching ThreadStatusEntry objects
249
+ */
250
+ getByStatus(status) {
251
+ const results = [];
252
+ for (const entry of this.entries.values()) {
253
+ if (entry.status === status) {
254
+ results.push(entry);
255
+ }
256
+ }
257
+ return results;
258
+ }
259
+ /**
260
+ * Retrieves a specific entry by threadKey.
261
+ *
262
+ * @param threadKey - Platform-specific thread identifier
263
+ * @returns The entry, or null if not found
264
+ */
265
+ get(threadKey) {
266
+ return this.entries.get(threadKey) ?? null;
267
+ }
268
+ /**
269
+ * Expires entries that have been in a non-terminal status longer than maxAgeMinutes.
270
+ * Transitions them to `expired`.
271
+ *
272
+ * @param maxAgeMinutes - Maximum age in minutes (defaults to STALE_TIMEOUT_MINUTES)
273
+ * @returns Number of entries expired
274
+ */
275
+ expireStale(maxAgeMinutes = THREAD_STATUS_CONSTANTS.STALE_TIMEOUT_MINUTES) {
276
+ const cutoff = Date.now() - maxAgeMinutes * 60 * 1000;
277
+ let count = 0;
278
+ for (const entry of this.entries.values()) {
279
+ if (isTerminalStatus(entry.status))
280
+ continue;
281
+ const updatedTime = new Date(entry.updatedAt).getTime();
282
+ if (updatedTime < cutoff) {
283
+ entry.status = 'expired';
284
+ entry.updatedAt = new Date().toISOString();
285
+ count++;
286
+ }
287
+ }
288
+ if (count > 0) {
289
+ this.schedulePersist();
290
+ this.logger.info('Expired stale threads', { count, maxAgeMinutes });
291
+ }
292
+ return count;
293
+ }
294
+ /**
295
+ * Removes terminal entries older than retentionHours.
296
+ * Also enforces MAX_ENTRIES by pruning oldest terminal entries first.
297
+ *
298
+ * @param retentionHours - Hours to retain terminal entries (defaults to CLEANUP_RETENTION_HOURS)
299
+ * @returns Number of entries removed
300
+ */
301
+ cleanup(retentionHours = THREAD_STATUS_CONSTANTS.CLEANUP_RETENTION_HOURS) {
302
+ const cutoff = Date.now() - retentionHours * 60 * 60 * 1000;
303
+ let count = 0;
304
+ // Remove terminal entries older than retention period
305
+ for (const [key, entry] of this.entries) {
306
+ if (isTerminalStatus(entry.status)) {
307
+ const updatedTime = new Date(entry.updatedAt).getTime();
308
+ if (updatedTime < cutoff) {
309
+ this.entries.delete(key);
310
+ count++;
311
+ }
312
+ }
313
+ }
314
+ // Enforce MAX_ENTRIES: prune oldest terminal entries first
315
+ if (this.entries.size > THREAD_STATUS_CONSTANTS.MAX_ENTRIES) {
316
+ const terminalEntries = Array.from(this.entries.entries())
317
+ .filter(([, e]) => isTerminalStatus(e.status))
318
+ .sort(([, a], [, b]) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime());
319
+ for (const [key] of terminalEntries) {
320
+ if (this.entries.size <= THREAD_STATUS_CONSTANTS.MAX_ENTRIES)
321
+ break;
322
+ this.entries.delete(key);
323
+ count++;
324
+ }
325
+ }
326
+ if (count > 0) {
327
+ this.lastCleanupAt = new Date().toISOString();
328
+ this.schedulePersist();
329
+ this.logger.info('Cleaned up entries', { removed: count, remaining: this.entries.size });
330
+ }
331
+ return count;
332
+ }
333
+ /**
334
+ * Returns statistics for monitoring the thread status queue.
335
+ *
336
+ * @returns Stats snapshot including total, per-status counts, and oldest pending timestamp
337
+ */
338
+ getStats() {
339
+ const byStatus = {
340
+ enqueued: 0,
341
+ delivered: 0,
342
+ replied_completed: 0,
343
+ replied_waiting_actions: 0,
344
+ replied_to_follow_up: 0,
345
+ expired: 0,
346
+ error: 0,
347
+ };
348
+ let oldestPending = null;
349
+ for (const entry of this.entries.values()) {
350
+ byStatus[entry.status]++;
351
+ if (!isTerminalStatus(entry.status)) {
352
+ if (oldestPending === null || entry.receivedAt < oldestPending) {
353
+ oldestPending = entry.receivedAt;
354
+ }
355
+ }
356
+ }
357
+ return {
358
+ total: this.entries.size,
359
+ byStatus,
360
+ oldestPending,
361
+ };
362
+ }
363
+ /**
364
+ * Initializes the service with a crewly home directory for persistence.
365
+ * Must be called before loadPersistedState() when using getInstance().
366
+ *
367
+ * @param crewlyHome - Absolute path to the crewly home directory (e.g. ~/.crewly)
368
+ */
369
+ init(crewlyHome) {
370
+ this.persistPath = path.join(crewlyHome, THREAD_STATUS_CONSTANTS.STORAGE_FILE);
371
+ this.logger.info('Initialized', { persistPath: this.persistPath });
372
+ }
373
+ /**
374
+ * Public convenience method: optionally re-inits the persist path,
375
+ * then loads persisted state from disk.
376
+ *
377
+ * @param crewlyHome - Optional crewly home directory to re-initialize with
378
+ */
379
+ async load(crewlyHome) {
380
+ if (crewlyHome) {
381
+ this.init(crewlyHome);
382
+ }
383
+ await this.loadPersistedState();
384
+ }
385
+ /**
386
+ * Immediately flushes the current state to disk. Call during graceful shutdown
387
+ * to ensure no in-memory mutations are lost.
388
+ */
389
+ async persist() {
390
+ // Cancel any debounced write — we flush synchronously
391
+ if (this.persistTimer) {
392
+ clearTimeout(this.persistTimer);
393
+ this.persistTimer = null;
394
+ }
395
+ await this.persistToDisk();
396
+ }
397
+ /**
398
+ * Cleans up timers. Call on shutdown.
399
+ */
400
+ destroy() {
401
+ if (this.persistTimer) {
402
+ clearTimeout(this.persistTimer);
403
+ this.persistTimer = null;
404
+ }
405
+ }
406
+ // ===========================================================================
407
+ // Private Helpers
408
+ // ===========================================================================
409
+ /**
410
+ * Retrieves an entry by threadKey or throws an error with a descriptive message.
411
+ *
412
+ * @param threadKey - Platform-specific thread identifier
413
+ * @param caller - Name of the calling method (for error messages)
414
+ * @returns The found entry
415
+ * @throws Error if entry is not found
416
+ */
417
+ getOrThrow(threadKey, caller) {
418
+ const entry = this.entries.get(threadKey);
419
+ if (!entry) {
420
+ throw new Error(`${caller}: thread not found — threadKey=${threadKey}`);
421
+ }
422
+ return entry;
423
+ }
424
+ /**
425
+ * Schedules a debounced persistence write to disk.
426
+ * Multiple mutations within PERSIST_DEBOUNCE_MS are batched into a single write.
427
+ */
428
+ schedulePersist() {
429
+ if (!this.persistPath)
430
+ return;
431
+ if (this.persistTimer) {
432
+ clearTimeout(this.persistTimer);
433
+ }
434
+ this.persistTimer = setTimeout(() => {
435
+ this.persistTimer = null;
436
+ this.persistToDisk().catch((err) => {
437
+ this.logger.warn('Failed to persist thread status queue', { error: String(err) });
438
+ });
439
+ }, THREAD_STATUS_CONSTANTS.PERSIST_DEBOUNCE_MS);
440
+ }
441
+ /**
442
+ * Writes the current queue state to disk atomically.
443
+ */
444
+ async persistToDisk() {
445
+ if (!this.persistPath) {
446
+ this.logger.warn('persistToDisk called before init — skipping');
447
+ return;
448
+ }
449
+ const state = {
450
+ version: PERSISTED_THREAD_STATUS_VERSION,
451
+ entries: Array.from(this.entries.values()),
452
+ lastCleanupAt: this.lastCleanupAt,
453
+ };
454
+ await atomicWriteFile(this.persistPath, JSON.stringify(state, null, 2));
455
+ this.logger.debug('Persisted to disk', { entryCount: state.entries.length });
456
+ }
457
+ }
458
+ //# sourceMappingURL=thread-status-queue.service.js.map