ideaco 1.1.5

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 (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,990 @@
1
+ /**
2
+ * Employee Lifecycle Manager
3
+ *
4
+ * Owns all per-employee behaviour that was previously scattered across
5
+ * GroupChatLoop:
6
+ *
7
+ * - Random-interval poll cycle (read group messages)
8
+ * - Inner monologue / flow-state (InnerMonologue)
9
+ * - "Should I speak?" decision (LLM call + fallback)
10
+ * - Anti-spam / rate-limiting
11
+ * - Self-check for stuck workflow nodes
12
+ * - Idle-chat topic initiation
13
+ * - Prompt building for dept-chat & work-chat
14
+ * - Agent memory (monologue summaries across rounds)
15
+ *
16
+ * The lifecycle is attached to a single Employee and receives a reference
17
+ * to the GroupChatLoop (the thin global coordinator) for event emission,
18
+ * company resolution, and cross-agent nudging.
19
+ */
20
+
21
+ import { v4 as uuidv4 } from 'uuid';
22
+ import path from 'path';
23
+ import { existsSync } from 'fs';
24
+ import {
25
+ PROMPT,
26
+ getTraitStyle,
27
+ getAgeStyle,
28
+ getFewShotExamples,
29
+ getFallbackReplies,
30
+ } from '../prompts.js';
31
+ import { robustJSONParse } from '../utils/json-parse.js';
32
+
33
+ // ─── File reference expansion ──────────────────────────────────────────
34
+ // Agent writes [[file:path/to/file]], we expand to [[file:deptId:path|displayName]]
35
+ const SIMPLE_FILE_REF = /\[\[file:([^\]|:]+)\]\]/g;
36
+ const INCOMPLETE_FILE_REF = /\[\[file:([^:]+):([^\]|]+)\]\]/g;
37
+
38
+ /**
39
+ * Expand short-form file references into full format for the frontend.
40
+ * [[file:src/index.js]] → [[file:dept123:src/index.js|index.js]]
41
+ * [[file:dept123:src/index.js]] → [[file:dept123:src/index.js|index.js]]
42
+ * Only creates clickable references for files that actually exist on disk.
43
+ * Returns { content, invalidRefs } so caller can provide feedback for bad references.
44
+ */
45
+ function expandFileReferences(content, departmentId, workspacePath) {
46
+ if (!content || !departmentId) return { content, invalidRefs: [] };
47
+ const invalidRefs = [];
48
+ // First: fix incomplete refs [[file:deptId:path]] → [[file:deptId:path|name]]
49
+ let expanded = content.replace(INCOMPLETE_FILE_REF, (_match, deptId, filePath) => {
50
+ const trimmed = filePath.trim();
51
+ if (workspacePath) {
52
+ const fullPath = path.join(workspacePath, trimmed);
53
+ if (!existsSync(fullPath)) {
54
+ invalidRefs.push(trimmed);
55
+ return trimmed;
56
+ }
57
+ }
58
+ const displayName = path.basename(trimmed);
59
+ return `[[file:${deptId}:${trimmed}|${displayName}]]`;
60
+ });
61
+ // Then: expand simple refs [[file:path]] → [[file:deptId:path|name]]
62
+ expanded = expanded.replace(SIMPLE_FILE_REF, (_match, filePath) => {
63
+ const trimmed = filePath.trim();
64
+ if (workspacePath) {
65
+ const fullPath = path.join(workspacePath, trimmed);
66
+ if (!existsSync(fullPath)) {
67
+ invalidRefs.push(trimmed);
68
+ return trimmed;
69
+ }
70
+ }
71
+ const displayName = path.basename(trimmed);
72
+ return `[[file:${departmentId}:${trimmed}|${displayName}]]`;
73
+ });
74
+ return { content: expanded, invalidRefs };
75
+ }
76
+
77
+ // ─── Default Config ────────────────────────────────────────────────────
78
+ const DEFAULT_CONFIG = {
79
+ pollIntervalMinMs: 10000, // 10 s
80
+ pollIntervalMaxMs: 300000, // 5 min
81
+ idleChatThresholdMs: 3600000, // 1 h
82
+ maxInnerMonologueLen: 5,
83
+ maxGroupMessagesPerTurn: 1,
84
+ debounceMs: 3000,
85
+ antiSpamWindowMs: 300000, // 5 min
86
+ antiSpamMaxMessages: 2,
87
+ cooldownAfterSpeakMs: 60000, // 60 s
88
+ selfCheckIntervalMs: 120000, // 120 s
89
+ stuckThresholdMs: 180000, // 3 min
90
+ // Department casual chat — relaxed
91
+ deptAntiSpamWindowMs: 600000,
92
+ deptAntiSpamMaxMessages: 4,
93
+ deptCooldownAfterSpeakMs: 30000,
94
+ deptIdleChatThresholdMs: 600000,
95
+ topicSaturationThreshold: 7,
96
+ };
97
+
98
+ // ─── InnerMonologue (flow state) ───────────────────────────────────────
99
+ export class InnerMonologue {
100
+ constructor(agentId, agentName, groupId) {
101
+ this.id = uuidv4();
102
+ this.agentId = agentId;
103
+ this.agentName = agentName;
104
+ this.groupId = groupId;
105
+ this.thoughts = [];
106
+ this.startedAt = new Date();
107
+ this.status = 'thinking'; // thinking | decided | done
108
+ this.decision = null;
109
+ }
110
+
111
+ addThought(content) {
112
+ this.thoughts.push({ id: uuidv4(), content, timestamp: new Date() });
113
+ }
114
+
115
+ toJSON() {
116
+ return {
117
+ id: this.id,
118
+ agentId: this.agentId,
119
+ agentName: this.agentName,
120
+ groupId: this.groupId,
121
+ thoughts: this.thoughts,
122
+ startedAt: this.startedAt,
123
+ status: this.status,
124
+ decision: this.decision,
125
+ };
126
+ }
127
+ }
128
+
129
+ // ─── EmployeeLifecycle ─────────────────────────────────────────────────
130
+ export class EmployeeLifecycle {
131
+ /**
132
+ * @param {import('./base-employee.js').Employee} employee
133
+ * @param {object} [config] Override individual config keys
134
+ */
135
+ constructor(employee, config = {}) {
136
+ this.employee = employee;
137
+ this.config = { ...DEFAULT_CONFIG, ...config };
138
+
139
+ // Per-group state key = groupId
140
+ this._lastReadIndex = new Map(); // groupId → number
141
+ this._lastProcessedVisible = new Map(); // groupId → number
142
+ this._recentSpeaks = new Map(); // groupId → Date[]
143
+ this._lastSelfCheck = new Map(); // groupId → Date
144
+ this._agentMemory = new Map(); // groupId → string[]
145
+ this._activeMonologues = new Map(); // groupId → InnerMonologue
146
+ this._monologueHistory = new Map(); // groupId → InnerMonologue[]
147
+ this._lastGroupActivity = new Map(); // groupId → Date
148
+
149
+ // Processing guards
150
+ this._processing = new Set(); // groupId
151
+
152
+ // Poll timer (single timer for the employee)
153
+ this._pollTimer = null;
154
+
155
+ // Back-reference to global coordinator (set externally)
156
+ this._coordinator = null;
157
+ }
158
+
159
+ // ──────────────────────────────────────────────────────────────────────
160
+ // Public API
161
+ // ──────────────────────────────────────────────────────────────────────
162
+
163
+ /** Attach the global coordinator (GroupChatLoop) so we can emit events & nudge peers. */
164
+ setCoordinator(coordinator) {
165
+ this._coordinator = coordinator;
166
+ }
167
+
168
+ /** Start the random-interval poll loop for this employee. */
169
+ start() {
170
+ if (this._pollTimer) return; // already running
171
+ this._scheduleNext();
172
+ console.log(` 🔄 [Lifecycle] ${this.employee.name} started (${this.config.pollIntervalMinMs}-${this.config.pollIntervalMaxMs}ms)`);
173
+ }
174
+
175
+ /** Stop polling. */
176
+ stop() {
177
+ if (this._pollTimer) {
178
+ clearTimeout(this._pollTimer);
179
+ this._pollTimer = null;
180
+ }
181
+ this._processing.clear();
182
+ this._recentSpeaks.clear();
183
+ this._lastSelfCheck.clear();
184
+ this._lastProcessedVisible.clear();
185
+ // NOTE: _agentMemory is NOT cleared — memories persist across stop/start
186
+ }
187
+
188
+ /** Trigger a delayed check for a specific group (e.g. when someone @mentions this employee). */
189
+ async triggerCheck(groupId) {
190
+ const delay = this._randomDelay();
191
+ console.log(` 📨 [Lifecycle] ${this.employee.name} will check ${groupId} in ${Math.round(delay / 1000)}s`);
192
+ setTimeout(async () => {
193
+ try {
194
+ if (!this._isRunning()) return;
195
+ await this._processGroupMessages(groupId, false);
196
+ } catch (err) {
197
+ console.error(` ❌ [Lifecycle] ${this.employee.name} delayed trigger error:`, err.message);
198
+ }
199
+ }, delay);
200
+ }
201
+
202
+ // ─── Query helpers (used by GroupChatLoop / API) ────────────────────
203
+
204
+ getActiveMonologue(groupId) {
205
+ return this._activeMonologues.get(groupId) || null;
206
+ }
207
+
208
+ getMonologueHistory(groupId, limit = 10) {
209
+ return (this._monologueHistory.get(groupId) || []).slice(-limit);
210
+ }
211
+
212
+ getActiveThinking() {
213
+ const result = [];
214
+ for (const [, monologue] of this._activeMonologues) {
215
+ if (monologue.status === 'thinking') {
216
+ result.push({
217
+ agentId: monologue.agentId,
218
+ agentName: monologue.agentName,
219
+ groupId: monologue.groupId,
220
+ startedAt: monologue.startedAt,
221
+ thoughtCount: monologue.thoughts.length,
222
+ status: 'thinking',
223
+ });
224
+ }
225
+ }
226
+ return result;
227
+ }
228
+
229
+ // ─── Serialization ──────────────────────────────────────────────────
230
+
231
+ serialize() {
232
+ const obj = (map) => {
233
+ const o = {};
234
+ for (const [k, v] of map) o[k] = v;
235
+ return o;
236
+ };
237
+ return {
238
+ lastReadIndex: obj(this._lastReadIndex),
239
+ lastProcessedVisible: obj(this._lastProcessedVisible),
240
+ agentMemory: obj(this._agentMemory),
241
+ };
242
+ }
243
+
244
+ restore(data) {
245
+ if (!data) return;
246
+ const load = (map, raw) => { if (raw) for (const [k, v] of Object.entries(raw)) map.set(k, v); };
247
+ load(this._lastReadIndex, data.lastReadIndex);
248
+ load(this._lastProcessedVisible, data.lastProcessedVisible);
249
+ load(this._agentMemory, data.agentMemory);
250
+ }
251
+
252
+ // ──────────────────────────────────────────────────────────────────────
253
+ // Internal — Poll Cycle
254
+ // ──────────────────────────────────────────────────────────────────────
255
+
256
+ _scheduleNext() {
257
+ if (!this._isRunning()) return;
258
+ const delay = this._randomDelay();
259
+ this._pollTimer = setTimeout(async () => {
260
+ try {
261
+ await this._pollCycle();
262
+ } catch (err) {
263
+ console.error(` ❌ [Lifecycle] ${this.employee.name} poll error:`, err.message);
264
+ }
265
+ this._scheduleNext();
266
+ }, delay);
267
+ }
268
+
269
+ async _pollCycle() {
270
+ if (!this._isRunning()) return;
271
+ const agent = this.employee;
272
+ if (agent.status === 'dismissed') return;
273
+
274
+ // Periodically clean expired short-term memories
275
+ agent.memory.cleanExpiredShortTerm();
276
+
277
+ const groups = this._getAgentGroups();
278
+ for (const group of groups) {
279
+ await this._checkGroupForAgent(group);
280
+ await this._selfCheckWorkflow(group);
281
+ }
282
+ }
283
+
284
+ // ──────────────────────────────────────────────────────────────────────
285
+ // Internal — Group discovery
286
+ // ──────────────────────────────────────────────────────────────────────
287
+
288
+ _getAgentGroups() {
289
+ const company = this._getCompany();
290
+ if (!company) return [];
291
+
292
+ const agent = this.employee;
293
+ const groups = [];
294
+
295
+ // 1. Requirement work groups
296
+ const requirements = company.requirementManager.listAll();
297
+ for (const req of requirements) {
298
+ if (req.status !== 'in_progress' && req.status !== 'planning') continue;
299
+ const dept = company.findDepartment(req.departmentId);
300
+ if (!dept || !dept.agents.has(agent.id)) continue;
301
+ groups.push({
302
+ id: req.id,
303
+ title: req.title,
304
+ departmentId: req.departmentId,
305
+ type: 'work',
306
+ messages: req.groupChat || [],
307
+ requirement: req,
308
+ });
309
+ }
310
+
311
+ // 2. Department general chat groups
312
+ for (const dept of company.departments.values()) {
313
+ if (!dept.agents.has(agent.id)) continue;
314
+ if (dept.status === 'disbanded') continue;
315
+ groups.push({
316
+ id: `dept-${dept.id}`,
317
+ title: `${dept.name} Department Chat`,
318
+ departmentId: dept.id,
319
+ type: 'chat',
320
+ messages: dept.groupChat || [],
321
+ department: dept,
322
+ });
323
+ }
324
+
325
+ return groups;
326
+ }
327
+
328
+ // ──────────────────────────────────────────────────────────────────────
329
+ // Internal — Check & process messages
330
+ // ──────────────────────────────────────────────────────────────────────
331
+
332
+ async _checkGroupForAgent(group) {
333
+ const groupId = group.id;
334
+ if (this._processing.has(groupId)) return;
335
+
336
+ const lastRead = this._lastReadIndex.get(groupId) || 0;
337
+ const messages = group.messages;
338
+
339
+ if (messages.length <= lastRead) {
340
+ if (group.type === 'chat') await this._maybeInitiateChat(group);
341
+ return;
342
+ }
343
+
344
+ const unreadMessages = messages.slice(lastRead);
345
+ this._lastReadIndex.set(groupId, messages.length);
346
+
347
+ const agent = this.employee;
348
+ const isMentioned = unreadMessages
349
+ .filter(msg => msg.visibility !== 'flow')
350
+ .some(msg => this._isMentionedInMessage(msg));
351
+
352
+ // Any group-visible message from others (including system summaries/reports)
353
+ // should trigger the monologue flow — the agent reads them, thinks, and
354
+ // may choose to stay silent. The key is to *enter* the thinking process.
355
+ const othersMessages = unreadMessages.filter(msg =>
356
+ msg.from?.id !== agent.id && msg.visibility !== 'flow'
357
+ );
358
+
359
+ if (othersMessages.length === 0 && !isMentioned) return;
360
+
361
+ await this._processGroupMessages(groupId, isMentioned);
362
+ }
363
+
364
+ /**
365
+ * Core logic — process unread messages for a group.
366
+ */
367
+ async _processGroupMessages(groupId, isMentioned = false) {
368
+ if (this._processing.has(groupId)) return;
369
+ this._processing.add(groupId);
370
+
371
+ const agent = this.employee;
372
+ const company = this._getCompany();
373
+
374
+ // NOTE: wakeUp follows lazy-loading principle.
375
+ // The employee will be automatically woken up on their first chat()
376
+ // call via _ensureSession(). No need to pre-initialize here.
377
+
378
+ try {
379
+ const isDeptChat = groupId.startsWith('dept-');
380
+ let dept, chatTarget, contextTitle;
381
+
382
+ let allVisibleMessages;
383
+ if (isDeptChat) {
384
+ const deptId = groupId.replace('dept-', '');
385
+ dept = company.findDepartment(deptId);
386
+ if (!dept) return;
387
+ chatTarget = dept;
388
+ contextTitle = `${dept.name} Department Chat`;
389
+ allVisibleMessages = (dept.groupChat || []).filter(m => m.visibility !== 'flow');
390
+ } else {
391
+ const requirement = company.requirementManager.get(groupId);
392
+ if (!requirement) return;
393
+ dept = company.findDepartment(requirement.departmentId);
394
+ if (!dept) return;
395
+ chatTarget = requirement;
396
+ contextTitle = requirement.title;
397
+ allVisibleMessages = (requirement.groupChat || []).filter(m => m.visibility !== 'flow');
398
+ }
399
+
400
+ // ── Per-employee context scene management ──
401
+ // Switch the employee's active context if the group has changed.
402
+ // For web agents this injects the scene prompt into the existing ChatGPT conversation
403
+ // without re-sending memory+prompt (which was done once at wake-up).
404
+ if (agent.switchContext) {
405
+ const contextType = isDeptChat ? 'dept-chat' : 'work-chat';
406
+ const members = dept.getMembers().map(a => `${a.name}(${a.role})`).join(', ');
407
+ const scenePrompt = isDeptChat
408
+ ? `You are now in the "${contextTitle}" group.\nMembers: ${members}\nThis is a casual department chat. Respond naturally in your personality.`
409
+ : `You are now in the "${contextTitle}" work group.\nMembers: ${members}\nThis is a task-focused discussion. Stay on topic and contribute professionally.`;
410
+ await agent.switchContext({
411
+ contextId: groupId,
412
+ contextType,
413
+ contextTitle,
414
+ scenePrompt,
415
+ });
416
+ }
417
+
418
+ // Distinguish read vs unread
419
+ const contextKey = groupId;
420
+ const lastProcessedCount = this._lastProcessedVisible.get(contextKey) || 0;
421
+ const readContext = allVisibleMessages.slice(0, lastProcessedCount).slice(-10);
422
+ const unreadNew = allVisibleMessages.slice(lastProcessedCount);
423
+ this._lastProcessedVisible.set(contextKey, allVisibleMessages.length);
424
+
425
+ let recentMessages = [
426
+ ...readContext.map(m => ({ ...m, _isRead: true })),
427
+ ...unreadNew.map(m => ({ ...m, _isRead: false })),
428
+ ];
429
+
430
+ if (unreadNew.length === 0 && !isMentioned) return;
431
+
432
+ // Jitter wait (5-20s)
433
+ const jitter = 5000 + Math.random() * 15000;
434
+ await this._delay(jitter);
435
+
436
+ // Re-fetch fresh messages after waiting
437
+ let freshVisibleMessages;
438
+ if (isDeptChat) {
439
+ freshVisibleMessages = (chatTarget.groupChat || []).filter(m => m.visibility !== 'flow');
440
+ } else {
441
+ freshVisibleMessages = (chatTarget.groupChat || []).filter(m => m.visibility !== 'flow');
442
+ }
443
+ if (freshVisibleMessages.length > allVisibleMessages.length) {
444
+ const extraMessages = freshVisibleMessages.slice(allVisibleMessages.length);
445
+ recentMessages = [
446
+ ...recentMessages,
447
+ ...extraMessages.map(m => ({ ...m, _isRead: false })),
448
+ ];
449
+ this._lastProcessedVisible.set(contextKey, freshVisibleMessages.length);
450
+ }
451
+
452
+ // Create flow state
453
+ const monologue = new InnerMonologue(agent.id, agent.name, groupId);
454
+ this._activeMonologues.set(groupId, monologue);
455
+
456
+ this._emit('monologue:start', {
457
+ agentId: agent.id, agentName: agent.name, groupId,
458
+ });
459
+
460
+ // LLM flow thinking
461
+ const thinkingResult = await this._agentThink(
462
+ { id: groupId, title: contextTitle }, dept, recentMessages, isMentioned, monologue
463
+ );
464
+
465
+ if (monologue.thoughts.length === 0 && thinkingResult.reason) {
466
+ monologue.addThought(thinkingResult.reason);
467
+ }
468
+
469
+ // Write inner monologue to group stream (boss peek)
470
+ const innerThoughtsContent = thinkingResult.innerThoughts || thinkingResult.reason || null;
471
+ if (innerThoughtsContent) {
472
+ chatTarget.addGroupMessage(
473
+ { id: agent.id, name: agent.name, avatar: agent.avatar, role: agent.role },
474
+ innerThoughtsContent,
475
+ 'monologue',
476
+ 'flow'
477
+ );
478
+ }
479
+
480
+ // Topic saturation gate
481
+ const topicSaturation = thinkingResult.topicSaturation || 0;
482
+ if (thinkingResult.shouldSpeak && topicSaturation >= this.config.topicSaturationThreshold && !isMentioned) {
483
+ console.log(` 🎯 [Lifecycle] ${agent.name} silenced by topic saturation (score=${topicSaturation})`);
484
+ thinkingResult.shouldSpeak = false;
485
+ thinkingResult.reason = `Topic saturation: ${topicSaturation}/10`;
486
+ monologue.addThought(PROMPT.monologue.topicSaturated(topicSaturation));
487
+ }
488
+
489
+ // Cooldown check
490
+ if (thinkingResult.shouldSpeak) {
491
+ const spamRecheck = this._getSpamInfo(groupId, isDeptChat);
492
+ if (spamRecheck.isOnCooldown) {
493
+ console.log(` 🕐 [Lifecycle] ${agent.name} on cooldown, suppressing`);
494
+ thinkingResult.shouldSpeak = false;
495
+ thinkingResult.reason = 'On cooldown after recent speak';
496
+ monologue.addThought(PROMPT.monologue.cooldownSilence);
497
+ }
498
+ }
499
+
500
+ // Send messages
501
+ if (thinkingResult.shouldSpeak) {
502
+ const messagesToSend = thinkingResult.messages || [];
503
+ for (const msg of messagesToSend.slice(0, this.config.maxGroupMessagesPerTurn)) {
504
+ // Expand [[file:path]] → [[file:deptId:path|name]] for frontend rendering
505
+ const { content: expandedContent, invalidRefs } = expandFileReferences(msg.content, dept.id, dept.workspacePath);
506
+ chatTarget.addGroupMessage(
507
+ { id: agent.id, name: agent.name, avatar: agent.avatar, role: agent.role },
508
+ expandedContent,
509
+ 'message'
510
+ );
511
+ monologue.addThought(`[Sent to group] ${expandedContent}`);
512
+
513
+ // Auto-feedback: notify agent about invalid file references
514
+ if (invalidRefs.length > 0) {
515
+ const invalidList = invalidRefs.map(f => ` - ${f}`).join('\n');
516
+ chatTarget.addGroupMessage(
517
+ { id: 'system', name: 'System', role: 'system' },
518
+ `⚠️ @[${agent.id}] File reference error: the following files do not exist in workspace:\n${invalidList}\nPlease use workspace_files or file_search tool to check available files before referencing them.`,
519
+ 'message', null, { auto: true }
520
+ );
521
+ }
522
+ this._lastGroupActivity.set(groupId, new Date());
523
+ this._recordSpeak(groupId);
524
+
525
+ // Nudge other group members
526
+ const allGroupMembers = dept.getMembers();
527
+ for (const member of allGroupMembers) {
528
+ if (member.id === agent.id) continue;
529
+ const nudgeDelay = this._randomDelay();
530
+ setTimeout(() => {
531
+ this._nudgePeer(member.id, groupId);
532
+ }, nudgeDelay);
533
+ }
534
+ }
535
+ // Save company state
536
+ company.save();
537
+ }
538
+
539
+ // Finalise monologue
540
+ monologue.status = 'done';
541
+ monologue.finishedAt = Date.now();
542
+ monologue.decision = thinkingResult.shouldSpeak ? 'spoke' : 'silent';
543
+
544
+ if (!this._monologueHistory.has(groupId)) this._monologueHistory.set(groupId, []);
545
+ const history = this._monologueHistory.get(groupId);
546
+ history.push(monologue);
547
+ if (history.length > 20) this._monologueHistory.set(groupId, history.slice(-20));
548
+
549
+ this._activeMonologues.delete(groupId);
550
+
551
+ // Save agent memory — now uses the structured Memory system.
552
+ // The AI-driven memoryOps (processed in _agentThink) handle long-term/short-term memory.
553
+ // Here we still keep a lightweight _agentMemory for backward compat (e.g. monologue peek).
554
+ if (!this._agentMemory.has(groupId)) this._agentMemory.set(groupId, []);
555
+ const legacyMemory = this._agentMemory.get(groupId);
556
+ const memoryThoughts = monologue.thoughts
557
+ .filter(t => !t.content.startsWith('[Sent to group]') && !t.content.startsWith('[Self-regulation]'))
558
+ .map(t => t.content);
559
+ if (memoryThoughts.length > 0) {
560
+ const summary = memoryThoughts[memoryThoughts.length - 1];
561
+ const action = monologue.decision === 'spoke' ? '[spoke]' : '[silent]';
562
+ legacyMemory.push(`${action} ${summary.slice(0, 200)}`);
563
+ if (legacyMemory.length > 10) this._agentMemory.set(groupId, legacyMemory.slice(-10));
564
+ }
565
+
566
+ this._emit('monologue:end', {
567
+ agentId: agent.id, agentName: agent.name, groupId,
568
+ decision: monologue.decision,
569
+ thoughtCount: monologue.thoughts.length,
570
+ thoughts: monologue.thoughts,
571
+ reason: thinkingResult.reason || '',
572
+ });
573
+
574
+ } catch (error) {
575
+ console.error(` ❌ [Lifecycle] ${agent.name} process error in ${groupId}:`, error.message);
576
+ this._activeMonologues.delete(groupId);
577
+ } finally {
578
+ this._processing.delete(groupId);
579
+ }
580
+ }
581
+
582
+ // ──────────────────────────────────────────────────────────────────────
583
+ // Internal — LLM thinking
584
+ // ──────────────────────────────────────────────────────────────────────
585
+
586
+ async _agentThink(requirement, dept, recentMessages, isMentioned, monologue) {
587
+ const agent = this.employee;
588
+ const members = dept.getMembers().map(a => ({ id: a.id, name: a.name, role: a.role }));
589
+
590
+ // Format messages — includes sender id, name, time
591
+ const readMessages = recentMessages.filter(m => m._isRead);
592
+ const unreadMessages = recentMessages.filter(m => !m._isRead);
593
+
594
+ const formatMsg = (msg) => {
595
+ const senderName = msg.from?.name || 'Unknown';
596
+ const senderId = msg.from?.id || 'unknown';
597
+ const time = new Date(msg.time).toLocaleTimeString('zh', { hour: '2-digit', minute: '2-digit' });
598
+ if (msg.type === 'system') return `[System] ${msg.content}`;
599
+ const selfMark = msg.from?.id === agent.id ? ' (you)' : '';
600
+ return `[${time}] ${senderName}(${senderId})${selfMark}: ${msg.content}`;
601
+ };
602
+
603
+ // Dedupe warning
604
+ const colleagueReplies = unreadMessages.filter(m =>
605
+ m.from?.id !== agent.id && m.from?.id !== 'boss' && members.some(mem => mem.id === m.from?.id)
606
+ );
607
+ const dedupeWarning = colleagueReplies.length > 0
608
+ ? PROMPT.context.dedupeWarning(
609
+ colleagueReplies.length,
610
+ colleagueReplies.map(m => `- ${m.from?.name}: "${(m.content || '').slice(0, 80)}"`).join('\n')
611
+ )
612
+ : '';
613
+
614
+ // Random angle seed
615
+ const angles = PROMPT.angles;
616
+ const myAngle = angles[Math.floor(Math.random() * angles.length)];
617
+ const angleHint = PROMPT.context.angleHint(myAngle);
618
+
619
+ let chatContext = '';
620
+ if (readMessages.length > 0) {
621
+ chatContext += PROMPT.context.readHeader + '\n';
622
+ chatContext += readMessages.map(formatMsg).join('\n');
623
+ chatContext += '\n\n';
624
+ }
625
+ if (unreadMessages.length > 0) {
626
+ chatContext += PROMPT.context.unreadHeader + '\n';
627
+ chatContext += unreadMessages.map(formatMsg).join('\n');
628
+ } else {
629
+ chatContext += PROMPT.context.noNewMessages;
630
+ }
631
+ chatContext += dedupeWarning;
632
+ chatContext += angleHint;
633
+
634
+ // Build structured memory context from the employee's Memory system
635
+ // This includes: rolling history summary + long-term memories + short-term memories + relationship impressions
636
+ const participantIds = members.filter(m => m.id !== agent.id).map(m => m.id);
637
+ const memoryContext = agent.memory.buildFullContext(requirement.id, participantIds);
638
+
639
+ const p = agent.personality;
640
+
641
+ // Anti-spam context
642
+ const groupId = requirement.id;
643
+ const isDeptChat = groupId.startsWith('dept-');
644
+ const spamInfo = this._getSpamInfo(groupId, isDeptChat);
645
+
646
+ const systemPrompt = isDeptChat
647
+ ? this._buildDeptChatPrompt(p, requirement, members, memoryContext, spamInfo, isMentioned)
648
+ : this._buildWorkChatPrompt(p, requirement, members, memoryContext, spamInfo, isMentioned);
649
+
650
+ // Thinking peers
651
+ const thinkingPeers = this._getThinkingPeers(groupId);
652
+ const thinkingInfo = thinkingPeers.length > 0
653
+ ? PROMPT.context.thinkingPeers(thinkingPeers.join(', '))
654
+ : '';
655
+
656
+ const userPrompt = isDeptChat
657
+ ? PROMPT.userPrompt.deptChat(chatContext, thinkingInfo, agent.name, agent.age, agent.personality.trait)
658
+ : PROMPT.userPrompt.workChat(chatContext, thinkingInfo, agent.name, agent.age, agent.personality.trait);
659
+
660
+ try {
661
+ if (!agent.canChat()) {
662
+ return this._fallbackThink(groupId, isMentioned, recentMessages);
663
+ }
664
+
665
+ const response = await agent.chat([
666
+ { role: 'system', content: systemPrompt },
667
+ { role: 'user', content: userPrompt },
668
+ ], { temperature: 0.95, maxTokens: 1024 });
669
+
670
+ const rawContent = response.content || '';
671
+ const result = robustJSONParse(rawContent);
672
+
673
+ const thoughtContent = (result.innerThoughts && result.innerThoughts.trim())
674
+ ? result.innerThoughts.trim()
675
+ : result.reason
676
+ ? `[Inner thought] ${result.reason}`
677
+ : `[Read group messages, processing...]`;
678
+ monologue.addThought(thoughtContent);
679
+
680
+ // ── Process AI-driven memory management ──
681
+ // 1. Rolling history summary: AI compresses old messages into a summary
682
+ if (result.memorySummary && typeof result.memorySummary === 'string' && result.memorySummary.trim()) {
683
+ agent.memory.updateHistorySummary(groupId, result.memorySummary.trim());
684
+ console.log(` 📜 [Lifecycle] ${agent.name} updated history summary for ${groupId} (${result.memorySummary.trim().length} chars)`);
685
+ }
686
+ // 2. Memory operations: AI adds/updates/deletes its own memories
687
+ if (result.memoryOps && Array.isArray(result.memoryOps) && result.memoryOps.length > 0) {
688
+ const memResult = agent.memory.processMemoryOps(result.memoryOps);
689
+ console.log(` 🧠 [Lifecycle] ${agent.name} memory ops: +${memResult.added} ~${memResult.updated} -${memResult.deleted}`);
690
+ }
691
+ // 3. Relationship impressions: AI updates its personal views of colleagues
692
+ if (result.relationshipOps && Array.isArray(result.relationshipOps) && result.relationshipOps.length > 0) {
693
+ const relResult = agent.memory.processRelationshipOps(result.relationshipOps);
694
+ console.log(` 👥 [Lifecycle] ${agent.name} relationship updates: ${relResult.updated}`);
695
+ }
696
+
697
+ // Anti-spam gate
698
+ const spamCheck = this._getSpamInfo(requirement.id, isDeptChat);
699
+ const maxMessages = isDeptChat ? this.config.deptAntiSpamMaxMessages : this.config.antiSpamMaxMessages;
700
+ if (result.shouldSpeak && spamCheck.recentCount >= maxMessages) {
701
+ console.log(` 🔇 [Lifecycle] ${agent.name} suppressed (anti-spam: ${spamCheck.recentCount} msgs)`);
702
+ result.shouldSpeak = false;
703
+ result.reason = `Anti-spam: already sent ${spamCheck.recentCount} messages recently`;
704
+ result.messages = [];
705
+ monologue.addThought(`[Self-regulation] I wanted to speak but I've been too active recently. Better stay quiet.`);
706
+ }
707
+
708
+ if (result.messages && result.messages.length > 1) {
709
+ result.messages = [result.messages[0]];
710
+ }
711
+
712
+ return {
713
+ shouldSpeak: !!result.shouldSpeak,
714
+ messages: (result.messages || []).filter(m => m.content && m.content.trim()),
715
+ reason: result.reason || '',
716
+ innerThoughts: thoughtContent,
717
+ topicSaturation: typeof result.topicSaturation === 'number' ? result.topicSaturation : 0,
718
+ };
719
+
720
+ } catch (error) {
721
+ console.warn(` ⚠️ [Lifecycle] ${agent.name} LLM think error:`, error.message);
722
+ return this._fallbackThink(groupId, isMentioned, recentMessages);
723
+ }
724
+ }
725
+
726
+ // ──────────────────────────────────────────────────────────────────────
727
+ // Internal — Prompt building
728
+ // ──────────────────────────────────────────────────────────────────────
729
+
730
+ _buildDeptChatPrompt(p, requirement, members, memoryContext, spamInfo, isMentioned) {
731
+ const agent = this.employee;
732
+ const genderLabel = PROMPT.genderLabel[agent.gender] || PROMPT.genderLabel.male;
733
+ const ageStyle = getAgeStyle(agent.age);
734
+ const traitStyle = getTraitStyle(p.trait);
735
+ const fewShot = getFewShotExamples(p.trait);
736
+ const memberList = members.map(m => `${m.name}(${m.role})`).join(', ');
737
+ const pt = PROMPT.deptChat;
738
+ return `${traitStyle}
739
+
740
+ ${pt.intro(agent.name, genderLabel, agent.age, agent.role, p.tone, p.quirk, agent.signature)}
741
+
742
+ ${pt.ageIntro}
743
+ ${ageStyle}
744
+
745
+ ---
746
+
747
+ ${pt.groupContext(requirement.title, memberList)}
748
+ ${memoryContext}
749
+
750
+ ${pt.examplesHeader}
751
+
752
+ ${fewShot}
753
+
754
+ ${pt.rules(spamInfo.recentCount, isMentioned)}
755
+
756
+ ${pt.topicSaturation}
757
+
758
+ ${pt.outputFormat}
759
+
760
+ ${pt.antiAIWarning(agent.age)}`;
761
+ }
762
+
763
+ _buildWorkChatPrompt(p, requirement, members, memoryContext, spamInfo, isMentioned) {
764
+ const agent = this.employee;
765
+ const genderLabel = PROMPT.genderLabel[agent.gender] || PROMPT.genderLabel.male;
766
+ const ageStyle = getAgeStyle(agent.age);
767
+ const traitStyle = getTraitStyle(p.trait);
768
+ const fewShot = getFewShotExamples(p.trait);
769
+ const memberList = members.map(m => `${m.name}(${m.role})`).join(', ');
770
+ const pt = PROMPT.workChat;
771
+ return `${traitStyle}
772
+
773
+ ${pt.intro(agent.name, genderLabel, agent.age, agent.role, p.tone, p.quirk, agent.signature)}
774
+
775
+ ${pt.ageIntro}
776
+ ${ageStyle}
777
+
778
+ ---
779
+
780
+ ${pt.groupContext(requirement.title, memberList)}
781
+ ${memoryContext}
782
+
783
+ ${pt.examplesHeader}
784
+
785
+ ${fewShot}
786
+
787
+ ${pt.shouldSpeak}
788
+
789
+ ${pt.shouldNotSpeak(spamInfo.recentCount, spamInfo.isOnCooldown, isMentioned)}
790
+
791
+ ${pt.topicSaturation}
792
+
793
+ ${pt.outputFormat}
794
+
795
+ ${pt.antiAIWarning(agent.age)}`;
796
+ }
797
+
798
+ // ──────────────────────────────────────────────────────────────────────
799
+ // Internal — Fallback thinking
800
+ // ──────────────────────────────────────────────────────────────────────
801
+
802
+ _fallbackThink(groupId, isMentioned, recentMessages) {
803
+ const agent = this.employee;
804
+ const isDeptChat = groupId.startsWith('dept-');
805
+ const spamCheck = this._getSpamInfo(groupId, isDeptChat);
806
+ const maxMessages = isDeptChat ? this.config.deptAntiSpamMaxMessages : this.config.antiSpamMaxMessages;
807
+ if (spamCheck.recentCount >= maxMessages) {
808
+ return { shouldSpeak: false, messages: [], reason: 'Anti-spam: too many recent messages' };
809
+ }
810
+
811
+ const trait = agent.personality?.trait || '';
812
+ const replies = getFallbackReplies(trait);
813
+
814
+ if (isDeptChat) {
815
+ const lastMsg = recentMessages[recentMessages.length - 1];
816
+ if (lastMsg && lastMsg.from?.id !== agent.id) {
817
+ return { shouldSpeak: true, messages: [{ content: replies.dept }], reason: 'Department chat fallback' };
818
+ }
819
+ }
820
+
821
+ const lastMsg = recentMessages[recentMessages.length - 1];
822
+ if (lastMsg && lastMsg.from?.id === 'boss') {
823
+ return { shouldSpeak: true, messages: [{ content: replies.boss }], reason: 'Boss message fallback' };
824
+ }
825
+ if (isMentioned) {
826
+ return { shouldSpeak: true, messages: [{ content: replies.mention }], reason: 'Mentioned fallback' };
827
+ }
828
+
829
+ return { shouldSpeak: false, messages: [], reason: 'LLM unavailable, staying silent' };
830
+ }
831
+
832
+ // ──────────────────────────────────────────────────────────────────────
833
+ // Internal — Anti-spam
834
+ // ──────────────────────────────────────────────────────────────────────
835
+
836
+ _recordSpeak(groupId) {
837
+ if (!this._recentSpeaks.has(groupId)) this._recentSpeaks.set(groupId, []);
838
+ this._recentSpeaks.get(groupId).push(Date.now());
839
+ }
840
+
841
+ _getSpamInfo(groupId, isDeptChat = false) {
842
+ const timestamps = this._recentSpeaks.get(groupId) || [];
843
+ const now = Date.now();
844
+ const windowMs = isDeptChat ? this.config.deptAntiSpamWindowMs : this.config.antiSpamWindowMs;
845
+ const cooldownMs = isDeptChat ? this.config.deptCooldownAfterSpeakMs : this.config.cooldownAfterSpeakMs;
846
+ const recentTimestamps = timestamps.filter(t => t > now - windowMs);
847
+ this._recentSpeaks.set(groupId, recentTimestamps);
848
+
849
+ return {
850
+ recentCount: recentTimestamps.length,
851
+ isOnCooldown: recentTimestamps.some(t => t > now - cooldownMs),
852
+ };
853
+ }
854
+
855
+ // ──────────────────────────────────────────────────────────────────────
856
+ // Internal — Workflow self-check
857
+ // ──────────────────────────────────────────────────────────────────────
858
+
859
+ async _selfCheckWorkflow(group) {
860
+ if (!group.requirement?.workflow?.nodes) return;
861
+
862
+ const agent = this.employee;
863
+ const groupId = group.id;
864
+ const now = Date.now();
865
+ const lastCheck = this._lastSelfCheck.get(groupId) || 0;
866
+ if (now - lastCheck < this.config.selfCheckIntervalMs) return;
867
+
868
+ const nodes = group.requirement.workflow.nodes;
869
+ const myStuckNodes = nodes.filter(n => {
870
+ if (n.assigneeId !== agent.id && n.reviewerId !== agent.id) return false;
871
+ if (!['running', 'reviewing', 'revision'].includes(n.status)) return false;
872
+ if (!n.startedAt) return false;
873
+ return now - new Date(n.startedAt).getTime() > this.config.stuckThresholdMs;
874
+ });
875
+ if (myStuckNodes.length === 0) return;
876
+
877
+ this._lastSelfCheck.set(groupId, now);
878
+
879
+ const recentGroupMessages = (group.requirement.groupChat || [])
880
+ .filter(m => m.visibility !== 'flow').slice(-10);
881
+ const myRecentMessages = recentGroupMessages.filter(m => m.from?.id === agent.id && m.type === 'message');
882
+ if (myRecentMessages.length > 0) {
883
+ const lastMsgTime = new Date(myRecentMessages[myRecentMessages.length - 1].time).getTime();
884
+ if (now - lastMsgTime < this.config.selfCheckIntervalMs * 2) return;
885
+ }
886
+
887
+ const spamCheck = this._getSpamInfo(groupId);
888
+ if (spamCheck.recentCount >= this.config.antiSpamMaxMessages) return;
889
+
890
+ for (const node of myStuckNodes.slice(0, 1)) {
891
+ const elapsed = Math.round((now - new Date(node.startedAt).getTime()) / 1000);
892
+ const isReviewer = node.reviewerId === agent.id;
893
+
894
+ let checkInContent;
895
+ if (isReviewer && node.status === 'reviewing') {
896
+ checkInContent = `🔍 Still reviewing "${node.title}" (${elapsed}s elapsed). Working on it...`;
897
+ } else if (node.status === 'revision') {
898
+ checkInContent = `✏️ Working on revisions for "${node.title}" (${elapsed}s elapsed). Making progress...`;
899
+ } else {
900
+ checkInContent = `⚙️ Still working on "${node.title}" (${elapsed}s elapsed). Will update when I have results.`;
901
+ }
902
+
903
+ group.requirement.addGroupMessage(
904
+ { id: agent.id, name: agent.name, avatar: agent.avatar, role: agent.role },
905
+ checkInContent,
906
+ 'message', null, { auto: true }
907
+ );
908
+
909
+ this._recordSpeak(groupId);
910
+ this._lastGroupActivity.set(groupId, new Date());
911
+ console.log(` 🔔 [Lifecycle] ${agent.name} self-check: ${checkInContent}`);
912
+ }
913
+ }
914
+
915
+ // ──────────────────────────────────────────────────────────────────────
916
+ // Internal — Idle chat initiation
917
+ // ──────────────────────────────────────────────────────────────────────
918
+
919
+ async _maybeInitiateChat(group) {
920
+ const lastActivity = this._lastGroupActivity.get(group.id);
921
+ if (!lastActivity) return;
922
+
923
+ const idleMs = Date.now() - lastActivity.getTime();
924
+ const threshold = group.type === 'chat' ? this.config.deptIdleChatThresholdMs : this.config.idleChatThresholdMs;
925
+ if (idleMs < threshold) return;
926
+ if (group.type !== 'chat') return;
927
+ if (Math.random() > 0.15) return;
928
+
929
+ console.log(` 💬 [Lifecycle] ${this.employee.name} considering topic in idle group ${group.title}`);
930
+ await this._processGroupMessages(group.id, false);
931
+ }
932
+
933
+ // ──────────────────────────────────────────────────────────────────────
934
+ // Internal — Mention detection
935
+ // ──────────────────────────────────────────────────────────────────────
936
+
937
+ _isMentionedInMessage(message) {
938
+ const agent = this.employee;
939
+ if (!message.content || typeof message.content !== 'string') return false;
940
+ const content = message.content;
941
+ if (content.includes(`@[${agent.id}]`)) return true;
942
+ if (content.includes(`@${agent.name}`)) return true;
943
+ if (content.includes('@all') || content.includes('@everyone')) return true;
944
+ return false;
945
+ }
946
+
947
+ // ──────────────────────────────────────────────────────────────────────
948
+ // Internal — Helpers
949
+ // ──────────────────────────────────────────────────────────────────────
950
+
951
+ _isRunning() {
952
+ return this._coordinator?.running ?? false;
953
+ }
954
+
955
+ _getCompany() {
956
+ return this._coordinator?.company ?? null;
957
+ }
958
+
959
+ _randomDelay() {
960
+ return this.config.pollIntervalMinMs +
961
+ Math.random() * (this.config.pollIntervalMaxMs - this.config.pollIntervalMinMs);
962
+ }
963
+
964
+ _delay(ms) {
965
+ return new Promise(resolve => setTimeout(resolve, ms));
966
+ }
967
+
968
+ _emit(event, data) {
969
+ this._coordinator?.emit(event, data);
970
+ }
971
+
972
+ /** Nudge another employee to check a group via the coordinator. */
973
+ _nudgePeer(peerId, groupId) {
974
+ this._coordinator?.nudgeAgent(peerId, groupId);
975
+ }
976
+
977
+ /** Get names of peers currently thinking in the same group (via coordinator). */
978
+ _getThinkingPeers(groupId) {
979
+ if (!this._coordinator) return [];
980
+ const peers = [];
981
+ // Ask coordinator for all active monologues in this group (from all employees)
982
+ const allThinking = this._coordinator.getActiveThinkingAgents();
983
+ for (const t of allThinking) {
984
+ if (t.groupId === groupId && t.agentId !== this.employee.id) {
985
+ peers.push(t.agentName);
986
+ }
987
+ }
988
+ return peers;
989
+ }
990
+ }