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.
- package/CHANGELOG.md +3 -1
- package/README.md +158 -592
- package/SECURITY.md +3 -3
- package/USAGE.md +151 -0
- package/agent-contracts.js +447 -0
- package/api-agents.js +760 -0
- package/autonomy/decision-v2.js +380 -0
- package/autonomy/watchdog-policy.js +572 -0
- package/cli.js +454 -298
- package/conversation-templates/autonomous-feature.json +83 -22
- package/conversation-templates/code-review.json +69 -21
- package/conversation-templates/debug-squad.json +69 -21
- package/conversation-templates/feature-build.json +69 -21
- package/conversation-templates/research-write.json +69 -21
- package/dashboard.html +3148 -174
- package/dashboard.js +823 -786
- package/data-dir.js +58 -0
- package/docs/architecture/branch-semantics.md +157 -0
- package/docs/architecture/canonical-event-schema.md +88 -0
- package/docs/architecture/markdown-workspace.md +183 -0
- package/docs/architecture/runtime-contract.md +459 -0
- package/docs/architecture/runtime-migration-hardening.md +64 -0
- package/events/hooks.js +154 -0
- package/events/log.js +457 -0
- package/events/replay.js +33 -0
- package/events/schema.js +432 -0
- package/managed-team-integration.js +261 -0
- package/office/agents.js +704 -597
- package/office/animation.js +1 -1
- package/office/assets/arcade-cabinet.js +141 -0
- package/office/assets/archway.js +77 -0
- package/office/assets/bar-counter.js +91 -0
- package/office/assets/bar-stool.js +71 -0
- package/office/assets/beanbag.js +64 -0
- package/office/assets/bench.js +99 -0
- package/office/assets/bollard.js +87 -0
- package/office/assets/cactus.js +100 -0
- package/office/assets/carpet-tile.js +46 -0
- package/office/assets/chair.js +123 -0
- package/office/assets/chandelier.js +107 -0
- package/office/assets/coffee-machine.js +95 -0
- package/office/assets/coffee-table.js +81 -0
- package/office/assets/column.js +95 -0
- package/office/assets/desk-lamp.js +102 -0
- package/office/assets/desk.js +76 -0
- package/office/assets/dining-table.js +105 -0
- package/office/assets/door.js +70 -0
- package/office/assets/dual-monitor.js +72 -0
- package/office/assets/fence.js +76 -0
- package/office/assets/filing-cabinet.js +111 -0
- package/office/assets/floor-lamp.js +69 -0
- package/office/assets/floor-tile.js +54 -0
- package/office/assets/flower-pot.js +76 -0
- package/office/assets/foosball.js +95 -0
- package/office/assets/fridge.js +99 -0
- package/office/assets/gaming-chair.js +154 -0
- package/office/assets/gaming-desk.js +105 -0
- package/office/assets/glass-door.js +72 -0
- package/office/assets/glass-wall.js +64 -0
- package/office/assets/half-wall.js +49 -0
- package/office/assets/hanging-plant.js +112 -0
- package/office/assets/index.js +151 -0
- package/office/assets/indoor-tree.js +90 -0
- package/office/assets/l-sofa.js +153 -0
- package/office/assets/marble-floor.js +64 -0
- package/office/assets/materials.js +40 -0
- package/office/assets/meeting-table.js +88 -0
- package/office/assets/microwave.js +94 -0
- package/office/assets/monitor.js +67 -0
- package/office/assets/neon-strip.js +73 -0
- package/office/assets/painting.js +84 -0
- package/office/assets/palm-tree.js +108 -0
- package/office/assets/pc-tower.js +91 -0
- package/office/assets/pendant-light.js +67 -0
- package/office/assets/ping-pong.js +114 -0
- package/office/assets/plant.js +72 -0
- package/office/assets/planter-box.js +95 -0
- package/office/assets/pool-table.js +94 -0
- package/office/assets/printer.js +113 -0
- package/office/assets/reception-desk.js +133 -0
- package/office/assets/rug.js +78 -0
- package/office/assets/sculpture.js +85 -0
- package/office/assets/server-rack.js +98 -0
- package/office/assets/sink.js +109 -0
- package/office/assets/sofa.js +106 -0
- package/office/assets/speaker.js +83 -0
- package/office/assets/spotlight.js +83 -0
- package/office/assets/street-lamp.js +97 -0
- package/office/assets/trash-can.js +83 -0
- package/office/assets/treadmill.js +126 -0
- package/office/assets/trophy.js +89 -0
- package/office/assets/tv-screen.js +79 -0
- package/office/assets/vase.js +84 -0
- package/office/assets/wall-clock.js +84 -0
- package/office/assets/wall.js +53 -0
- package/office/assets/water-cooler.js +146 -0
- package/office/assets/whiteboard.js +115 -0
- package/office/assets.js +3 -431
- package/office/builder.js +791 -355
- package/office/campus-env.js +1012 -1119
- package/office/environment.js +2 -0
- package/office/gallery.js +997 -0
- package/office/index.js +165 -61
- package/office/navigation.js +173 -152
- package/office/player.js +178 -68
- package/office/robot-character.js +272 -0
- package/office/spectator-camera.js +33 -10
- package/office/state.js +2 -0
- package/office/world-save.js +35 -4
- package/package.json +57 -3
- package/providers/comfyui.js +383 -0
- package/providers/dalle.js +79 -0
- package/providers/gemini.js +181 -0
- package/providers/ollama.js +184 -0
- package/providers/replicate.js +115 -0
- package/providers/zai.js +183 -0
- package/runtime-descriptor.js +270 -0
- package/scripts/check-agent-contract-advisory.js +132 -0
- package/scripts/check-api-agent-parity.js +277 -0
- package/scripts/check-autonomy-v2-decision.js +207 -0
- package/scripts/check-autonomy-v2-execution.js +588 -0
- package/scripts/check-autonomy-v2-watchdog.js +224 -0
- package/scripts/check-branch-fork-snapshot.js +337 -0
- package/scripts/check-branch-isolation.js +787 -0
- package/scripts/check-branch-semantics.js +139 -0
- package/scripts/check-dashboard-control-plane.js +1304 -0
- package/scripts/check-docs-onboarding.js +490 -0
- package/scripts/check-event-schema.js +276 -0
- package/scripts/check-evidence-completion.js +239 -0
- package/scripts/check-invariants.js +992 -0
- package/scripts/check-lifecycle-hooks.js +525 -0
- package/scripts/check-managed-team-integration.js +166 -0
- package/scripts/check-markdown-workspace-export.js +548 -0
- package/scripts/check-markdown-workspace-safety.js +347 -0
- package/scripts/check-markdown-workspace.js +136 -0
- package/scripts/check-message-replay.js +429 -0
- package/scripts/check-migration-hardening.js +300 -0
- package/scripts/check-performance-indexing.js +272 -0
- package/scripts/check-provider-capabilities.js +316 -0
- package/scripts/check-runtime-contract.js +109 -0
- package/scripts/check-session-aware-context.js +172 -0
- package/scripts/check-session-lifecycle.js +210 -0
- package/scripts/export-markdown-workspace.js +84 -0
- package/scripts/fixtures/message-replay/clean.jsonl +2 -0
- package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
- package/scripts/migrate-legacy-to-canonical.js +201 -0
- package/scripts/run-verification-suite.js +242 -0
- package/scripts/sync-packaged-docs.js +69 -0
- package/server.js +9546 -7214
- package/state/agents.js +161 -0
- package/state/canonical.js +3068 -0
- package/state/dashboard-queries.js +441 -0
- package/state/evidence.js +56 -0
- package/state/io.js +69 -0
- package/state/markdown-workspace.js +951 -0
- package/state/messages.js +669 -0
- package/state/sessions.js +683 -0
- package/state/tasks-workflows.js +92 -0
- package/templates/debate.json +2 -2
- package/templates/managed.json +4 -4
- package/templates/pair.json +2 -2
- package/templates/review.json +2 -2
- 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
|
+
};
|