let-them-talk 5.2.5 → 5.4.0

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 (166) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +158 -592
  3. package/SECURITY.md +3 -3
  4. package/USAGE.md +151 -0
  5. package/agent-contracts.js +447 -0
  6. package/api-agents.js +760 -0
  7. package/autonomy/decision-v2.js +380 -0
  8. package/autonomy/watchdog-policy.js +572 -0
  9. package/cli.js +454 -298
  10. package/conversation-templates/autonomous-feature.json +83 -22
  11. package/conversation-templates/code-review.json +69 -21
  12. package/conversation-templates/debug-squad.json +69 -21
  13. package/conversation-templates/feature-build.json +69 -21
  14. package/conversation-templates/research-write.json +69 -21
  15. package/dashboard.html +3148 -174
  16. package/dashboard.js +823 -786
  17. package/data-dir.js +58 -0
  18. package/docs/architecture/branch-semantics.md +157 -0
  19. package/docs/architecture/canonical-event-schema.md +88 -0
  20. package/docs/architecture/markdown-workspace.md +183 -0
  21. package/docs/architecture/runtime-contract.md +459 -0
  22. package/docs/architecture/runtime-migration-hardening.md +64 -0
  23. package/events/hooks.js +154 -0
  24. package/events/log.js +457 -0
  25. package/events/replay.js +33 -0
  26. package/events/schema.js +432 -0
  27. package/managed-team-integration.js +261 -0
  28. package/office/agents.js +704 -597
  29. package/office/animation.js +1 -1
  30. package/office/assets/arcade-cabinet.js +141 -0
  31. package/office/assets/archway.js +77 -0
  32. package/office/assets/bar-counter.js +91 -0
  33. package/office/assets/bar-stool.js +71 -0
  34. package/office/assets/beanbag.js +64 -0
  35. package/office/assets/bench.js +99 -0
  36. package/office/assets/bollard.js +87 -0
  37. package/office/assets/cactus.js +100 -0
  38. package/office/assets/carpet-tile.js +46 -0
  39. package/office/assets/chair.js +123 -0
  40. package/office/assets/chandelier.js +107 -0
  41. package/office/assets/coffee-machine.js +95 -0
  42. package/office/assets/coffee-table.js +81 -0
  43. package/office/assets/column.js +95 -0
  44. package/office/assets/desk-lamp.js +102 -0
  45. package/office/assets/desk.js +76 -0
  46. package/office/assets/dining-table.js +105 -0
  47. package/office/assets/door.js +70 -0
  48. package/office/assets/dual-monitor.js +72 -0
  49. package/office/assets/fence.js +76 -0
  50. package/office/assets/filing-cabinet.js +111 -0
  51. package/office/assets/floor-lamp.js +69 -0
  52. package/office/assets/floor-tile.js +54 -0
  53. package/office/assets/flower-pot.js +76 -0
  54. package/office/assets/foosball.js +95 -0
  55. package/office/assets/fridge.js +99 -0
  56. package/office/assets/gaming-chair.js +154 -0
  57. package/office/assets/gaming-desk.js +105 -0
  58. package/office/assets/glass-door.js +72 -0
  59. package/office/assets/glass-wall.js +64 -0
  60. package/office/assets/half-wall.js +49 -0
  61. package/office/assets/hanging-plant.js +112 -0
  62. package/office/assets/index.js +151 -0
  63. package/office/assets/indoor-tree.js +90 -0
  64. package/office/assets/l-sofa.js +153 -0
  65. package/office/assets/marble-floor.js +64 -0
  66. package/office/assets/materials.js +40 -0
  67. package/office/assets/meeting-table.js +88 -0
  68. package/office/assets/microwave.js +94 -0
  69. package/office/assets/monitor.js +67 -0
  70. package/office/assets/neon-strip.js +73 -0
  71. package/office/assets/painting.js +84 -0
  72. package/office/assets/palm-tree.js +108 -0
  73. package/office/assets/pc-tower.js +91 -0
  74. package/office/assets/pendant-light.js +67 -0
  75. package/office/assets/ping-pong.js +114 -0
  76. package/office/assets/plant.js +72 -0
  77. package/office/assets/planter-box.js +95 -0
  78. package/office/assets/pool-table.js +94 -0
  79. package/office/assets/printer.js +113 -0
  80. package/office/assets/reception-desk.js +133 -0
  81. package/office/assets/rug.js +78 -0
  82. package/office/assets/sculpture.js +85 -0
  83. package/office/assets/server-rack.js +98 -0
  84. package/office/assets/sink.js +109 -0
  85. package/office/assets/sofa.js +106 -0
  86. package/office/assets/speaker.js +83 -0
  87. package/office/assets/spotlight.js +83 -0
  88. package/office/assets/street-lamp.js +97 -0
  89. package/office/assets/trash-can.js +83 -0
  90. package/office/assets/treadmill.js +126 -0
  91. package/office/assets/trophy.js +89 -0
  92. package/office/assets/tv-screen.js +79 -0
  93. package/office/assets/vase.js +84 -0
  94. package/office/assets/wall-clock.js +84 -0
  95. package/office/assets/wall.js +53 -0
  96. package/office/assets/water-cooler.js +146 -0
  97. package/office/assets/whiteboard.js +115 -0
  98. package/office/assets.js +3 -431
  99. package/office/builder.js +791 -355
  100. package/office/campus-env.js +1012 -1119
  101. package/office/environment.js +2 -0
  102. package/office/gallery.js +997 -0
  103. package/office/index.js +165 -61
  104. package/office/navigation.js +173 -152
  105. package/office/player.js +178 -68
  106. package/office/robot-character.js +272 -0
  107. package/office/spectator-camera.js +33 -10
  108. package/office/state.js +2 -0
  109. package/office/world-save.js +35 -4
  110. package/package.json +57 -3
  111. package/providers/comfyui.js +383 -0
  112. package/providers/dalle.js +79 -0
  113. package/providers/gemini.js +181 -0
  114. package/providers/ollama.js +184 -0
  115. package/providers/replicate.js +115 -0
  116. package/providers/zai.js +183 -0
  117. package/runtime-descriptor.js +270 -0
  118. package/scripts/check-agent-contract-advisory.js +132 -0
  119. package/scripts/check-api-agent-parity.js +277 -0
  120. package/scripts/check-autonomy-v2-decision.js +207 -0
  121. package/scripts/check-autonomy-v2-execution.js +588 -0
  122. package/scripts/check-autonomy-v2-watchdog.js +224 -0
  123. package/scripts/check-branch-fork-snapshot.js +337 -0
  124. package/scripts/check-branch-isolation.js +787 -0
  125. package/scripts/check-branch-semantics.js +139 -0
  126. package/scripts/check-dashboard-control-plane.js +1304 -0
  127. package/scripts/check-docs-onboarding.js +490 -0
  128. package/scripts/check-event-schema.js +276 -0
  129. package/scripts/check-evidence-completion.js +239 -0
  130. package/scripts/check-invariants.js +992 -0
  131. package/scripts/check-lifecycle-hooks.js +525 -0
  132. package/scripts/check-managed-team-integration.js +166 -0
  133. package/scripts/check-markdown-workspace-export.js +548 -0
  134. package/scripts/check-markdown-workspace-safety.js +347 -0
  135. package/scripts/check-markdown-workspace.js +136 -0
  136. package/scripts/check-message-replay.js +429 -0
  137. package/scripts/check-migration-hardening.js +300 -0
  138. package/scripts/check-performance-indexing.js +272 -0
  139. package/scripts/check-provider-capabilities.js +316 -0
  140. package/scripts/check-runtime-contract.js +109 -0
  141. package/scripts/check-session-aware-context.js +172 -0
  142. package/scripts/check-session-lifecycle.js +210 -0
  143. package/scripts/export-markdown-workspace.js +84 -0
  144. package/scripts/fixtures/message-replay/clean.jsonl +2 -0
  145. package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
  146. package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
  147. package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
  148. package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
  149. package/scripts/migrate-legacy-to-canonical.js +201 -0
  150. package/scripts/run-verification-suite.js +242 -0
  151. package/scripts/sync-packaged-docs.js +69 -0
  152. package/server.js +9546 -7214
  153. package/state/agents.js +161 -0
  154. package/state/canonical.js +3068 -0
  155. package/state/dashboard-queries.js +441 -0
  156. package/state/evidence.js +56 -0
  157. package/state/io.js +69 -0
  158. package/state/markdown-workspace.js +951 -0
  159. package/state/messages.js +669 -0
  160. package/state/sessions.js +683 -0
  161. package/state/tasks-workflows.js +92 -0
  162. package/templates/debate.json +2 -2
  163. package/templates/managed.json +4 -4
  164. package/templates/pair.json +2 -2
  165. package/templates/review.json +2 -2
  166. package/templates/team.json +3 -3
@@ -0,0 +1,683 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const SESSION_MANIFEST_SCHEMA_VERSION = 1;
5
+ const SESSION_INDEX_SCHEMA_VERSION = 1;
6
+ const DEFAULT_STALE_THRESHOLD_MS = 60000;
7
+
8
+ const SESSION_STATES = Object.freeze([
9
+ 'active',
10
+ 'interrupted',
11
+ 'completed',
12
+ 'failed',
13
+ 'abandoned',
14
+ ]);
15
+
16
+ const TERMINAL_SESSION_EVENTS = Object.freeze({
17
+ interrupted: 'session.interrupted',
18
+ completed: 'session.completed',
19
+ failed: 'session.failed',
20
+ abandoned: 'session.abandoned',
21
+ });
22
+
23
+ function fallbackSessionId() {
24
+ return `session_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
25
+ }
26
+
27
+ function toTimestamp(value) {
28
+ const timestamp = Date.parse(value || '');
29
+ return Number.isFinite(timestamp) ? timestamp : 0;
30
+ }
31
+
32
+ function compareSessionsByRecency(left, right) {
33
+ return toTimestamp(left && (left.updated_at || left.last_activity_at || left.resumed_at || left.started_at || left.created_at))
34
+ - toTimestamp(right && (right.updated_at || right.last_activity_at || right.resumed_at || right.started_at || right.created_at));
35
+ }
36
+
37
+ function cloneJsonValue(value) {
38
+ return value == null ? value : JSON.parse(JSON.stringify(value));
39
+ }
40
+
41
+ function readFileFingerprint(filePath) {
42
+ if (!filePath || !fs.existsSync(filePath)) {
43
+ return {
44
+ exists: false,
45
+ size: 0,
46
+ mtime_ms: 0,
47
+ };
48
+ }
49
+
50
+ const stats = fs.statSync(filePath);
51
+ return {
52
+ exists: true,
53
+ size: stats.size,
54
+ mtime_ms: stats.mtimeMs,
55
+ };
56
+ }
57
+
58
+ function sameFileFingerprint(left, right) {
59
+ return !!left
60
+ && !!right
61
+ && left.exists === right.exists
62
+ && left.size === right.size
63
+ && left.mtime_ms === right.mtime_ms;
64
+ }
65
+
66
+ function normalizeAgentIndexSummary(summary) {
67
+ return {
68
+ latest_session_id: typeof (summary && summary.latest_session_id) === 'string' ? summary.latest_session_id : null,
69
+ latest_branch_id: typeof (summary && summary.latest_branch_id) === 'string' ? summary.latest_branch_id : null,
70
+ active_session_id: typeof (summary && summary.active_session_id) === 'string' ? summary.active_session_id : null,
71
+ active_branch_id: typeof (summary && summary.active_branch_id) === 'string' ? summary.active_branch_id : null,
72
+ branch_ids: Array.isArray(summary && summary.branch_ids)
73
+ ? [...new Set(summary.branch_ids.filter((branchId) => typeof branchId === 'string' && branchId.length > 0))]
74
+ : [],
75
+ };
76
+ }
77
+
78
+ function normalizeBranchIndexSummary(summary) {
79
+ const latestByAgent = summary && summary.latest_by_agent && typeof summary.latest_by_agent === 'object' && !Array.isArray(summary.latest_by_agent)
80
+ ? summary.latest_by_agent
81
+ : {};
82
+
83
+ return {
84
+ latest_session_id: typeof (summary && summary.latest_session_id) === 'string' ? summary.latest_session_id : null,
85
+ session_ids: Array.isArray(summary && summary.session_ids)
86
+ ? [...new Set(summary.session_ids.filter((sessionId) => typeof sessionId === 'string' && sessionId.length > 0))]
87
+ : [],
88
+ active_session_ids: Array.isArray(summary && summary.active_session_ids)
89
+ ? [...new Set(summary.active_session_ids.filter((sessionId) => typeof sessionId === 'string' && sessionId.length > 0))]
90
+ : [],
91
+ latest_by_agent: Object.fromEntries(
92
+ Object.entries(latestByAgent).filter(([, sessionId]) => typeof sessionId === 'string' && sessionId.length > 0)
93
+ ),
94
+ };
95
+ }
96
+
97
+ function moveRecentIdToFront(values, value, includeValue = true) {
98
+ const normalized = Array.isArray(values)
99
+ ? values.filter((entry) => typeof entry === 'string' && entry.length > 0 && entry !== value)
100
+ : [];
101
+
102
+ if (includeValue && typeof value === 'string' && value.length > 0) {
103
+ normalized.unshift(value);
104
+ }
105
+
106
+ return normalized;
107
+ }
108
+
109
+ function normalizeIndex(index) {
110
+ const sessions = index && index.sessions && typeof index.sessions === 'object' && !Array.isArray(index.sessions)
111
+ ? { ...index.sessions }
112
+ : {};
113
+ const byAgent = index && index.by_agent && typeof index.by_agent === 'object' && !Array.isArray(index.by_agent)
114
+ ? index.by_agent
115
+ : {};
116
+ const byBranch = index && index.by_branch && typeof index.by_branch === 'object' && !Array.isArray(index.by_branch)
117
+ ? index.by_branch
118
+ : {};
119
+
120
+ return {
121
+ schema_version: SESSION_INDEX_SCHEMA_VERSION,
122
+ updated_at: index && index.updated_at ? index.updated_at : null,
123
+ sessions,
124
+ active_sessions: Array.isArray(index && index.active_sessions)
125
+ ? [...new Set(index.active_sessions.filter((sessionId) => typeof sessionId === 'string' && sessionId.length > 0))]
126
+ : [],
127
+ by_agent: Object.fromEntries(
128
+ Object.entries(byAgent).map(([agentName, summary]) => [agentName, normalizeAgentIndexSummary(summary)])
129
+ ),
130
+ by_branch: Object.fromEntries(
131
+ Object.entries(byBranch).map(([branchId, summary]) => [branchId, normalizeBranchIndexSummary(summary)])
132
+ ),
133
+ };
134
+ }
135
+
136
+ function buildSessionSummary(session, indexedAt, options = {}) {
137
+ const staleThresholdMs = Number.isFinite(options.staleThresholdMs) ? options.staleThresholdMs : DEFAULT_STALE_THRESHOLD_MS;
138
+ const lastActivityAt = session.last_activity_at || session.resumed_at || session.started_at || session.created_at || null;
139
+ const indexedAtMs = toTimestamp(indexedAt);
140
+ const stale = session.state === 'active'
141
+ && indexedAtMs > 0
142
+ && toTimestamp(lastActivityAt) > 0
143
+ && indexedAtMs - toTimestamp(lastActivityAt) > staleThresholdMs;
144
+
145
+ return {
146
+ session_id: session.session_id,
147
+ agent_name: session.agent_name,
148
+ branch_id: session.branch_id,
149
+ provider: session.provider || null,
150
+ state: session.state,
151
+ created_at: session.created_at || null,
152
+ started_at: session.started_at || null,
153
+ resumed_at: session.resumed_at || null,
154
+ ended_at: session.ended_at || null,
155
+ updated_at: session.updated_at || null,
156
+ last_activity_at: lastActivityAt,
157
+ last_heartbeat_at: session.last_heartbeat_at || null,
158
+ transition_reason: session.transition_reason || null,
159
+ resume_count: Number.isInteger(session.resume_count) ? session.resume_count : 0,
160
+ recovery_snapshot_file: session.recovery_snapshot_file || null,
161
+ stale,
162
+ };
163
+ }
164
+
165
+ function createSessionsState(options = {}) {
166
+ const {
167
+ io,
168
+ branchPaths,
169
+ canonicalEventLog = null,
170
+ now = () => new Date().toISOString(),
171
+ createSessionId = fallbackSessionId,
172
+ staleThresholdMs = DEFAULT_STALE_THRESHOLD_MS,
173
+ } = options;
174
+
175
+ if (!io) throw new Error('createSessionsState requires io');
176
+ if (!branchPaths) throw new Error('createSessionsState requires branchPaths');
177
+ if (typeof branchPaths.getBranchSessionFile !== 'function') {
178
+ throw new Error('createSessionsState requires branchPaths.getBranchSessionFile()');
179
+ }
180
+ if (typeof branchPaths.getBranchSessionsDir !== 'function') {
181
+ throw new Error('createSessionsState requires branchPaths.getBranchSessionsDir()');
182
+ }
183
+ if (typeof branchPaths.getSessionsIndexFile !== 'function') {
184
+ throw new Error('createSessionsState requires branchPaths.getSessionsIndexFile()');
185
+ }
186
+
187
+ let indexCache = null;
188
+
189
+ function ensureFileDir(filePath) {
190
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
191
+ }
192
+
193
+ function readSessionManifest(sessionId, branchName = 'main') {
194
+ return io.readJsonFile(branchPaths.getBranchSessionFile(sessionId, branchName), null);
195
+ }
196
+
197
+ function listBranchSessionFiles(branchName = 'main') {
198
+ const sessionsDir = branchPaths.getBranchSessionsDir(branchName);
199
+ if (!fs.existsSync(sessionsDir)) return [];
200
+ return fs.readdirSync(sessionsDir)
201
+ .filter((fileName) => fileName.endsWith('.json'))
202
+ .map((fileName) => path.join(sessionsDir, fileName));
203
+ }
204
+
205
+ function listBranchSessions(branchName = 'main') {
206
+ return listBranchSessionFiles(branchName)
207
+ .map((filePath) => io.readJsonFile(filePath, null))
208
+ .filter((session) => session && typeof session === 'object' && session.session_id)
209
+ .sort(compareSessionsByRecency);
210
+ }
211
+
212
+ function cacheIndex(index) {
213
+ const indexFile = branchPaths.getSessionsIndexFile();
214
+ const normalized = normalizeIndex(index);
215
+ indexCache = {
216
+ index: normalized,
217
+ fingerprint: readFileFingerprint(indexFile),
218
+ };
219
+ return normalized;
220
+ }
221
+
222
+ function getIndexedSessionSummary(index, sessionId, branchName, indexedAt) {
223
+ const entry = index
224
+ && index.sessions
225
+ && Object.prototype.hasOwnProperty.call(index.sessions, sessionId)
226
+ ? index.sessions[sessionId]
227
+ : null;
228
+
229
+ if (!entry || entry.branch_id !== branchName) return null;
230
+ return buildSessionSummary(entry, indexedAt || index.updated_at || now(), {
231
+ staleThresholdMs,
232
+ });
233
+ }
234
+
235
+ function getIndexedLatestSessionId(index, branchName, agentName) {
236
+ const branchSummary = index
237
+ && index.by_branch
238
+ && index.by_branch[branchName]
239
+ && typeof index.by_branch[branchName] === 'object'
240
+ ? index.by_branch[branchName]
241
+ : null;
242
+
243
+ if (!branchSummary || !branchSummary.latest_by_agent) return null;
244
+ return typeof branchSummary.latest_by_agent[agentName] === 'string'
245
+ ? branchSummary.latest_by_agent[agentName]
246
+ : null;
247
+ }
248
+
249
+ function getMostRecentActiveAgentSession(index, agentName, excludingSessionId = null) {
250
+ if (!index || !index.sessions || typeof index.sessions !== 'object') return null;
251
+
252
+ let best = null;
253
+ for (const entry of Object.values(index.sessions)) {
254
+ if (!entry || entry.agent_name !== agentName || entry.state !== 'active') continue;
255
+ if (excludingSessionId && entry.session_id === excludingSessionId) continue;
256
+ if (!best || compareSessionsByRecency(best, entry) < 0) {
257
+ best = entry;
258
+ }
259
+ }
260
+
261
+ return best;
262
+ }
263
+
264
+ function getLatestSessionForAgent(branchName = 'main', agentName) {
265
+ const index = loadIndex();
266
+ const indexedSessionId = getIndexedLatestSessionId(index, branchName, agentName);
267
+
268
+ if (indexedSessionId) {
269
+ const indexedSession = readSessionManifest(indexedSessionId, branchName);
270
+ if (indexedSession && indexedSession.agent_name === agentName) {
271
+ return indexedSession;
272
+ }
273
+
274
+ const rebuiltIndex = rebuildIndex({ at: now() });
275
+ const rebuiltSessionId = getIndexedLatestSessionId(rebuiltIndex, branchName, agentName);
276
+ if (rebuiltSessionId) {
277
+ const rebuiltSession = readSessionManifest(rebuiltSessionId, branchName);
278
+ if (rebuiltSession && rebuiltSession.agent_name === agentName) {
279
+ return rebuiltSession;
280
+ }
281
+ }
282
+ }
283
+
284
+ const sessions = listBranchSessions(branchName).filter((session) => session.agent_name === agentName);
285
+ return sessions.length > 0 ? sessions[sessions.length - 1] : null;
286
+ }
287
+
288
+ function summarizeSession(session, options = {}) {
289
+ if (!session || !session.session_id) return null;
290
+ return buildSessionSummary(session, options.indexedAt || now(), {
291
+ staleThresholdMs,
292
+ });
293
+ }
294
+
295
+ function getSessionSummary(sessionId, branchName = 'main', options = {}) {
296
+ const index = loadIndex();
297
+ const indexedSummary = getIndexedSessionSummary(index, sessionId, branchName, options.indexedAt || (index && index.updated_at) || now());
298
+ if (indexedSummary) return indexedSummary;
299
+
300
+ const session = readSessionManifest(sessionId, branchName);
301
+ if (!session) return null;
302
+ return summarizeSession(session, {
303
+ indexedAt: options.indexedAt || (index && index.updated_at) || now(),
304
+ });
305
+ }
306
+
307
+ function getLatestSessionSummaryForAgent(branchName = 'main', agentName, options = {}) {
308
+ const index = loadIndex();
309
+ const indexedSessionId = getIndexedLatestSessionId(index, branchName, agentName);
310
+ const indexedSummary = indexedSessionId
311
+ ? getIndexedSessionSummary(index, indexedSessionId, branchName, options.indexedAt || (index && index.updated_at) || now())
312
+ : null;
313
+ if (indexedSummary) return indexedSummary;
314
+
315
+ const session = getLatestSessionForAgent(branchName, agentName);
316
+ if (!session) return null;
317
+ return summarizeSession(session, {
318
+ indexedAt: options.indexedAt || (index && index.updated_at) || now(),
319
+ });
320
+ }
321
+
322
+ function finalizeIndex(index, indexedAt) {
323
+ const normalized = normalizeIndex(index);
324
+ const entries = Object.values(normalized.sessions).sort(compareSessionsByRecency).reverse();
325
+
326
+ normalized.updated_at = indexedAt;
327
+ normalized.active_sessions = entries
328
+ .filter((entry) => entry.state === 'active')
329
+ .map((entry) => entry.session_id);
330
+ normalized.by_agent = {};
331
+ normalized.by_branch = {};
332
+
333
+ for (const entry of entries) {
334
+ if (!normalized.by_agent[entry.agent_name]) {
335
+ normalized.by_agent[entry.agent_name] = {
336
+ latest_session_id: entry.session_id,
337
+ latest_branch_id: entry.branch_id,
338
+ active_session_id: null,
339
+ active_branch_id: null,
340
+ branch_ids: [],
341
+ };
342
+ }
343
+
344
+ const agentSummary = normalized.by_agent[entry.agent_name];
345
+ if (!agentSummary.branch_ids.includes(entry.branch_id)) agentSummary.branch_ids.push(entry.branch_id);
346
+ if (entry.state === 'active' && !agentSummary.active_session_id) {
347
+ agentSummary.active_session_id = entry.session_id;
348
+ agentSummary.active_branch_id = entry.branch_id;
349
+ }
350
+
351
+ if (!normalized.by_branch[entry.branch_id]) {
352
+ normalized.by_branch[entry.branch_id] = {
353
+ latest_session_id: entry.session_id,
354
+ session_ids: [],
355
+ active_session_ids: [],
356
+ latest_by_agent: {},
357
+ };
358
+ }
359
+
360
+ normalized.by_branch[entry.branch_id].session_ids.push(entry.session_id);
361
+ if (!normalized.by_branch[entry.branch_id].latest_by_agent[entry.agent_name]) {
362
+ normalized.by_branch[entry.branch_id].latest_by_agent[entry.agent_name] = entry.session_id;
363
+ }
364
+ if (entry.state === 'active') {
365
+ normalized.by_branch[entry.branch_id].active_session_ids.push(entry.session_id);
366
+ }
367
+ }
368
+
369
+ return normalized;
370
+ }
371
+
372
+ function writeIndex(index) {
373
+ const filePath = branchPaths.getSessionsIndexFile();
374
+ ensureFileDir(filePath);
375
+ return io.withLock(filePath, () => {
376
+ const normalized = normalizeIndex(index);
377
+ fs.writeFileSync(filePath, JSON.stringify(normalized));
378
+ return cloneJsonValue(cacheIndex(normalized));
379
+ });
380
+ }
381
+
382
+ function rebuildIndex(options = {}) {
383
+ const indexedAt = options.at || now();
384
+ const runtimeBranchesDir = path.join(branchPaths.runtimeDir, 'branches');
385
+ const sessions = {};
386
+
387
+ if (fs.existsSync(runtimeBranchesDir)) {
388
+ for (const entry of fs.readdirSync(runtimeBranchesDir, { withFileTypes: true })) {
389
+ if (!entry.isDirectory()) continue;
390
+ const branchName = entry.name;
391
+ for (const session of listBranchSessions(branchName)) {
392
+ sessions[session.session_id] = buildSessionSummary(session, indexedAt, {
393
+ staleThresholdMs,
394
+ });
395
+ }
396
+ }
397
+ }
398
+
399
+ return writeIndex(finalizeIndex({ schema_version: SESSION_INDEX_SCHEMA_VERSION, sessions }, indexedAt));
400
+ }
401
+
402
+ function loadIndex() {
403
+ const indexFile = branchPaths.getSessionsIndexFile();
404
+ const currentFingerprint = readFileFingerprint(indexFile);
405
+ if (!currentFingerprint.exists) {
406
+ indexCache = null;
407
+ return null;
408
+ }
409
+
410
+ if (indexCache && sameFileFingerprint(indexCache.fingerprint, currentFingerprint)) {
411
+ return cloneJsonValue(indexCache.index);
412
+ }
413
+
414
+ const parsed = io.readJsonFile(indexFile, null);
415
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
416
+ const normalized = normalizeIndex(parsed);
417
+ indexCache = {
418
+ index: normalized,
419
+ fingerprint: currentFingerprint,
420
+ };
421
+ return cloneJsonValue(normalized);
422
+ }
423
+
424
+ function syncIndexForSession(session, indexedAt) {
425
+ const currentIndex = loadIndex() || rebuildIndex({ at: indexedAt });
426
+ const nextSummary = buildSessionSummary(session, indexedAt, {
427
+ staleThresholdMs,
428
+ });
429
+ const previousSummary = currentIndex.sessions[session.session_id] || null;
430
+
431
+ if (previousSummary
432
+ && (previousSummary.agent_name !== nextSummary.agent_name || previousSummary.branch_id !== nextSummary.branch_id)) {
433
+ return rebuildIndex({ at: indexedAt });
434
+ }
435
+
436
+ currentIndex.updated_at = indexedAt;
437
+ currentIndex.sessions[session.session_id] = nextSummary;
438
+ currentIndex.active_sessions = moveRecentIdToFront(
439
+ currentIndex.active_sessions,
440
+ session.session_id,
441
+ nextSummary.state === 'active'
442
+ );
443
+
444
+ const agentSummary = normalizeAgentIndexSummary(currentIndex.by_agent[nextSummary.agent_name]);
445
+ agentSummary.latest_session_id = session.session_id;
446
+ agentSummary.latest_branch_id = nextSummary.branch_id;
447
+ agentSummary.branch_ids = moveRecentIdToFront(agentSummary.branch_ids, nextSummary.branch_id, true);
448
+ if (nextSummary.state === 'active') {
449
+ agentSummary.active_session_id = session.session_id;
450
+ agentSummary.active_branch_id = nextSummary.branch_id;
451
+ } else if (agentSummary.active_session_id === session.session_id) {
452
+ const fallbackActiveSession = getMostRecentActiveAgentSession(currentIndex, nextSummary.agent_name, session.session_id);
453
+ agentSummary.active_session_id = fallbackActiveSession ? fallbackActiveSession.session_id : null;
454
+ agentSummary.active_branch_id = fallbackActiveSession ? fallbackActiveSession.branch_id : null;
455
+ }
456
+ currentIndex.by_agent[nextSummary.agent_name] = agentSummary;
457
+
458
+ const branchSummary = normalizeBranchIndexSummary(currentIndex.by_branch[nextSummary.branch_id]);
459
+ branchSummary.session_ids = moveRecentIdToFront(branchSummary.session_ids, session.session_id, true);
460
+ branchSummary.latest_session_id = branchSummary.session_ids[0] || session.session_id;
461
+ branchSummary.active_session_ids = moveRecentIdToFront(
462
+ branchSummary.active_session_ids,
463
+ session.session_id,
464
+ nextSummary.state === 'active'
465
+ );
466
+ branchSummary.latest_by_agent = {
467
+ ...branchSummary.latest_by_agent,
468
+ [nextSummary.agent_name]: session.session_id,
469
+ };
470
+ currentIndex.by_branch[nextSummary.branch_id] = branchSummary;
471
+
472
+ return writeIndex(currentIndex);
473
+ }
474
+
475
+ function writeSessionManifest(session) {
476
+ const filePath = branchPaths.getBranchSessionFile(session.session_id, session.branch_id);
477
+ ensureFileDir(filePath);
478
+ return io.withLock(filePath, () => {
479
+ fs.writeFileSync(filePath, JSON.stringify(session, null, 2));
480
+ return session;
481
+ });
482
+ }
483
+
484
+ function appendSessionEvent(type, session, payload = {}) {
485
+ if (!canonicalEventLog) return null;
486
+ return canonicalEventLog.appendEvent({
487
+ type,
488
+ branchId: session.branch_id,
489
+ actorAgent: session.agent_name,
490
+ sessionId: session.session_id,
491
+ payload: {
492
+ state: session.state,
493
+ reason: payload.reason || session.transition_reason || null,
494
+ provider: session.provider || null,
495
+ started_at: session.started_at || null,
496
+ resumed_at: session.resumed_at || null,
497
+ ended_at: session.ended_at || null,
498
+ recovery_snapshot_file: payload.recoverySnapshotFile || session.recovery_snapshot_file || null,
499
+ },
500
+ });
501
+ }
502
+
503
+ function activateSession(params = {}) {
504
+ const branchName = params.branchName || 'main';
505
+ const agentName = params.agentName;
506
+ const activatedAt = params.at || now();
507
+ const transitionReason = params.reason || 'register';
508
+
509
+ if (!agentName) throw new Error('activateSession requires agentName');
510
+
511
+ let latest = getLatestSessionForAgent(branchName, agentName);
512
+
513
+ if (latest && latest.state === 'active') {
514
+ latest = transitionSession({
515
+ sessionId: latest.session_id,
516
+ branchName,
517
+ state: 'interrupted',
518
+ reason: params.orphanedReason || 'orphaned_active_recovery',
519
+ at: activatedAt,
520
+ }).session;
521
+ }
522
+
523
+ if (latest && latest.state === 'interrupted') {
524
+ const resumedSession = {
525
+ ...latest,
526
+ state: 'active',
527
+ provider: params.provider || latest.provider || null,
528
+ resumed_at: activatedAt,
529
+ ended_at: null,
530
+ updated_at: activatedAt,
531
+ last_activity_at: activatedAt,
532
+ transition_reason: transitionReason,
533
+ recovery_snapshot_file: null,
534
+ resume_count: (Number.isInteger(latest.resume_count) ? latest.resume_count : 0) + 1,
535
+ };
536
+
537
+ appendSessionEvent('session.resumed', resumedSession, { reason: transitionReason });
538
+ writeSessionManifest(resumedSession);
539
+ syncIndexForSession(resumedSession, activatedAt);
540
+
541
+ return {
542
+ session: resumedSession,
543
+ created: false,
544
+ resumed: true,
545
+ previous_state: latest.state,
546
+ };
547
+ }
548
+
549
+ const startedSession = {
550
+ schema_version: SESSION_MANIFEST_SCHEMA_VERSION,
551
+ session_id: params.sessionId || createSessionId(),
552
+ agent_name: agentName,
553
+ branch_id: branchName,
554
+ provider: params.provider || null,
555
+ state: 'active',
556
+ created_at: activatedAt,
557
+ started_at: activatedAt,
558
+ resumed_at: activatedAt,
559
+ ended_at: null,
560
+ updated_at: activatedAt,
561
+ last_activity_at: activatedAt,
562
+ last_heartbeat_at: null,
563
+ transition_reason: transitionReason,
564
+ recovery_snapshot_file: null,
565
+ resume_count: 0,
566
+ };
567
+
568
+ appendSessionEvent('session.started', startedSession, { reason: transitionReason });
569
+ writeSessionManifest(startedSession);
570
+ syncIndexForSession(startedSession, activatedAt);
571
+
572
+ return {
573
+ session: startedSession,
574
+ created: true,
575
+ resumed: false,
576
+ previous_state: latest ? latest.state : null,
577
+ };
578
+ }
579
+
580
+ function touchSession(params = {}) {
581
+ const sessionId = params.sessionId;
582
+ const branchName = params.branchName || 'main';
583
+ const touchedAt = params.at || now();
584
+ if (!sessionId) return { session: null, updated: false };
585
+
586
+ const session = readSessionManifest(sessionId, branchName);
587
+ if (!session || session.state !== 'active') {
588
+ return { session, updated: false };
589
+ }
590
+
591
+ const touchedSession = {
592
+ ...session,
593
+ last_activity_at: touchedAt,
594
+ updated_at: touchedAt,
595
+ ...(params.heartbeat ? { last_heartbeat_at: touchedAt } : {}),
596
+ };
597
+
598
+ writeSessionManifest(touchedSession);
599
+ syncIndexForSession(touchedSession, touchedAt);
600
+
601
+ return { session: touchedSession, updated: true };
602
+ }
603
+
604
+ function transitionSession(params = {}) {
605
+ const sessionId = params.sessionId;
606
+ const branchName = params.branchName || 'main';
607
+ const nextState = params.state;
608
+ const transitionedAt = params.at || now();
609
+
610
+ if (!sessionId) return { session: null, updated: false };
611
+ if (!SESSION_STATES.includes(nextState) || nextState === 'active') {
612
+ throw new Error(`Invalid session transition target: ${String(nextState)}`);
613
+ }
614
+
615
+ const session = readSessionManifest(sessionId, branchName);
616
+ if (!session) return { session: null, updated: false };
617
+ if (session.state === nextState) return { session, updated: false };
618
+ if (session.state !== 'active' && nextState !== 'active') return { session, updated: false };
619
+
620
+ const transitionedSession = {
621
+ ...session,
622
+ state: nextState,
623
+ ended_at: transitionedAt,
624
+ updated_at: transitionedAt,
625
+ transition_reason: params.reason || session.transition_reason || null,
626
+ recovery_snapshot_file: params.recoverySnapshotFile || session.recovery_snapshot_file || null,
627
+ last_activity_at: session.last_activity_at || transitionedAt,
628
+ };
629
+
630
+ appendSessionEvent(TERMINAL_SESSION_EVENTS[nextState], transitionedSession, {
631
+ reason: params.reason,
632
+ recoverySnapshotFile: params.recoverySnapshotFile,
633
+ });
634
+ writeSessionManifest(transitionedSession);
635
+ syncIndexForSession(transitionedSession, transitionedAt);
636
+
637
+ return { session: transitionedSession, updated: true };
638
+ }
639
+
640
+ function transitionLatestSessionForAgent(params = {}) {
641
+ const branchName = params.branchName || 'main';
642
+ const agentName = params.agentName;
643
+ if (!agentName) return { session: null, updated: false };
644
+ const latest = getLatestSessionForAgent(branchName, agentName);
645
+ if (!latest) return { session: null, updated: false };
646
+
647
+ return transitionSession({
648
+ sessionId: latest.session_id,
649
+ branchName,
650
+ state: params.state,
651
+ reason: params.reason,
652
+ at: params.at,
653
+ recoverySnapshotFile: params.recoverySnapshotFile,
654
+ });
655
+ }
656
+
657
+ return {
658
+ SESSION_MANIFEST_SCHEMA_VERSION,
659
+ SESSION_INDEX_SCHEMA_VERSION,
660
+ SESSION_STATES,
661
+ activateSession,
662
+ getLatestSessionForAgent,
663
+ getLatestSessionSummaryForAgent,
664
+ getSessionSummary,
665
+ listBranchSessions,
666
+ loadIndex,
667
+ readSessionManifest,
668
+ rebuildIndex,
669
+ summarizeSession,
670
+ touchSession,
671
+ transitionLatestSessionForAgent,
672
+ transitionSession,
673
+ };
674
+ }
675
+
676
+ module.exports = {
677
+ DEFAULT_STALE_THRESHOLD_MS,
678
+ buildSessionSummary,
679
+ SESSION_INDEX_SCHEMA_VERSION,
680
+ SESSION_MANIFEST_SCHEMA_VERSION,
681
+ SESSION_STATES,
682
+ createSessionsState,
683
+ };