@evelinka-ai/core 0.1.3

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 (130) hide show
  1. package/README.md +16 -0
  2. package/dist/agent-lib/llm-semaphore.d.ts +47 -0
  3. package/dist/agent-lib/llm-semaphore.d.ts.map +1 -0
  4. package/dist/agent-lib/llm-semaphore.js +255 -0
  5. package/dist/agent-lib/llm-semaphore.js.map +1 -0
  6. package/dist/agent-lib/managed-tool.d.ts +29 -0
  7. package/dist/agent-lib/managed-tool.d.ts.map +1 -0
  8. package/dist/agent-lib/managed-tool.js +28 -0
  9. package/dist/agent-lib/managed-tool.js.map +1 -0
  10. package/dist/channels/catalog.d.ts +3 -0
  11. package/dist/channels/catalog.d.ts.map +1 -0
  12. package/dist/channels/catalog.js +20 -0
  13. package/dist/channels/catalog.js.map +1 -0
  14. package/dist/channels/model-catalog.d.ts +3 -0
  15. package/dist/channels/model-catalog.d.ts.map +1 -0
  16. package/dist/channels/model-catalog.js +6 -0
  17. package/dist/channels/model-catalog.js.map +1 -0
  18. package/dist/channels/usage.d.ts +3 -0
  19. package/dist/channels/usage.d.ts.map +1 -0
  20. package/dist/channels/usage.js +35 -0
  21. package/dist/channels/usage.js.map +1 -0
  22. package/dist/lib/atomic-file.d.ts +2 -0
  23. package/dist/lib/atomic-file.d.ts.map +1 -0
  24. package/dist/lib/atomic-file.js +16 -0
  25. package/dist/lib/atomic-file.js.map +1 -0
  26. package/dist/lib/budgets/record-call-usage.d.ts +31 -0
  27. package/dist/lib/budgets/record-call-usage.d.ts.map +1 -0
  28. package/dist/lib/budgets/record-call-usage.js +44 -0
  29. package/dist/lib/budgets/record-call-usage.js.map +1 -0
  30. package/dist/lib/budgets/token-budget.d.ts +61 -0
  31. package/dist/lib/budgets/token-budget.d.ts.map +1 -0
  32. package/dist/lib/budgets/token-budget.js +250 -0
  33. package/dist/lib/budgets/token-budget.js.map +1 -0
  34. package/dist/lib/budgets/token-usage-pg.d.ts +96 -0
  35. package/dist/lib/budgets/token-usage-pg.d.ts.map +1 -0
  36. package/dist/lib/budgets/token-usage-pg.js +305 -0
  37. package/dist/lib/budgets/token-usage-pg.js.map +1 -0
  38. package/dist/lib/budgets/usage.d.ts +103 -0
  39. package/dist/lib/budgets/usage.d.ts.map +1 -0
  40. package/dist/lib/budgets/usage.js +151 -0
  41. package/dist/lib/budgets/usage.js.map +1 -0
  42. package/dist/lib/control-plane/call-context.d.ts +7 -0
  43. package/dist/lib/control-plane/call-context.d.ts.map +1 -0
  44. package/dist/lib/control-plane/call-context.js +9 -0
  45. package/dist/lib/control-plane/call-context.js.map +1 -0
  46. package/dist/lib/control-plane/catalog.d.ts +33 -0
  47. package/dist/lib/control-plane/catalog.d.ts.map +1 -0
  48. package/dist/lib/control-plane/catalog.js +163 -0
  49. package/dist/lib/control-plane/catalog.js.map +1 -0
  50. package/dist/lib/control-plane/flow-settings.d.ts +32 -0
  51. package/dist/lib/control-plane/flow-settings.d.ts.map +1 -0
  52. package/dist/lib/control-plane/flow-settings.js +139 -0
  53. package/dist/lib/control-plane/flow-settings.js.map +1 -0
  54. package/dist/lib/control-plane/tools-config.d.ts +30 -0
  55. package/dist/lib/control-plane/tools-config.d.ts.map +1 -0
  56. package/dist/lib/control-plane/tools-config.js +104 -0
  57. package/dist/lib/control-plane/tools-config.js.map +1 -0
  58. package/dist/lib/delivery/push-store.d.ts +20 -0
  59. package/dist/lib/delivery/push-store.d.ts.map +1 -0
  60. package/dist/lib/delivery/push-store.js +74 -0
  61. package/dist/lib/delivery/push-store.js.map +1 -0
  62. package/dist/lib/delivery/push.d.ts +15 -0
  63. package/dist/lib/delivery/push.d.ts.map +1 -0
  64. package/dist/lib/delivery/push.js +97 -0
  65. package/dist/lib/delivery/push.js.map +1 -0
  66. package/dist/lib/delivery/schedule-lock.d.ts +16 -0
  67. package/dist/lib/delivery/schedule-lock.d.ts.map +1 -0
  68. package/dist/lib/delivery/schedule-lock.js +86 -0
  69. package/dist/lib/delivery/schedule-lock.js.map +1 -0
  70. package/dist/lib/memory/memory-dream.d.ts +181 -0
  71. package/dist/lib/memory/memory-dream.d.ts.map +1 -0
  72. package/dist/lib/memory/memory-dream.js +405 -0
  73. package/dist/lib/memory/memory-dream.js.map +1 -0
  74. package/dist/lib/memory/memory-vault-git.d.ts +14 -0
  75. package/dist/lib/memory/memory-vault-git.d.ts.map +1 -0
  76. package/dist/lib/memory/memory-vault-git.js +345 -0
  77. package/dist/lib/memory/memory-vault-git.js.map +1 -0
  78. package/dist/lib/memory/memory-vault.d.ts +83 -0
  79. package/dist/lib/memory/memory-vault.d.ts.map +1 -0
  80. package/dist/lib/memory/memory-vault.js +466 -0
  81. package/dist/lib/memory/memory-vault.js.map +1 -0
  82. package/dist/lib/memory/static-instructions.d.ts +2 -0
  83. package/dist/lib/memory/static-instructions.d.ts.map +1 -0
  84. package/dist/lib/memory/static-instructions.js +48 -0
  85. package/dist/lib/memory/static-instructions.js.map +1 -0
  86. package/dist/lib/memory/vault-explorer.d.ts +37 -0
  87. package/dist/lib/memory/vault-explorer.d.ts.map +1 -0
  88. package/dist/lib/memory/vault-explorer.js +212 -0
  89. package/dist/lib/memory/vault-explorer.js.map +1 -0
  90. package/dist/lib/models/llm-concurrency.d.ts +29 -0
  91. package/dist/lib/models/llm-concurrency.d.ts.map +1 -0
  92. package/dist/lib/models/llm-concurrency.js +103 -0
  93. package/dist/lib/models/llm-concurrency.js.map +1 -0
  94. package/dist/lib/models/model-catalog.d.ts +32 -0
  95. package/dist/lib/models/model-catalog.d.ts.map +1 -0
  96. package/dist/lib/models/model-catalog.js +115 -0
  97. package/dist/lib/models/model-catalog.js.map +1 -0
  98. package/dist/lib/models/model-config.d.ts +27 -0
  99. package/dist/lib/models/model-config.d.ts.map +1 -0
  100. package/dist/lib/models/model-config.js +155 -0
  101. package/dist/lib/models/model-config.js.map +1 -0
  102. package/dist/lib/observability/console-link.d.ts +3 -0
  103. package/dist/lib/observability/console-link.d.ts.map +1 -0
  104. package/dist/lib/observability/console-link.js +15 -0
  105. package/dist/lib/observability/console-link.js.map +1 -0
  106. package/dist/lib/observability/event-bus.d.ts +11 -0
  107. package/dist/lib/observability/event-bus.d.ts.map +1 -0
  108. package/dist/lib/observability/event-bus.js +24 -0
  109. package/dist/lib/observability/event-bus.js.map +1 -0
  110. package/dist/lib/observability/logger.d.ts +5 -0
  111. package/dist/lib/observability/logger.d.ts.map +1 -0
  112. package/dist/lib/observability/logger.js +47 -0
  113. package/dist/lib/observability/logger.js.map +1 -0
  114. package/dist/lib/observability/payload-store.d.ts +8 -0
  115. package/dist/lib/observability/payload-store.d.ts.map +1 -0
  116. package/dist/lib/observability/payload-store.js +101 -0
  117. package/dist/lib/observability/payload-store.js.map +1 -0
  118. package/dist/lib/observability/recorder-hook.d.ts +22 -0
  119. package/dist/lib/observability/recorder-hook.d.ts.map +1 -0
  120. package/dist/lib/observability/recorder-hook.js +241 -0
  121. package/dist/lib/observability/recorder-hook.js.map +1 -0
  122. package/dist/lib/observability/recorder.d.ts +339 -0
  123. package/dist/lib/observability/recorder.d.ts.map +1 -0
  124. package/dist/lib/observability/recorder.js +1182 -0
  125. package/dist/lib/observability/recorder.js.map +1 -0
  126. package/dist/tenant.d.ts +59 -0
  127. package/dist/tenant.d.ts.map +1 -0
  128. package/dist/tenant.js +18 -0
  129. package/dist/tenant.js.map +1 -0
  130. package/package.json +119 -0
@@ -0,0 +1,1182 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { appendFileSync, closeSync, mkdirSync, openSync, readFileSync, readSync, statSync, } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { publishSessionDelta } from "./event-bus.js";
5
+ import { logger } from "./logger.js";
6
+ import { savePayloadDocSerialized } from "./payload-store.js";
7
+ const log = logger.child({ module: "recorder" });
8
+ export function usagePatchFrom(id, at, usage, stepKey) {
9
+ if (!usage)
10
+ return null;
11
+ const { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens } = usage;
12
+ if (inputTokens === undefined &&
13
+ outputTokens === undefined &&
14
+ cacheReadTokens === undefined &&
15
+ cacheWriteTokens === undefined) {
16
+ return null;
17
+ }
18
+ return {
19
+ id,
20
+ at,
21
+ t: "usage",
22
+ input: inputTokens,
23
+ output: outputTokens,
24
+ cacheRead: cacheReadTokens,
25
+ cacheWrite: cacheWriteTokens,
26
+ ...(stepKey ? { stepKey } : {}),
27
+ };
28
+ }
29
+ export function stepKeyFrom(turnId, stepIndex) {
30
+ if (typeof turnId === "string" && typeof stepIndex === "number") {
31
+ return `${turnId}#${stepIndex}`;
32
+ }
33
+ return undefined;
34
+ }
35
+ export function storePath() {
36
+ return process.env.EVE_RECORDER_PATH ?? join(process.cwd(), ".workflow-data", "sessions.jsonl");
37
+ }
38
+ export function appendPatchStrict(patch, file = storePath()) {
39
+ mkdirSync(dirname(file), { recursive: true });
40
+ appendFileSync(file, `${JSON.stringify(patch)}\n`);
41
+ publishSessionDelta({ sessionId: patch.id, kind: patch.t, at: patch.at });
42
+ }
43
+ export function appendPatch(patch, file = storePath()) {
44
+ try {
45
+ appendPatchStrict(patch, file);
46
+ }
47
+ catch (err) {
48
+ log.error({ err, id: patch.id, t: patch.t }, "failed to append patch");
49
+ }
50
+ }
51
+ export function findSessionIdByInboundMarker(marker, file = storePath(), maxTailBytes = 2 * 1024 * 1024) {
52
+ const needle = `${JSON.stringify(marker).slice(1, -1)}\\"`;
53
+ const rawNeedle = `${marker}"`;
54
+ let raw;
55
+ try {
56
+ const size = statSync(file).size;
57
+ const start = Math.max(0, size - maxTailBytes);
58
+ const fd = openSync(file, "r");
59
+ try {
60
+ const buf = Buffer.alloc(size - start);
61
+ readSync(fd, buf, 0, buf.length, start);
62
+ raw = buf.toString("utf8");
63
+ }
64
+ finally {
65
+ closeSync(fd);
66
+ }
67
+ }
68
+ catch (err) {
69
+ if (err.code === "ENOENT")
70
+ return null;
71
+ throw err;
72
+ }
73
+ const lines = raw.split("\n");
74
+ for (let i = lines.length - 1; i >= 0; i--) {
75
+ const trimmed = lines[i].trim();
76
+ if (!trimmed || !(trimmed.includes(needle) || trimmed.includes(rawNeedle)))
77
+ continue;
78
+ try {
79
+ const p = JSON.parse(trimmed);
80
+ if (p?.t === "inbound" && typeof p.id === "string" && typeof p.text === "string") {
81
+ if (p.text.includes(rawNeedle))
82
+ return p.id;
83
+ }
84
+ }
85
+ catch {
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+ export function readPatches(file = storePath()) {
91
+ let raw;
92
+ try {
93
+ raw = readFileSync(file, "utf8");
94
+ }
95
+ catch (err) {
96
+ if (err.code === "ENOENT")
97
+ return [];
98
+ throw err;
99
+ }
100
+ const patches = [];
101
+ for (const line of raw.split("\n")) {
102
+ const trimmed = line.trim();
103
+ if (!trimmed)
104
+ continue;
105
+ try {
106
+ const p = JSON.parse(trimmed);
107
+ if (p && typeof p.id === "string" && typeof p.t === "string")
108
+ patches.push(p);
109
+ }
110
+ catch {
111
+ log.warn("skipping malformed patch line");
112
+ }
113
+ }
114
+ return patches;
115
+ }
116
+ const KIND_TO_SOURCE = {
117
+ "channel:prospect": "prospect",
118
+ prospect: "prospect",
119
+ "channel:partners": "partners",
120
+ partners: "partners",
121
+ "channel:instagram": "instagram",
122
+ instagram: "instagram",
123
+ "channel:answers": "answers",
124
+ answers: "answers",
125
+ "channel:build-run": "build",
126
+ build: "build",
127
+ "channel:email": "email",
128
+ email: "email",
129
+ "channel:health-probe": "health",
130
+ health: "health",
131
+ };
132
+ export function deriveSource(token, channelKind) {
133
+ if (token?.startsWith("eve:eve:"))
134
+ token = token.slice("eve:".length);
135
+ const base = {
136
+ kind: "unknown",
137
+ repo: null,
138
+ prNumber: null,
139
+ sender: null,
140
+ url: null,
141
+ continuationToken: token ?? null,
142
+ };
143
+ const byKind = channelKind ? KIND_TO_SOURCE[channelKind] : undefined;
144
+ if (byKind)
145
+ return { ...base, kind: byKind };
146
+ if (!token)
147
+ return base;
148
+ if (token.startsWith("build-run:"))
149
+ return { ...base, kind: "build" };
150
+ if (token.startsWith("prospect:"))
151
+ return { ...base, kind: "prospect" };
152
+ if (token.startsWith("health-probe:"))
153
+ return { ...base, kind: "health" };
154
+ if (token.startsWith("eve:"))
155
+ return { ...base, kind: "web" };
156
+ if (token.startsWith("sentry:"))
157
+ return { ...base, kind: "sentry" };
158
+ if (token.startsWith("telegram:"))
159
+ return { ...base, kind: "telegram" };
160
+ if (token.startsWith("email:"))
161
+ return { ...base, kind: "email" };
162
+ if (token.startsWith("fixit:"))
163
+ return { ...base, kind: "fixit" };
164
+ const m = token.match(/^repo:[^:]+:(pull|issue):(\d+)/);
165
+ if (m) {
166
+ return { ...base, kind: "mention", prNumber: Number(m[2]) };
167
+ }
168
+ return base;
169
+ }
170
+ export function defaultTriggerFor(kind) {
171
+ switch (kind) {
172
+ case "pr":
173
+ case "mention":
174
+ case "review":
175
+ case "journey_refine":
176
+ case "sentry":
177
+ case "health":
178
+ return "webhook";
179
+ case "web":
180
+ case "telegram":
181
+ case "email":
182
+ case "fixit":
183
+ return "manual";
184
+ default:
185
+ return null;
186
+ }
187
+ }
188
+ export function parseGithubInbound(text) {
189
+ const field = (re) => text.match(re)?.[1]?.trim() ?? null;
190
+ const numStr = field(/pull_request_number:\s*(\d+)/) ?? field(/issue_number:\s*(\d+)/);
191
+ const close = text.indexOf("</github_context>");
192
+ let body = (close >= 0 ? text.slice(close + "</github_context>".length) : text).trim();
193
+ const modeMatch = body.match(/^mode:\s*(review|journey_refine)\s*(?:\n|$)/);
194
+ const mode = modeMatch ? modeMatch[1] : null;
195
+ let sentinelRepo = null;
196
+ let sentinelNumber = null;
197
+ let sentinelTitle = null;
198
+ if (modeMatch) {
199
+ body = body.slice(modeMatch[0].length);
200
+ let m;
201
+ while ((m = body.match(/^(headSha|Repo|Number|Title):[ \t]*([^\n]*)\n?/))) {
202
+ const val = m[2].trim();
203
+ if (m[1] === "Repo")
204
+ sentinelRepo = val || null;
205
+ else if (m[1] === "Number")
206
+ sentinelNumber = /^\d+$/.test(val) ? Number(val) : null;
207
+ else if (m[1] === "Title")
208
+ sentinelTitle = val || null;
209
+ body = body.slice(m[0].length);
210
+ }
211
+ body = body.trim();
212
+ }
213
+ return {
214
+ repo: field(/repository:\s*(\S+)/) ?? sentinelRepo,
215
+ number: numStr != null ? Number(numStr) : sentinelNumber,
216
+ sender: field(/sender:\s*(\S+)/),
217
+ url: field(/comment_url:\s*(\S+)/),
218
+ body,
219
+ mode,
220
+ title: sentinelTitle,
221
+ isPrOpened: /^Pull request \w+:\s*#\d+/.test(body),
222
+ };
223
+ }
224
+ export function parseSentryInbound(text) {
225
+ const field = (re) => text.match(re)?.[1]?.trim() ?? null;
226
+ return {
227
+ repo: field(/target_repo:\s*(\S+)/),
228
+ url: field(/issue_url:\s*(\S+)/),
229
+ shortId: field(/short_id:\s*(\S+)/),
230
+ environment: field(/environment:\s*(\S+)/),
231
+ };
232
+ }
233
+ function truncate(s, n = 80) {
234
+ const clean = s.replace(/\s+/g, " ").trim();
235
+ return clean.length > n ? `${clean.slice(0, n - 1)}…` : clean;
236
+ }
237
+ const SENSITIVE_KEY = /(token|secret|password|authorization|api[-_]?key|^key$|_key$|\bkey\b)/i;
238
+ const MAX_STRING = 500;
239
+ const MAX_DEPTH = 6;
240
+ export const MAX_TOOL_STRING = 2000;
241
+ export const FULL_PAYLOAD_DEPTH = 12;
242
+ const MAX_ARRAY_ITEMS = 200;
243
+ const MAX_OBJECT_KEYS = 200;
244
+ const MAX_TOTAL_NODES = 5_000;
245
+ export function redactValue(value, maxString = MAX_STRING, maxDepth = MAX_DEPTH, budget = {
246
+ nodes: MAX_TOTAL_NODES,
247
+ arrayItems: MAX_ARRAY_ITEMS,
248
+ objectKeys: MAX_OBJECT_KEYS,
249
+ }) {
250
+ const masked = redactDeep(value, 0, maxString, maxDepth, budget);
251
+ try {
252
+ JSON.stringify(masked);
253
+ }
254
+ catch {
255
+ return "[unserializable]";
256
+ }
257
+ return masked;
258
+ }
259
+ const SECRET_STRING_PATTERNS = [
260
+ [
261
+ /([?&][^=&\s"']*(?:token|secret|password|signature|bypass|credential|apikey|api[-_]key|access[-_]key|sess(?:ion)?id)[^=&\s"']*=)[^&\s"']+/gi,
262
+ "$1[redacted]",
263
+ ],
264
+ [/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/g, "$1 [redacted]"],
265
+ [
266
+ /\b(sk-[A-Za-z0-9_-]{10,}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16})\b/g,
267
+ "[redacted]",
268
+ ],
269
+ [/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\b/g, "[redacted]"],
270
+ ];
271
+ function scrubSecretStrings(s) {
272
+ let out = s;
273
+ for (const [re, sub] of SECRET_STRING_PATTERNS)
274
+ out = out.replace(re, sub);
275
+ return out;
276
+ }
277
+ function redactDeep(value, depth, maxString, maxDepth, budget) {
278
+ if (budget.nodes <= 0)
279
+ return "[truncated]";
280
+ budget.nodes--;
281
+ if (typeof value === "string") {
282
+ const scrubbed = scrubSecretStrings(value);
283
+ return scrubbed.length > maxString ? `${scrubbed.slice(0, maxString - 1)}…` : scrubbed;
284
+ }
285
+ if (value === null || typeof value !== "object")
286
+ return value;
287
+ if (depth >= maxDepth)
288
+ return "[truncated]";
289
+ if (Array.isArray(value)) {
290
+ const capped = value
291
+ .slice(0, budget.arrayItems)
292
+ .map((v) => redactDeep(v, depth + 1, maxString, maxDepth, budget));
293
+ if (value.length > budget.arrayItems) {
294
+ capped.push(`[+${value.length - budget.arrayItems} items truncated]`);
295
+ }
296
+ return capped;
297
+ }
298
+ const out = {};
299
+ const rec = value;
300
+ let kept = 0;
301
+ let truncated = false;
302
+ for (const k in rec) {
303
+ if (!Object.hasOwn(rec, k))
304
+ continue;
305
+ if (kept >= budget.objectKeys || budget.nodes <= 0) {
306
+ truncated = true;
307
+ break;
308
+ }
309
+ out[k] = SENSITIVE_KEY.test(k)
310
+ ? "[redacted]"
311
+ : redactDeep(rec[k], depth + 1, maxString, maxDepth, budget);
312
+ kept++;
313
+ }
314
+ if (truncated)
315
+ out["[truncatedKeys]"] = true;
316
+ return out;
317
+ }
318
+ export function redactValueFull(value) {
319
+ return redactValue(value, Number.POSITIVE_INFINITY, FULL_PAYLOAD_DEPTH, {
320
+ nodes: Number.POSITIVE_INFINITY,
321
+ arrayItems: Number.POSITIVE_INFINITY,
322
+ objectKeys: Number.POSITIVE_INFINITY,
323
+ });
324
+ }
325
+ export function fullPayloadDocIdFor(result, summary) {
326
+ const full = redactValueFull(result);
327
+ let fullSerialized;
328
+ let summarySerialized;
329
+ try {
330
+ fullSerialized = JSON.stringify(full);
331
+ summarySerialized = JSON.stringify(summary);
332
+ }
333
+ catch {
334
+ return undefined;
335
+ }
336
+ if (fullSerialized === undefined || fullSerialized === summarySerialized)
337
+ return undefined;
338
+ return savePayloadDocSerialized(fullSerialized) ?? undefined;
339
+ }
340
+ function titleFor(source, inbound) {
341
+ if (source.kind === "pr" || source.kind === "review") {
342
+ const gh = parseGithubInbound(inbound);
343
+ const prTitle = gh.title ?? (gh.isPrOpened ? gh.body.replace(/^Pull request \w+:\s*#\d+\s*/, "").trim() : "");
344
+ const num = source.prNumber ?? gh.number;
345
+ const prefix = source.kind === "review" ? "Review PR" : "PR";
346
+ return prTitle ? `${prefix} #${num} · ${truncate(prTitle, 72)}` : `${prefix} #${num}`;
347
+ }
348
+ if (source.kind === "mention") {
349
+ const gh = parseGithubInbound(inbound);
350
+ const where = source.prNumber ? `#${source.prNumber} ` : "";
351
+ return gh.body ? `${where}@mention · ${truncate(gh.body, 64)}` : `${where}@mention`;
352
+ }
353
+ if (source.kind === "journey_refine") {
354
+ const num = source.prNumber ?? parseGithubInbound(inbound).number;
355
+ return num != null ? `Refine journeys · PR #${num}` : "Refine journeys";
356
+ }
357
+ if (source.kind === "sentry") {
358
+ const s = parseSentryInbound(inbound);
359
+ const env = s.environment ? ` · ${s.environment}` : "";
360
+ return s.shortId ? `Sentry · ${s.shortId}${env}` : "Sentry issue";
361
+ }
362
+ if (source.kind === "prospect") {
363
+ return inbound ? `Prospect · ${truncate(inbound, 72)}` : "Prospect run";
364
+ }
365
+ if (source.kind === "partners") {
366
+ return inbound ? `Partneři · ${truncate(inbound, 72)}` : "Partner scan";
367
+ }
368
+ if (source.kind === "instagram") {
369
+ return inbound ? `Instagram · ${truncate(inbound, 72)}` : "Instagram scan";
370
+ }
371
+ if (source.kind === "answers") {
372
+ return inbound ? `Odpovědi · ${truncate(inbound, 72)}` : "Answers run";
373
+ }
374
+ if (source.kind === "build") {
375
+ return inbound ? `Build · ${truncate(inbound, 72)}` : "Build preview";
376
+ }
377
+ if (source.kind === "email") {
378
+ return inbound ? `Email · ${truncate(inbound, 72)}` : "Email";
379
+ }
380
+ return inbound ? truncate(inbound, 80) : "Session";
381
+ }
382
+ export const MAX_NARRATIVE_TEXT = 4000;
383
+ const MAX_MESSAGES = 200;
384
+ const MAX_USER_MESSAGES = 200;
385
+ const MAX_REASONING = 200;
386
+ const MAX_STEP_TIMINGS = 200;
387
+ export function capText(s, n = MAX_NARRATIVE_TEXT) {
388
+ return s.length > n ? `${s.slice(0, n - 1)}…` : s;
389
+ }
390
+ export const MAX_CONTEXT_SECTION_TEXT = 131_072;
391
+ const MAX_CONTEXT_SECTIONS = 40;
392
+ export function recordContext(sessionId, sections, at = new Date().toISOString()) {
393
+ const capped = sections
394
+ .filter((s) => s.label && s.text)
395
+ .slice(0, MAX_CONTEXT_SECTIONS)
396
+ .map((s) => ({
397
+ label: capText(s.label, 200),
398
+ text: capText(s.text, MAX_CONTEXT_SECTION_TEXT),
399
+ }));
400
+ if (capped.length === 0)
401
+ return;
402
+ appendPatch({ id: sessionId, at, t: "context", sections: capped });
403
+ }
404
+ export function recordContextStrict(sessionId, sections, at = new Date().toISOString()) {
405
+ const capped = sections
406
+ .filter((s) => s.label && s.text)
407
+ .slice(0, MAX_CONTEXT_SECTIONS)
408
+ .map((s) => ({
409
+ label: capText(s.label, 200),
410
+ text: capText(s.text, MAX_CONTEXT_SECTION_TEXT),
411
+ }));
412
+ if (capped.length === 0)
413
+ return;
414
+ appendPatchStrict({ id: sessionId, at, t: "context", sections: capped });
415
+ }
416
+ export function recordSystemAction(sessionId, input) {
417
+ const startedAt = input.startedAt ?? new Date().toISOString();
418
+ const endedAt = input.endedAt ?? new Date().toISOString();
419
+ const callId = input.callId ?? `sys_${randomUUID()}`;
420
+ const summary = redactValue(input.result, MAX_TOOL_STRING);
421
+ const payloadDocId = input.result !== undefined ? fullPayloadDocIdFor(input.result, summary) : undefined;
422
+ appendPatch({
423
+ id: sessionId,
424
+ at: startedAt,
425
+ t: "action_started",
426
+ callId,
427
+ tool: input.tool,
428
+ args: redactValue(input.args, MAX_TOOL_STRING),
429
+ system: true,
430
+ });
431
+ appendPatch({
432
+ id: sessionId,
433
+ at: endedAt,
434
+ t: "action_result",
435
+ callId,
436
+ tool: input.tool,
437
+ status: input.ok ? "completed" : "failed",
438
+ ok: input.ok,
439
+ resultSummary: summary,
440
+ ...(input.error !== undefined ? { error: input.error } : {}),
441
+ ...(payloadDocId ? { payloadDocId } : {}),
442
+ });
443
+ }
444
+ function emptyRow(id, at) {
445
+ return {
446
+ id,
447
+ source: {
448
+ kind: "unknown",
449
+ repo: null,
450
+ prNumber: null,
451
+ sender: null,
452
+ url: null,
453
+ continuationToken: null,
454
+ },
455
+ title: "Session",
456
+ status: "running",
457
+ stalled: false,
458
+ prompt: null,
459
+ summary: null,
460
+ channelKind: null,
461
+ model: null,
462
+ flow: null,
463
+ env: null,
464
+ trigger: null,
465
+ mode: null,
466
+ parentSessionId: null,
467
+ workflowFlow: null,
468
+ childCount: 0,
469
+ waitingTool: null,
470
+ waitingCallId: null,
471
+ approvalKind: null,
472
+ result: null,
473
+ inputTokens: 0,
474
+ outputTokens: 0,
475
+ cacheReadTokens: 0,
476
+ cacheWriteTokens: 0,
477
+ turns: 0,
478
+ actions: [],
479
+ stepTimings: [],
480
+ messages: [],
481
+ userMessages: [],
482
+ reasoning: [],
483
+ context: [],
484
+ error: null,
485
+ startedAt: at,
486
+ updatedAt: at,
487
+ completedAt: null,
488
+ };
489
+ }
490
+ function firstTime(seen, id, key) {
491
+ if (!seen)
492
+ return true;
493
+ let set = seen.get(id);
494
+ if (!set) {
495
+ set = new Set();
496
+ seen.set(id, set);
497
+ }
498
+ if (set.has(key))
499
+ return false;
500
+ set.add(key);
501
+ return true;
502
+ }
503
+ export function applyPatch(rows, p, seen) {
504
+ if (p.t === "row") {
505
+ rows.set(p.id, {
506
+ ...p.row,
507
+ context: p.row.context ?? [],
508
+ stepTimings: p.row.stepTimings ?? [],
509
+ userMessages: p.row.userMessages ?? [],
510
+ trigger: p.row.trigger ?? null,
511
+ mode: p.row.mode ?? null,
512
+ waitingCallId: p.row.waitingCallId ?? null,
513
+ });
514
+ return;
515
+ }
516
+ const row = rows.get(p.id) ?? emptyRow(p.id, p.at);
517
+ row.updatedAt = p.at;
518
+ switch (p.t) {
519
+ case "start": {
520
+ row.channelKind = p.channelKind ?? row.channelKind;
521
+ row.model = p.model ?? row.model;
522
+ row.flow = p.flow ?? row.flow;
523
+ row.env = p.env ?? row.env;
524
+ row.parentSessionId = p.parentSessionId ?? row.parentSessionId;
525
+ row.source = deriveSource(p.continuationToken ?? null, row.channelKind);
526
+ row.trigger = p.trigger ?? row.trigger ?? defaultTriggerFor(row.source.kind);
527
+ break;
528
+ }
529
+ case "inbound": {
530
+ if (row.prompt == null) {
531
+ if (row.source.kind === "mention" ||
532
+ row.source.kind === "pr" ||
533
+ row.source.kind === "review" ||
534
+ row.source.kind === "journey_refine") {
535
+ const gh = parseGithubInbound(p.text);
536
+ if (gh.repo)
537
+ row.source.repo = gh.repo;
538
+ if (gh.sender)
539
+ row.source.sender = gh.sender;
540
+ if (gh.url)
541
+ row.source.url = gh.url;
542
+ if (gh.number != null)
543
+ row.source.prNumber = gh.number;
544
+ row.source.kind =
545
+ gh.mode === "journey_refine"
546
+ ? "journey_refine"
547
+ : gh.mode === "review" || gh.isPrOpened
548
+ ? "review"
549
+ : "mention";
550
+ row.mode = gh.mode ?? row.mode;
551
+ row.prompt = gh.body;
552
+ }
553
+ else if (row.source.kind === "sentry") {
554
+ const s = parseSentryInbound(p.text);
555
+ if (s.repo)
556
+ row.source.repo = s.repo;
557
+ if (s.url)
558
+ row.source.url = s.url;
559
+ row.prompt = p.text.trim();
560
+ }
561
+ else if (row.source.kind === "unknown") {
562
+ const gh = parseGithubInbound(p.text);
563
+ if (gh.mode === "review" || gh.mode === "journey_refine") {
564
+ if (gh.repo)
565
+ row.source.repo = gh.repo;
566
+ if (gh.number != null)
567
+ row.source.prNumber = gh.number;
568
+ row.source.kind = gh.mode === "journey_refine" ? "journey_refine" : "review";
569
+ row.mode = gh.mode;
570
+ row.prompt = gh.body;
571
+ }
572
+ else {
573
+ row.prompt = p.text.trim();
574
+ }
575
+ }
576
+ else {
577
+ row.prompt = p.text.trim();
578
+ }
579
+ row.title = titleFor(row.source, p.text);
580
+ }
581
+ const userText = p.text.trim();
582
+ if (userText) {
583
+ row.userMessages.push({ at: p.at, text: capText(userText) });
584
+ if (row.userMessages.length > MAX_USER_MESSAGES)
585
+ row.userMessages.shift();
586
+ }
587
+ if (row.status === "waiting")
588
+ row.status = "running";
589
+ break;
590
+ }
591
+ case "output": {
592
+ const text = p.text.trim();
593
+ if (text && (p.stepKey == null || firstTime(seen, p.id, `out:${p.stepKey}`))) {
594
+ row.summary = text;
595
+ row.turns += 1;
596
+ if (row.approvalKind !== null) {
597
+ row.approvalKind = null;
598
+ row.waitingTool = null;
599
+ row.waitingCallId = null;
600
+ if (row.status === "waiting")
601
+ row.status = "running";
602
+ }
603
+ row.messages.push({
604
+ at: p.at,
605
+ text: capText(text),
606
+ finishReason: p.finishReason ?? null,
607
+ ...(p.stepKey ? { stepKey: p.stepKey } : {}),
608
+ });
609
+ if (row.messages.length > MAX_MESSAGES)
610
+ row.messages.shift();
611
+ }
612
+ break;
613
+ }
614
+ case "reasoning": {
615
+ const text = p.text.trim();
616
+ if (text) {
617
+ row.reasoning.push({ at: p.at, text: capText(text) });
618
+ if (row.reasoning.length > MAX_REASONING)
619
+ row.reasoning.shift();
620
+ }
621
+ break;
622
+ }
623
+ case "context": {
624
+ for (const s of p.sections) {
625
+ if (!s || typeof s.label !== "string" || typeof s.text !== "string")
626
+ continue;
627
+ const section = { label: s.label, text: capText(s.text, MAX_CONTEXT_SECTION_TEXT) };
628
+ const i = row.context.findIndex((c) => c.label === section.label);
629
+ if (i >= 0)
630
+ row.context[i] = section;
631
+ else if (row.context.length < MAX_CONTEXT_SECTIONS)
632
+ row.context.push(section);
633
+ }
634
+ break;
635
+ }
636
+ case "action": {
637
+ row.actions.push({ tool: p.tool, status: p.status, at: p.at });
638
+ break;
639
+ }
640
+ case "action_started": {
641
+ const existing = row.actions.find((a) => a.callId === p.callId);
642
+ if (existing) {
643
+ existing.tool = p.tool;
644
+ existing.args = p.args;
645
+ existing.at = p.at;
646
+ existing.status = "running";
647
+ delete existing.ok;
648
+ delete existing.resultSummary;
649
+ delete existing.durationMs;
650
+ delete existing.error;
651
+ delete existing.payloadDocId;
652
+ break;
653
+ }
654
+ row.actions.push({
655
+ tool: p.tool,
656
+ callId: p.callId,
657
+ args: p.args,
658
+ at: p.at,
659
+ status: "running",
660
+ ...(p.system ? { system: true } : {}),
661
+ });
662
+ break;
663
+ }
664
+ case "action_result": {
665
+ const started = row.actions.find((a) => a.callId === p.callId && a.status === "running");
666
+ const target = started ??
667
+ (() => {
668
+ const fresh = { tool: p.tool, callId: p.callId, at: p.at, status: "" };
669
+ row.actions.push(fresh);
670
+ return fresh;
671
+ })();
672
+ target.status = p.status;
673
+ target.ok = p.ok;
674
+ target.resultSummary = p.resultSummary;
675
+ if (p.error !== undefined)
676
+ target.error = p.error;
677
+ if (p.detail !== undefined)
678
+ target.detail = p.detail;
679
+ if (p.model !== undefined)
680
+ target.model = p.model;
681
+ if (p.usage !== undefined) {
682
+ target.usage = p.usage;
683
+ if (firstTime(seen, p.id, `ausg:${p.callId}`)) {
684
+ row.inputTokens += p.usage.inputTokens ?? 0;
685
+ row.outputTokens += p.usage.outputTokens ?? 0;
686
+ row.cacheReadTokens += p.usage.cacheReadTokens ?? 0;
687
+ row.cacheWriteTokens += p.usage.cacheWriteTokens ?? 0;
688
+ }
689
+ }
690
+ if (p.payloadDocId !== undefined)
691
+ target.payloadDocId = p.payloadDocId;
692
+ if (started) {
693
+ const ms = Date.parse(p.at) - Date.parse(started.at);
694
+ if (Number.isFinite(ms) && ms >= 0)
695
+ target.durationMs = ms;
696
+ }
697
+ const resolvesPark = row.waitingCallId !== null ? p.callId === row.waitingCallId : p.tool === row.waitingTool;
698
+ if (row.approvalKind !== null && resolvesPark) {
699
+ row.approvalKind = null;
700
+ row.waitingTool = null;
701
+ row.waitingCallId = null;
702
+ if (row.status === "waiting")
703
+ row.status = "running";
704
+ }
705
+ break;
706
+ }
707
+ case "waiting": {
708
+ if (row.status === "running")
709
+ row.status = "waiting";
710
+ break;
711
+ }
712
+ case "link": {
713
+ row.parentSessionId = p.parentSessionId ?? row.parentSessionId;
714
+ break;
715
+ }
716
+ case "waiting_meta": {
717
+ row.waitingTool = p.waitingTool;
718
+ row.waitingCallId = p.waitingCallId ?? null;
719
+ row.approvalKind = p.approvalKind;
720
+ break;
721
+ }
722
+ case "result": {
723
+ const redacted = redactValue(p.result);
724
+ row.result = typeof redacted === "string" ? redacted : JSON.stringify(redacted);
725
+ break;
726
+ }
727
+ case "usage": {
728
+ if (p.stepKey == null || firstTime(seen, p.id, `usg:${p.stepKey}`)) {
729
+ row.inputTokens += p.input ?? 0;
730
+ row.outputTokens += p.output ?? 0;
731
+ row.cacheReadTokens += p.cacheRead ?? 0;
732
+ row.cacheWriteTokens += p.cacheWrite ?? 0;
733
+ }
734
+ break;
735
+ }
736
+ case "step_timing": {
737
+ if (p.stepKey == null || firstTime(seen, p.id, `tim:${p.stepKey}`)) {
738
+ const started = Date.parse(p.startedAt);
739
+ const completed = Date.parse(p.at);
740
+ const first = p.firstTokenAt ? Date.parse(p.firstTokenAt) : Number.NaN;
741
+ const durationMs = completed - started;
742
+ const ttftMs = first - started;
743
+ const genMs = completed - first;
744
+ const tokensPerSec = p.outputTokens !== undefined && p.outputTokens > 0 && genMs > 0
745
+ ? Math.round((p.outputTokens / (genMs / 1000)) * 10) / 10
746
+ : undefined;
747
+ row.stepTimings.push({
748
+ ...(p.stepKey ? { stepKey: p.stepKey } : {}),
749
+ startedAt: p.startedAt,
750
+ ...(p.firstTokenAt ? { firstTokenAt: p.firstTokenAt } : {}),
751
+ completedAt: p.at,
752
+ ...(Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {}),
753
+ ...(Number.isFinite(ttftMs) && ttftMs >= 0 ? { ttftMs } : {}),
754
+ ...(p.outputTokens !== undefined ? { outputTokens: p.outputTokens } : {}),
755
+ ...(tokensPerSec !== undefined ? { tokensPerSec } : {}),
756
+ });
757
+ if (row.stepTimings.length > MAX_STEP_TIMINGS)
758
+ row.stepTimings.shift();
759
+ }
760
+ break;
761
+ }
762
+ case "done": {
763
+ row.status = "done";
764
+ row.completedAt = p.at;
765
+ break;
766
+ }
767
+ case "failed": {
768
+ row.status = "failed";
769
+ row.error = p.message ?? p.code ?? "failed";
770
+ row.completedAt = p.at;
771
+ break;
772
+ }
773
+ }
774
+ rows.set(p.id, row);
775
+ }
776
+ export function compareSessionsDesc(a, b) {
777
+ return b.startedAt.localeCompare(a.startedAt) || b.id.localeCompare(a.id);
778
+ }
779
+ export function reduceSessions(patches) {
780
+ const rows = new Map();
781
+ const seen = new Map();
782
+ for (const p of patches)
783
+ applyPatch(rows, p, seen);
784
+ return [...rows.values()].sort(compareSessionsDesc);
785
+ }
786
+ export function readSessions(file = storePath()) {
787
+ return reduceSessions(readPatches(file));
788
+ }
789
+ let idx = null;
790
+ export function __resetSessionIndex() {
791
+ idx = null;
792
+ }
793
+ function refreshIndex(file) {
794
+ if (!idx || idx.file !== file)
795
+ idx = { file, offset: 0, rows: new Map(), seen: new Map() };
796
+ let size;
797
+ try {
798
+ size = statSync(file).size;
799
+ }
800
+ catch (err) {
801
+ if (err.code === "ENOENT") {
802
+ idx.offset = 0;
803
+ idx.rows.clear();
804
+ idx.seen.clear();
805
+ return idx.rows;
806
+ }
807
+ throw err;
808
+ }
809
+ if (size < idx.offset) {
810
+ idx.offset = 0;
811
+ idx.rows.clear();
812
+ idx.seen.clear();
813
+ }
814
+ if (size > idx.offset) {
815
+ const fd = openSync(file, "r");
816
+ try {
817
+ const len = size - idx.offset;
818
+ const buf = Buffer.allocUnsafe(len);
819
+ const read = readSync(fd, buf, 0, len, idx.offset);
820
+ const text = buf.toString("utf8", 0, read);
821
+ const lastNl = text.lastIndexOf("\n");
822
+ if (lastNl < 0)
823
+ return idx.rows;
824
+ const complete = text.slice(0, lastNl);
825
+ for (const line of complete.split("\n")) {
826
+ const trimmed = line.trim();
827
+ if (!trimmed)
828
+ continue;
829
+ try {
830
+ const p = JSON.parse(trimmed);
831
+ if (p && typeof p.id === "string" && typeof p.t === "string")
832
+ applyPatch(idx.rows, p, idx.seen);
833
+ }
834
+ catch {
835
+ log.warn("skipping malformed patch line (index tail)");
836
+ }
837
+ }
838
+ idx.offset += Buffer.byteLength(complete, "utf8") + 1;
839
+ }
840
+ finally {
841
+ closeSync(fd);
842
+ }
843
+ }
844
+ return idx.rows;
845
+ }
846
+ export function readSessionsIndexed(file = storePath()) {
847
+ return [...refreshIndex(file).values()].sort(compareSessionsDesc);
848
+ }
849
+ export const DEFAULT_PAGE_LIMIT = 50;
850
+ export const MAX_PAGE_LIMIT = 200;
851
+ const SESSION_STATUS_FILTERS = [
852
+ "active",
853
+ "parked",
854
+ "stalled",
855
+ "failed",
856
+ "prod",
857
+ "dev",
858
+ ];
859
+ export function isSessionStatusFilter(v) {
860
+ return SESSION_STATUS_FILTERS.includes(v);
861
+ }
862
+ export function isActiveSession(r) {
863
+ if (r.status === "running")
864
+ return true;
865
+ if (r.status !== "waiting")
866
+ return false;
867
+ return r.approvalKind !== null || (r.turns === 0 && r.summary === null);
868
+ }
869
+ function matchesStatusFilter(r, f, isStalled) {
870
+ if (f === "active")
871
+ return isActiveSession(r);
872
+ if (f === "parked")
873
+ return r.approvalKind === "approval" && r.status !== "done" && r.status !== "failed";
874
+ if (f === "stalled")
875
+ return isStalled(r);
876
+ if (f === "failed")
877
+ return r.status === "failed";
878
+ if (f === "prod")
879
+ return r.env === "prod";
880
+ return r.env === "dev";
881
+ }
882
+ export const SESSION_FACETS = {
883
+ repo: (r) => r.source.repo,
884
+ };
885
+ export function isSessionFacet(v) {
886
+ return Object.hasOwn(SESSION_FACETS, v);
887
+ }
888
+ const CHAT_SOURCE_KINDS = new Set([
889
+ "web",
890
+ "email",
891
+ "telegram",
892
+ "fixit",
893
+ ]);
894
+ const HEALTH_PING_MARKER = "CI health ping:";
895
+ export function isChatSession(r) {
896
+ if (!CHAT_SOURCE_KINDS.has(r.source.kind))
897
+ return false;
898
+ return !(r.source.kind === "web" &&
899
+ ((r.title ?? "").startsWith(HEALTH_PING_MARKER) ||
900
+ (r.prompt ?? "").startsWith(HEALTH_PING_MARKER)));
901
+ }
902
+ export function encodeCursor(r) {
903
+ return Buffer.from(`${r.startedAt}|${r.id}`, "utf8").toString("base64");
904
+ }
905
+ export function decodeCursor(c) {
906
+ try {
907
+ const s = Buffer.from(c, "base64").toString("utf8");
908
+ const i = s.indexOf("|");
909
+ if (i <= 0 || i >= s.length - 1)
910
+ return null;
911
+ return { startedAt: s.slice(0, i), id: s.slice(i + 1) };
912
+ }
913
+ catch {
914
+ return null;
915
+ }
916
+ }
917
+ export function parseSessionsQuery(rawUrl) {
918
+ let limit;
919
+ let before;
920
+ let view;
921
+ let root;
922
+ let flow;
923
+ let status;
924
+ let groupBy;
925
+ let group;
926
+ let scope;
927
+ try {
928
+ const u = new URL(rawUrl, "http://localhost");
929
+ const l = u.searchParams.get("limit");
930
+ if (l != null) {
931
+ const n = Number(l);
932
+ if (Number.isFinite(n))
933
+ limit = n;
934
+ }
935
+ const b = u.searchParams.get("before");
936
+ if (b)
937
+ before = b;
938
+ const v = u.searchParams.get("view");
939
+ if (v === "top" || v === "all" || v === "approvals")
940
+ view = v;
941
+ const r = u.searchParams.get("root");
942
+ if (r)
943
+ root = r;
944
+ const fl = u.searchParams.get("flow");
945
+ if (fl)
946
+ flow = fl;
947
+ const st = u.searchParams.get("status");
948
+ if (st && isSessionStatusFilter(st))
949
+ status = st;
950
+ const gb = u.searchParams.get("groupBy");
951
+ if (gb && isSessionFacet(gb))
952
+ groupBy = gb;
953
+ const g = u.searchParams.get("group");
954
+ if (g)
955
+ group = g;
956
+ if (u.searchParams.get("scope") === "chat")
957
+ scope = "chat";
958
+ }
959
+ catch {
960
+ }
961
+ const out = {
962
+ limit,
963
+ before,
964
+ };
965
+ if (view !== undefined)
966
+ out.view = view;
967
+ if (root !== undefined)
968
+ out.root = root;
969
+ if (flow !== undefined)
970
+ out.flow = flow;
971
+ if (status !== undefined)
972
+ out.status = status;
973
+ if (groupBy !== undefined)
974
+ out.groupBy = groupBy;
975
+ if (group !== undefined)
976
+ out.group = group;
977
+ if (scope !== undefined)
978
+ out.scope = scope;
979
+ return out;
980
+ }
981
+ export function collectTree(rows, rootId) {
982
+ const childrenByParent = new Map();
983
+ for (const r of rows) {
984
+ if (r.parentSessionId != null) {
985
+ const list = childrenByParent.get(r.parentSessionId) ?? [];
986
+ list.push(r.id);
987
+ childrenByParent.set(r.parentSessionId, list);
988
+ }
989
+ }
990
+ const wanted = new Set([rootId]);
991
+ const queue = [rootId];
992
+ for (let head = 0; head < queue.length; head++) {
993
+ for (const child of childrenByParent.get(queue[head]) ?? []) {
994
+ if (!wanted.has(child)) {
995
+ wanted.add(child);
996
+ queue.push(child);
997
+ }
998
+ }
999
+ }
1000
+ return wanted;
1001
+ }
1002
+ export function isApprovalParked(r) {
1003
+ return r.status !== "done" && r.status !== "failed" && r.approvalKind === "approval";
1004
+ }
1005
+ export function dismissSession(id, file = storePath()) {
1006
+ const row = readSessionsIndexed(file).find((r) => r.id === id);
1007
+ if (!row || row.status === "done" || row.status === "failed")
1008
+ return false;
1009
+ const patch = {
1010
+ id,
1011
+ at: new Date().toISOString(),
1012
+ t: "failed",
1013
+ code: "expired",
1014
+ message: "dismissed by operator",
1015
+ };
1016
+ mkdirSync(dirname(file), { recursive: true });
1017
+ appendFileSync(file, `${JSON.stringify(patch)}\n`);
1018
+ return true;
1019
+ }
1020
+ export function readSessionsPage(opts = {}, file = storePath(), processBootMs = Date.now() - process.uptime() * 1000) {
1021
+ const raw = Number(opts.limit);
1022
+ const limit = Number.isFinite(raw)
1023
+ ? Math.min(Math.max(1, Math.floor(raw)), MAX_PAGE_LIMIT)
1024
+ : DEFAULT_PAGE_LIMIT;
1025
+ let allRows;
1026
+ try {
1027
+ allRows = readSessionsIndexed(file);
1028
+ }
1029
+ catch (err) {
1030
+ log.error({ err }, "index refresh failed; full-fold fallback");
1031
+ allRows = readSessions(file);
1032
+ }
1033
+ const isStalled = (r) => r.status === "running" && Date.parse(r.updatedAt) < processBootMs;
1034
+ const stalledCount = allRows.reduce((n, r) => n + (isStalled(r) ? 1 : 0), 0);
1035
+ const byId = new Map(allRows.map((r) => [r.id, r]));
1036
+ const rootIdOf = (r) => {
1037
+ let cur = r;
1038
+ const seen = new Set([cur.id]);
1039
+ while (cur.parentSessionId && byId.has(cur.parentSessionId) && !seen.has(cur.parentSessionId)) {
1040
+ cur = byId.get(cur.parentSessionId);
1041
+ seen.add(cur.id);
1042
+ }
1043
+ return cur.id;
1044
+ };
1045
+ const parkedRoots = new Set();
1046
+ for (const r of allRows)
1047
+ if (isApprovalParked(r))
1048
+ parkedRoots.add(rootIdOf(r));
1049
+ const pendingApprovalCount = parkedRoots.size;
1050
+ if (opts.view === "approvals") {
1051
+ const wanted = new Set();
1052
+ for (const r of allRows) {
1053
+ if (!isApprovalParked(r))
1054
+ continue;
1055
+ let cur = r;
1056
+ const seen = new Set();
1057
+ while (cur && !seen.has(cur.id)) {
1058
+ wanted.add(cur.id);
1059
+ seen.add(cur.id);
1060
+ cur = cur.parentSessionId ? byId.get(cur.parentSessionId) : undefined;
1061
+ }
1062
+ }
1063
+ const sessions = allRows
1064
+ .filter((r) => wanted.has(r.id))
1065
+ .map((r) => ({ ...r, stalled: Date.parse(r.updatedAt) < processBootMs }));
1066
+ return { sessions, nextCursor: null, hasMore: false, stalledCount, pendingApprovalCount };
1067
+ }
1068
+ if (opts.root) {
1069
+ const wanted = collectTree(allRows, opts.root);
1070
+ const sessions = allRows
1071
+ .filter((r) => wanted.has(r.id))
1072
+ .map((r) => ({ ...r, stalled: isStalled(r) }));
1073
+ return { sessions, nextCursor: null, hasMore: false, stalledCount, pendingApprovalCount };
1074
+ }
1075
+ let rows = allRows;
1076
+ let flows;
1077
+ let groups;
1078
+ if (opts.view === "top") {
1079
+ const childrenByParent = new Map();
1080
+ for (const r of allRows) {
1081
+ if (r.parentSessionId != null) {
1082
+ const list = childrenByParent.get(r.parentSessionId) ?? [];
1083
+ list.push(r);
1084
+ childrenByParent.set(r.parentSessionId, list);
1085
+ }
1086
+ }
1087
+ rows = allRows
1088
+ .filter((r) => r.parentSessionId == null)
1089
+ .map((r) => {
1090
+ const kids = (childrenByParent.get(r.id) ?? []).slice().sort(compareSessionsDesc).reverse();
1091
+ return {
1092
+ ...r,
1093
+ childCount: kids.length,
1094
+ workflowFlow: kids.find((k) => k.flow != null)?.flow ?? r.flow ?? null,
1095
+ approvalKind: parkedRoots.has(r.id) ? "approval" : r.approvalKind,
1096
+ };
1097
+ });
1098
+ flows = {};
1099
+ for (const r of rows) {
1100
+ if (r.workflowFlow)
1101
+ flows[r.workflowFlow] = (flows[r.workflowFlow] ?? 0) + 1;
1102
+ }
1103
+ if (opts.scope === "chat")
1104
+ rows = rows.filter(isChatSession);
1105
+ if (opts.flow)
1106
+ rows = rows.filter((r) => r.workflowFlow === opts.flow);
1107
+ if (opts.groupBy && isSessionFacet(opts.groupBy)) {
1108
+ const facetOf = SESSION_FACETS[opts.groupBy];
1109
+ groups = {};
1110
+ for (const r of rows) {
1111
+ const v = facetOf(r);
1112
+ if (v)
1113
+ groups[v] = (groups[v] ?? 0) + 1;
1114
+ }
1115
+ if (opts.group)
1116
+ rows = rows.filter((r) => facetOf(r) === opts.group);
1117
+ }
1118
+ }
1119
+ const counts = {
1120
+ all: rows.length,
1121
+ active: 0,
1122
+ parked: 0,
1123
+ stalled: 0,
1124
+ failed: 0,
1125
+ prod: 0,
1126
+ dev: 0,
1127
+ };
1128
+ for (const r of rows) {
1129
+ if (isActiveSession(r))
1130
+ counts.active++;
1131
+ if (r.approvalKind === "approval" && r.status !== "done" && r.status !== "failed")
1132
+ counts.parked++;
1133
+ if (isStalled(r))
1134
+ counts.stalled++;
1135
+ if (r.status === "failed")
1136
+ counts.failed++;
1137
+ if (r.env === "prod")
1138
+ counts.prod++;
1139
+ else if (r.env === "dev")
1140
+ counts.dev++;
1141
+ }
1142
+ if (opts.status && opts.view === "top") {
1143
+ rows = rows.filter((r) => matchesStatusFilter(r, opts.status, isStalled));
1144
+ }
1145
+ let start = 0;
1146
+ if (opts.before) {
1147
+ const c = decodeCursor(opts.before);
1148
+ if (c) {
1149
+ const i = rows.findIndex((r) => r.startedAt < c.startedAt || (r.startedAt === c.startedAt && r.id < c.id));
1150
+ start = i < 0 ? rows.length : i;
1151
+ }
1152
+ }
1153
+ const LIST_PROMPT_CAP = 500;
1154
+ const trimForList = (r) => opts.view === "top"
1155
+ ? {
1156
+ ...r,
1157
+ actions: r.actions.map((a) => ({ tool: a.tool, status: a.status, at: a.at })),
1158
+ context: [],
1159
+ stepTimings: [],
1160
+ userMessages: [],
1161
+ prompt: r.prompt && r.prompt.length > LIST_PROMPT_CAP
1162
+ ? r.prompt.slice(0, LIST_PROMPT_CAP)
1163
+ : r.prompt,
1164
+ }
1165
+ : r;
1166
+ const sessions = rows
1167
+ .slice(start, start + limit)
1168
+ .map((r) => ({ ...trimForList(r), stalled: isStalled(r) }));
1169
+ const hasMore = start + limit < rows.length;
1170
+ const nextCursor = hasMore && sessions.length ? encodeCursor(sessions[sessions.length - 1]) : null;
1171
+ return {
1172
+ sessions,
1173
+ nextCursor,
1174
+ hasMore,
1175
+ stalledCount,
1176
+ pendingApprovalCount,
1177
+ counts,
1178
+ flows,
1179
+ groups,
1180
+ };
1181
+ }
1182
+ //# sourceMappingURL=recorder.js.map