evolcore 0.0.28 → 0.0.29

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 (183) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +19 -9
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +6 -2
  7. package/dist/agents/codex/diagnostics.js +4 -3
  8. package/dist/agents/codex-runner.js +396 -95
  9. package/dist/agents/gemini-runner.js +35 -17
  10. package/dist/aun/group/history.js +130 -90
  11. package/dist/aun/group/operations.js +1 -3
  12. package/dist/aun/menu/transport.js +5 -3
  13. package/dist/aun/msg/managed-operation.js +70 -0
  14. package/dist/aun/msg/p2p.js +1 -3
  15. package/dist/aun/msg/upload.js +2 -2
  16. package/dist/aun/outbox.js +1 -37
  17. package/dist/channels/aun.js +249 -89
  18. package/dist/channels/daemon.js +18 -1
  19. package/dist/channels/feishu.js +1 -2
  20. package/dist/cli/agent-command.js +23 -3
  21. package/dist/cli/agent-create.js +68 -2
  22. package/dist/cli/agent.js +12 -2
  23. package/dist/cli/aun-commands.js +87 -13
  24. package/dist/cli/cli-argv.js +26 -0
  25. package/dist/cli/daemon-commands.js +40 -10
  26. package/dist/cli/data-command.js +4 -4
  27. package/dist/cli/handoff-command.js +2 -0
  28. package/dist/cli/index.js +18 -1
  29. package/dist/cli/managed-command-guard.js +10 -0
  30. package/dist/cli/memory-command.js +441 -0
  31. package/dist/cli/restart-monitor.js +62 -6
  32. package/dist/cli/skill-command.js +41 -0
  33. package/dist/cli/task-context.js +2 -23
  34. package/dist/cli/trigger-command.js +9 -1
  35. package/dist/cli/watch-msg-diag.js +209 -0
  36. package/dist/config/config-field-policy.js +52 -3
  37. package/dist/config/config-manager.js +17 -4
  38. package/dist/config/contact-book-store.js +2 -2
  39. package/dist/config/mention-mode.js +7 -2
  40. package/dist/config/merge.js +28 -9
  41. package/dist/config/resolved-config-op.js +1 -1
  42. package/dist/config/runtime-env.js +137 -0
  43. package/dist/config/schema-registry.js +11 -1
  44. package/dist/config-store.js +10 -10
  45. package/dist/core/audit/event-key.js +3 -1
  46. package/dist/core/audit/log-integrity.js +4 -0
  47. package/dist/core/auth/agent-delegation.js +2 -1
  48. package/dist/core/auth/auth-gateway.js +13 -6
  49. package/dist/core/auth/authorization-audit.js +54 -19
  50. package/dist/core/auth/operation-authorizer.js +17 -0
  51. package/dist/core/auth/operation-catalog.js +9 -0
  52. package/dist/core/causation/context.js +5 -1
  53. package/dist/core/command/agent-control.js +1 -2
  54. package/dist/core/command/command-handler.js +51 -23
  55. package/dist/core/command/memory-slash.js +270 -0
  56. package/dist/core/command/menu-catalog.js +1 -0
  57. package/dist/core/command/menu-handler.js +315 -60
  58. package/dist/core/command/menu-protocol.js +42 -4
  59. package/dist/core/command/slash-gate.js +10 -10
  60. package/dist/core/command/slash-handler.js +206 -64
  61. package/dist/core/data-migration.js +282 -505
  62. package/dist/core/event-catalog.js +26 -0
  63. package/dist/core/evolagent-registry.js +6 -0
  64. package/dist/core/evolagent.js +0 -10
  65. package/dist/core/handoff/store.js +1 -20
  66. package/dist/core/inference/text-inference.js +57 -1
  67. package/dist/core/memory/consolidation-context.js +505 -0
  68. package/dist/core/memory/consolidation-ops-records.js +124 -0
  69. package/dist/core/memory/consolidation-trigger.js +394 -0
  70. package/dist/core/memory/index-manager.js +308 -0
  71. package/dist/core/memory/launch-consolidation.js +395 -0
  72. package/dist/core/memory/memory-admin.js +234 -0
  73. package/dist/core/memory/memory-document.js +100 -0
  74. package/dist/core/memory/memory-session-policy.js +41 -0
  75. package/dist/core/memory/memory-signature.js +257 -0
  76. package/dist/core/memory/memory-writer.js +432 -0
  77. package/dist/core/memory/staleness.js +117 -0
  78. package/dist/core/message/auto-mention-gate.js +454 -0
  79. package/dist/core/message/im-renderer.js +69 -16
  80. package/dist/core/message/message-bridge.js +287 -21
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +114 -27
  83. package/dist/core/message/pending-hints.js +329 -161
  84. package/dist/core/message/response-engine.js +567 -80
  85. package/dist/core/permission/ec-command-parser.js +1029 -80
  86. package/dist/core/permission/protected-paths.js +376 -126
  87. package/dist/core/permission/sandbox-runtime.js +40 -12
  88. package/dist/core/permission/tool-error-code.js +96 -13
  89. package/dist/core/permission/tool-policy.js +276 -23
  90. package/dist/core/role/runtime-policy.js +3 -7
  91. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  92. package/dist/core/session/session-fs-store.js +0 -76
  93. package/dist/core/session/session-idle-monitor.js +355 -0
  94. package/dist/core/session/session-manager.js +478 -17
  95. package/dist/core/session/session-mapper.js +33 -16
  96. package/dist/core/session/session-renew.js +217 -155
  97. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  98. package/dist/core/skill/global-watcher.js +282 -0
  99. package/dist/core/skill/index-generator.js +84 -0
  100. package/dist/core/skill/startup.js +73 -0
  101. package/dist/core/skill/switch-resolver.js +115 -0
  102. package/dist/core/task/task-activity-store.js +219 -0
  103. package/dist/eck/kit-renderer.js +30 -3
  104. package/dist/eck/manifest-engine.js +26 -4
  105. package/dist/index.js +708 -18
  106. package/dist/ipc.js +564 -2
  107. package/dist/paths.js +30 -9
  108. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  109. package/dist/response-system/modes/single-session/index.js +3 -0
  110. package/dist/trigger/feedback.js +1 -0
  111. package/dist/trigger/manager.js +0 -10
  112. package/dist/trigger/scheduler.js +11 -0
  113. package/dist/trigger/validation.js +22 -0
  114. package/dist/utils/avatar-upload.js +59 -18
  115. package/dist/utils/error-dict.json +6 -3
  116. package/dist/utils/error-utils.js +27 -0
  117. package/dist/utils/logger.js +32 -10
  118. package/dist/utils/media-cache.js +84 -0
  119. package/dist/utils/msg-diag.js +97 -0
  120. package/dist/utils/welcome.js +37 -8
  121. package/kits/docs/AUTHORING.md +314 -0
  122. package/kits/docs/GUIDE.md +2 -0
  123. package/kits/docs/INDEX.md +2 -0
  124. package/kits/docs/aun/CHEATSHEET.md +4 -0
  125. package/kits/docs/aun/SDK.md +60 -0
  126. package/kits/docs/context-assembly.md +10 -6
  127. package/kits/docs/evolcore/INDEX.md +17 -3
  128. package/kits/docs/evolcore/agent-create.md +24 -0
  129. package/kits/docs/evolcore/agent.md +5 -2
  130. package/kits/docs/evolcore/config.md +1 -1
  131. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  132. package/kits/docs/evolcore/group.md +34 -18
  133. package/kits/docs/evolcore/memory-architecture.md +78 -0
  134. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  135. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  136. package/kits/docs/evolcore/memory-tags.md +63 -0
  137. package/kits/docs/evolcore/memory.md +110 -0
  138. package/kits/docs/evolcore/menu-client.md +27 -11
  139. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  140. package/kits/docs/evolcore/menu-protocol.md +213 -0
  141. package/kits/docs/evolcore/msg.md +2 -0
  142. package/kits/docs/evolcore/trigger.md +7 -5
  143. package/kits/docs/path-registry.md +6 -8
  144. package/kits/docs/prompt-loading-architecture.md +5 -3
  145. package/kits/docs/venues/group.md +5 -4
  146. package/kits/eck_manifest.json +294 -88
  147. package/kits/eck_manifest.memory.json +257 -0
  148. package/kits/rules/03-identity.md +8 -8
  149. package/kits/rules/05-venue.md +4 -2
  150. package/kits/rules/06-channel.md +1 -1
  151. package/kits/schemas/_meta.json +276 -47
  152. package/kits/schemas/agent-config.schema.12.json +38 -2
  153. package/kits/schemas/agent-config.schema.13.json +591 -0
  154. package/kits/schemas/daemon.schema.6.json +9 -9
  155. package/kits/schemas/defaults.schema.5.json +29 -0
  156. package/kits/schemas/relation-config.schema.8.json +14 -1
  157. package/kits/schemas/relation-config.schema.9.json +129 -0
  158. package/kits/schemas/role-config.schema.1.json +3 -3
  159. package/kits/skills/.switches.json +3 -0
  160. package/kits/skills/ec-skill/SKILL.md +217 -0
  161. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  162. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  163. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  164. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  165. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  166. package/kits/templates/decision/auto-mention.md +25 -0
  167. package/kits/templates/decision/session-renew.md +22 -0
  168. package/kits/templates/memory-consolidation/forget.md +104 -0
  169. package/kits/templates/memory-consolidation/generalize.md +76 -0
  170. package/kits/templates/memory-consolidation/notes.md +133 -0
  171. package/kits/templates/memory-consolidation/profile.md +58 -0
  172. package/kits/templates/memory-consolidation/promote.md +72 -0
  173. package/kits/templates/memory-consolidation/workflow.md +75 -0
  174. package/kits/templates/system-fragments/bootstrap.md +14 -5
  175. package/kits/templates/system-fragments/commands.md +1 -0
  176. package/kits/templates/system-fragments/group-history.md +6 -0
  177. package/kits/templates/system-fragments/identity.md +0 -3
  178. package/kits/templates/system-fragments/memory.md +79 -0
  179. package/kits/templates/system-fragments/reply.md +1 -0
  180. package/kits/templates/system-fragments/venue.md +1 -1
  181. package/package.json +4 -6
  182. package/dist/trigger/legacy-session-history.js +0 -19
  183. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -0,0 +1,355 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { logger } from '../../utils/logger.js';
4
+ import { atomicWriteJson } from './session-fs-store.js';
5
+ import { createRootCausation } from '../causation/context.js';
6
+ export const MIN_SESSION_IDLE_SECONDS = 30;
7
+ /**
8
+ * Publishes one session:idle event after a runner task ends, using the
9
+ * per-session relation configuration as the idle period.
10
+ * Trigger-generated terminal events from session:idle consumers are excluded
11
+ * using the trigger causation path to avoid resetting the idle cycle they caused.
12
+ */
13
+ export class SessionIdleMonitor {
14
+ options;
15
+ sessions = new Map();
16
+ timers = new Map();
17
+ generations = new Map();
18
+ knownSessionIdleTriggerIds = new Set();
19
+ taskEncryption = new Map();
20
+ started = false;
21
+ active = false;
22
+ onTaskTerminal = (event) => {
23
+ if (!isTaskTerminalEvent(event) || !event.sessionId)
24
+ return;
25
+ this.refreshSessionIdleTriggerIds();
26
+ const triggerPath = event.causation?.trigger?.path ?? [];
27
+ if (triggerPath.some(node => this.knownSessionIdleTriggerIds.has(node.triggerId))) {
28
+ this.taskEncryption.delete(event.sessionId);
29
+ return;
30
+ }
31
+ const timestamp = 'timestamp' in event && typeof event.timestamp === 'number' && Number.isFinite(event.timestamp)
32
+ ? event.timestamp
33
+ : this.now();
34
+ const eventEncrypted = 'encrypted' in event && typeof event.encrypted === 'boolean' ? event.encrypted : undefined;
35
+ const encrypted = eventEncrypted
36
+ ?? this.taskEncryption.get(event.sessionId)
37
+ ?? this.options.getSessionEncrypt?.(event.sessionId);
38
+ this.recordActivity(event.sessionId, timestamp, encrypted);
39
+ this.taskEncryption.delete(event.sessionId);
40
+ };
41
+ onTaskStarted = (event) => {
42
+ if (event.type !== 'task:started' || !event.sessionId)
43
+ return;
44
+ if (typeof event.encrypt === 'boolean')
45
+ this.taskEncryption.set(event.sessionId, event.encrypt);
46
+ else
47
+ this.taskEncryption.delete(event.sessionId);
48
+ };
49
+ onSessionDeleted = (event) => {
50
+ if (event.type === 'session:deleted')
51
+ this.clearSession(event.sessionId);
52
+ };
53
+ onSessionSwitched = (event) => {
54
+ if (event.type !== 'session:switched')
55
+ return;
56
+ this.clearSession(event.fromSessionId);
57
+ this.clearSession(event.toSessionId);
58
+ };
59
+ constructor(options) {
60
+ this.options = options;
61
+ this.loadState();
62
+ }
63
+ /** Subscribe early so startup task completions are captured before triggers start. */
64
+ start() {
65
+ if (this.started)
66
+ return;
67
+ this.started = true;
68
+ this.options.eventBus.subscribe('task:completed', this.onTaskTerminal);
69
+ this.options.eventBus.subscribe('task:error', this.onTaskTerminal);
70
+ this.options.eventBus.subscribe('task:interrupted', this.onTaskTerminal);
71
+ this.options.eventBus.subscribe('task:started', this.onTaskStarted);
72
+ this.options.eventBus.subscribe('session:deleted', this.onSessionDeleted);
73
+ this.options.eventBus.subscribe('session:switched', this.onSessionSwitched);
74
+ }
75
+ /** Arm restored timers only after event-trigger subscriptions are registered. */
76
+ activate() {
77
+ if (this.active)
78
+ return;
79
+ this.active = true;
80
+ this.refreshSessionIdleTriggerIds();
81
+ for (const sessionId of this.sessions.keys())
82
+ this.scheduleSession(sessionId);
83
+ }
84
+ /** Re-read relation configuration and re-arm every restored/current session. */
85
+ refresh() {
86
+ if (!this.active)
87
+ return;
88
+ for (const sessionId of this.sessions.keys()) {
89
+ this.clearTimers(sessionId);
90
+ this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
91
+ this.scheduleSession(sessionId);
92
+ }
93
+ }
94
+ stop() {
95
+ this.active = false;
96
+ this.clearAllTimers();
97
+ if (!this.started)
98
+ return;
99
+ this.started = false;
100
+ this.options.eventBus.unsubscribe('task:completed', this.onTaskTerminal);
101
+ this.options.eventBus.unsubscribe('task:error', this.onTaskTerminal);
102
+ this.options.eventBus.unsubscribe('task:interrupted', this.onTaskTerminal);
103
+ this.options.eventBus.unsubscribe('task:started', this.onTaskStarted);
104
+ this.options.eventBus.unsubscribe('session:deleted', this.onSessionDeleted);
105
+ this.options.eventBus.unsubscribe('session:switched', this.onSessionSwitched);
106
+ }
107
+ now() {
108
+ return this.options.now?.() ?? Date.now();
109
+ }
110
+ recordActivity(sessionId, lastEndedAt, encrypted) {
111
+ const current = this.sessions.get(sessionId);
112
+ if (current && current.lastEndedAt > lastEndedAt)
113
+ return;
114
+ this.clearTimers(sessionId);
115
+ this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
116
+ this.sessions.set(sessionId, {
117
+ lastEndedAt,
118
+ emitted: false,
119
+ ...(typeof encrypted === 'boolean' ? { encrypted } : {}),
120
+ });
121
+ this.persistState();
122
+ if (this.active)
123
+ this.scheduleSession(sessionId);
124
+ }
125
+ clearSession(sessionId) {
126
+ if (!sessionId)
127
+ return;
128
+ this.taskEncryption.delete(sessionId);
129
+ this.clearTimers(sessionId);
130
+ this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
131
+ if (this.sessions.delete(sessionId))
132
+ this.persistState();
133
+ }
134
+ scheduleSession(sessionId) {
135
+ void this.scheduleSessionAsync(sessionId);
136
+ }
137
+ async scheduleSessionAsync(sessionId) {
138
+ const state = this.sessions.get(sessionId);
139
+ if (!state || !this.active)
140
+ return;
141
+ const generation = this.generations.get(sessionId) ?? 0;
142
+ if (this.timers.has(sessionId) || state.emitted)
143
+ return;
144
+ let session;
145
+ try {
146
+ session = await this.options.getSessionById(sessionId);
147
+ }
148
+ catch (error) {
149
+ this.warn(`Failed to load session ${sessionId} while scheduling idle check`, error);
150
+ return;
151
+ }
152
+ if (!this.isCurrentCycle(sessionId, state.lastEndedAt, generation))
153
+ return;
154
+ if (!session || session.deletedAt) {
155
+ this.clearSession(sessionId);
156
+ return;
157
+ }
158
+ if (!isSupportedSession(session))
159
+ return;
160
+ const idleSeconds = await this.readIdleSeconds(session);
161
+ if (!this.isCurrentCycle(sessionId, state.lastEndedAt, generation))
162
+ return;
163
+ const idleMs = toIdleMs(idleSeconds);
164
+ if (idleMs === undefined)
165
+ return;
166
+ const delay = Math.max(0, state.lastEndedAt + idleMs - this.now());
167
+ const timer = setTimeout(() => {
168
+ this.timers.delete(sessionId);
169
+ void this.emitIfIdle(sessionId, state.lastEndedAt, generation);
170
+ }, delay);
171
+ timer.unref?.();
172
+ if (this.isCurrentCycle(sessionId, state.lastEndedAt, generation))
173
+ this.timers.set(sessionId, timer);
174
+ else
175
+ clearTimeout(timer);
176
+ }
177
+ async emitIfIdle(sessionId, lastEndedAt, generation) {
178
+ if (!this.isCurrentCycle(sessionId, lastEndedAt, generation))
179
+ return;
180
+ let session;
181
+ try {
182
+ session = await this.options.getSessionById(sessionId);
183
+ }
184
+ catch (error) {
185
+ this.warn(`Failed to load session ${sessionId} for idle check`, error);
186
+ this.recordActivity(sessionId, this.now());
187
+ return;
188
+ }
189
+ if (!this.isCurrentCycle(sessionId, lastEndedAt, generation))
190
+ return;
191
+ if (!session || session.deletedAt) {
192
+ this.clearSession(sessionId);
193
+ return;
194
+ }
195
+ if (!isSupportedSession(session))
196
+ return;
197
+ const idleMs = toIdleMs(await this.readIdleSeconds(session));
198
+ if (!this.isCurrentCycle(sessionId, lastEndedAt, generation))
199
+ return;
200
+ if (idleMs === undefined)
201
+ return;
202
+ const remaining = lastEndedAt + idleMs - this.now();
203
+ if (remaining > 0) {
204
+ this.scheduleSession(sessionId);
205
+ return;
206
+ }
207
+ let busy;
208
+ try {
209
+ busy = await this.options.isSessionBusy(sessionId);
210
+ }
211
+ catch (error) {
212
+ this.warn(`Failed to check whether session ${sessionId} is busy`, error);
213
+ this.recordActivity(sessionId, this.now());
214
+ return;
215
+ }
216
+ if (!this.isCurrentCycle(sessionId, lastEndedAt, generation))
217
+ return;
218
+ if (busy) {
219
+ // An active/queued task is not idle. Disarm this cycle and let its next
220
+ // terminal event start a fresh one; repeatedly rearming here would poll
221
+ // long-running sessions every 30 seconds.
222
+ this.clearTimers(sessionId);
223
+ return;
224
+ }
225
+ if (!session.selfAID) {
226
+ this.warn(`Skipping idle event for session ${sessionId}: session has no selfAID`);
227
+ this.clearSession(sessionId);
228
+ return;
229
+ }
230
+ const state = this.sessions.get(sessionId);
231
+ if (!state || state.emitted)
232
+ return;
233
+ state.emitted = true;
234
+ this.persistState();
235
+ this.options.eventBus.publish({
236
+ type: 'session:idle',
237
+ sessionId,
238
+ agentAid: session.selfAID,
239
+ channel: session.channelType || session.channel,
240
+ channelName: session.channel,
241
+ channelId: session.channelId,
242
+ chatType: session.chatType,
243
+ ...(session.threadId ? { threadId: session.threadId } : {}),
244
+ ...(session.metadata?.peerId || session.channelId ? { peerId: session.metadata?.peerId || session.channelId } : {}),
245
+ ...(typeof state.encrypted === 'boolean'
246
+ ? { encrypted: state.encrypted }
247
+ : typeof session.metadata?.replyContext?.metadata?.encrypted === 'boolean'
248
+ ? { encrypted: session.metadata.replyContext.metadata.encrypted }
249
+ : {}),
250
+ idleMs,
251
+ lastEndedAt,
252
+ timestamp: this.now(),
253
+ causation: createRootCausation(),
254
+ });
255
+ }
256
+ isCurrentCycle(sessionId, lastEndedAt, generation) {
257
+ const current = this.sessions.get(sessionId);
258
+ return this.active
259
+ && (this.generations.get(sessionId) ?? 0) === generation
260
+ && current?.lastEndedAt === lastEndedAt
261
+ && current !== undefined
262
+ && !current.emitted;
263
+ }
264
+ clearTimers(sessionId) {
265
+ const timer = this.timers.get(sessionId);
266
+ if (timer)
267
+ clearTimeout(timer);
268
+ this.timers.delete(sessionId);
269
+ }
270
+ clearAllTimers() {
271
+ for (const sessionId of this.timers.keys())
272
+ this.clearTimers(sessionId);
273
+ }
274
+ refreshSessionIdleTriggerIds() {
275
+ try {
276
+ for (const triggerId of this.options.getSessionIdleTriggerIds()) {
277
+ if (typeof triggerId === 'string' && triggerId.trim())
278
+ this.knownSessionIdleTriggerIds.add(triggerId);
279
+ }
280
+ }
281
+ catch (error) {
282
+ this.warn('Failed to refresh session:idle trigger identities', error);
283
+ }
284
+ }
285
+ async readIdleSeconds(session) {
286
+ try {
287
+ return await this.options.getSessionIdleSeconds(session);
288
+ }
289
+ catch (error) {
290
+ this.warn(`Failed to read idle configuration for session ${session.id}`, error);
291
+ return undefined;
292
+ }
293
+ }
294
+ loadState() {
295
+ try {
296
+ if (!fs.existsSync(this.options.statePath))
297
+ return;
298
+ const parsed = JSON.parse(fs.readFileSync(this.options.statePath, 'utf8'));
299
+ if ((parsed.version !== 1 && parsed.version !== 2)
300
+ || !parsed.sessions || typeof parsed.sessions !== 'object' || Array.isArray(parsed.sessions)) {
301
+ this.warn(`Ignoring unsupported session idle state at ${this.options.statePath}`);
302
+ return;
303
+ }
304
+ for (const [sessionId, rawState] of Object.entries(parsed.sessions)) {
305
+ if (!sessionId || !rawState || typeof rawState !== 'object'
306
+ || !Number.isFinite(rawState.lastEndedAt))
307
+ continue;
308
+ const lastEndedAt = rawState.lastEndedAt;
309
+ const emitted = parsed.version === 2
310
+ ? rawState.emitted === true
311
+ : false;
312
+ this.sessions.set(sessionId, {
313
+ lastEndedAt,
314
+ emitted,
315
+ ...(typeof rawState.encrypted === 'boolean' ? { encrypted: rawState.encrypted } : {}),
316
+ });
317
+ }
318
+ }
319
+ catch (error) {
320
+ this.warn(`Failed to load session idle state at ${this.options.statePath}`, error);
321
+ }
322
+ }
323
+ persistState() {
324
+ const sessions = {};
325
+ for (const [sessionId, state] of this.sessions) {
326
+ sessions[sessionId] = {
327
+ lastEndedAt: state.lastEndedAt,
328
+ emitted: state.emitted,
329
+ ...(typeof state.encrypted === 'boolean' ? { encrypted: state.encrypted } : {}),
330
+ };
331
+ }
332
+ try {
333
+ fs.mkdirSync(path.dirname(this.options.statePath), { recursive: true });
334
+ atomicWriteJson(this.options.statePath, { version: 2, sessions });
335
+ }
336
+ catch (error) {
337
+ this.warn(`Failed to persist session idle state at ${this.options.statePath}`, error);
338
+ }
339
+ }
340
+ warn(message, error) {
341
+ logger.warn(`[SessionIdleMonitor] ${message}${error === undefined ? '' : `: ${error instanceof Error ? error.message : String(error)}`}`);
342
+ }
343
+ }
344
+ function isSupportedSession(session) {
345
+ return session.chatType === 'group' || session.chatType === 'private';
346
+ }
347
+ function toIdleMs(seconds) {
348
+ if (seconds === undefined || !Number.isFinite(seconds) || seconds <= 0)
349
+ return undefined;
350
+ const idleMs = Math.max(MIN_SESSION_IDLE_SECONDS, seconds) * 1000;
351
+ return Number.isSafeInteger(idleMs) && idleMs > 0 ? idleMs : undefined;
352
+ }
353
+ function isTaskTerminalEvent(event) {
354
+ return event.type === 'task:completed' || event.type === 'task:error' || event.type === 'task:interrupted';
355
+ }