sentinelayer-cli 0.34.6 → 0.35.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/package.json +1 -1
- package/src/commands/session.js +114 -17
- package/src/mcp/registry.js +56 -1
- package/src/mcp/session-stdio-server.js +205 -1
- package/src/scan/generator.js +19 -11
- package/src/session/coordination-guidance.js +1 -1
- package/src/session/first-message.js +1 -1
- package/src/session/listener-process.js +259 -0
- package/src/session/listener.js +3 -3
- package/src/session/listeners.js +8 -2
- package/src/session/pricing-ledger.js +208 -1
- package/src/session/recap.js +66 -2
- package/src/session/sync.js +28 -2
- package/src/session/transcript.js +27 -3
- package/src/session/usage-report.js +23 -4
- package/src/session/usage.js +2 -2
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
import { isProcessAlive } from "./daemon-spawn.js";
|
|
9
|
+
import { resolveSessionPaths } from "./paths.js";
|
|
10
|
+
|
|
11
|
+
const LISTENER_DIR_NAME = "listeners";
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
function normalizeString(value) {
|
|
15
|
+
return String(value || "").trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeListenerProcessKey(agentId = "") {
|
|
19
|
+
return (
|
|
20
|
+
normalizeString(agentId)
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
23
|
+
.replace(/^-+|-+$/g, "") || "agent"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeCommandLine(value) {
|
|
28
|
+
return normalizeString(value).toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listenerCommandLineMatches(commandLine, { sessionId = "", agentId = "" } = {}) {
|
|
32
|
+
const normalized = normalizeCommandLine(commandLine);
|
|
33
|
+
if (!normalized) return false;
|
|
34
|
+
const normalizedSessionId = normalizeString(sessionId).toLowerCase();
|
|
35
|
+
const normalizedAgentId = normalizeString(agentId).toLowerCase();
|
|
36
|
+
const hasCliEntry =
|
|
37
|
+
normalized.includes("sentinelayer-cli") ||
|
|
38
|
+
/[\\/](?:sl|sentinelayer-cli)\.js\b/.test(normalized);
|
|
39
|
+
return (
|
|
40
|
+
hasCliEntry &&
|
|
41
|
+
normalized.includes("session") &&
|
|
42
|
+
normalized.includes("listen") &&
|
|
43
|
+
(!normalizedSessionId || normalized.includes(normalizedSessionId)) &&
|
|
44
|
+
(!normalizedAgentId || normalized.includes(normalizedAgentId))
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readWindowsCommandLine(pid) {
|
|
49
|
+
const command = [
|
|
50
|
+
"$ErrorActionPreference = 'Stop';",
|
|
51
|
+
`$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${Number(pid)}";`,
|
|
52
|
+
"if ($p) { $p.CommandLine }",
|
|
53
|
+
].join(" ");
|
|
54
|
+
const { stdout } = await execFileAsync(
|
|
55
|
+
"powershell.exe",
|
|
56
|
+
["-NoProfile", "-NonInteractive", "-Command", command],
|
|
57
|
+
{ windowsHide: true, timeout: 2000 },
|
|
58
|
+
);
|
|
59
|
+
return normalizeString(stdout);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readPosixCommandLine(pid) {
|
|
63
|
+
if (process.platform === "linux") {
|
|
64
|
+
try {
|
|
65
|
+
const raw = await fsp.readFile(`/proc/${Number(pid)}/cmdline`);
|
|
66
|
+
return normalizeString(raw.toString("utf8").replace(/\0/g, " "));
|
|
67
|
+
} catch {
|
|
68
|
+
// Fall through to ps(1), which also covers restricted /proc mounts.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const { stdout } = await execFileAsync("ps", ["-p", String(Number(pid)), "-o", "command="], {
|
|
72
|
+
timeout: 2000,
|
|
73
|
+
});
|
|
74
|
+
return normalizeString(stdout);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function readProcessCommandLine(pid) {
|
|
78
|
+
const numericPid = Number(pid);
|
|
79
|
+
if (!Number.isInteger(numericPid) || numericPid <= 0) return "";
|
|
80
|
+
try {
|
|
81
|
+
return process.platform === "win32"
|
|
82
|
+
? await readWindowsCommandLine(numericPid)
|
|
83
|
+
: await readPosixCommandLine(numericPid);
|
|
84
|
+
} catch {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function resolveListenerPidPath(
|
|
90
|
+
sessionId,
|
|
91
|
+
agentId,
|
|
92
|
+
{ targetPath = process.cwd() } = {},
|
|
93
|
+
) {
|
|
94
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
95
|
+
return path.join(
|
|
96
|
+
paths.sessionDir,
|
|
97
|
+
LISTENER_DIR_NAME,
|
|
98
|
+
`${normalizeListenerProcessKey(agentId)}.json`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function readListenerPidRecord(
|
|
103
|
+
sessionId,
|
|
104
|
+
agentId,
|
|
105
|
+
{ targetPath = process.cwd() } = {},
|
|
106
|
+
) {
|
|
107
|
+
const pidPath = resolveListenerPidPath(sessionId, agentId, { targetPath });
|
|
108
|
+
try {
|
|
109
|
+
const raw = await fsp.readFile(pidPath, "utf-8");
|
|
110
|
+
const parsed = JSON.parse(raw);
|
|
111
|
+
if (!parsed || typeof parsed !== "object") {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return parsed;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function writeListenerPidRecord(
|
|
121
|
+
sessionId,
|
|
122
|
+
agentId,
|
|
123
|
+
{
|
|
124
|
+
targetPath = process.cwd(),
|
|
125
|
+
pid = process.pid,
|
|
126
|
+
listenerId = "",
|
|
127
|
+
transport = "",
|
|
128
|
+
intervalSeconds = null,
|
|
129
|
+
activeIntervalSeconds = null,
|
|
130
|
+
presenceIntervalSeconds = null,
|
|
131
|
+
logFile = "",
|
|
132
|
+
} = {},
|
|
133
|
+
) {
|
|
134
|
+
const pidPath = resolveListenerPidPath(sessionId, agentId, { targetPath });
|
|
135
|
+
const record = {
|
|
136
|
+
pid: Number(pid),
|
|
137
|
+
sessionId: normalizeString(sessionId),
|
|
138
|
+
agentId: normalizeString(agentId),
|
|
139
|
+
listenerKey: normalizeListenerProcessKey(agentId),
|
|
140
|
+
listenerId: normalizeString(listenerId),
|
|
141
|
+
targetPath: path.resolve(String(targetPath || ".")),
|
|
142
|
+
transport: normalizeString(transport) || undefined,
|
|
143
|
+
intervalSeconds: Number(intervalSeconds) || undefined,
|
|
144
|
+
activeIntervalSeconds: Number(activeIntervalSeconds) || undefined,
|
|
145
|
+
presenceIntervalSeconds: Number(presenceIntervalSeconds) || undefined,
|
|
146
|
+
logFile: normalizeString(logFile) || undefined,
|
|
147
|
+
startedAt: new Date().toISOString(),
|
|
148
|
+
};
|
|
149
|
+
await fsp.mkdir(path.dirname(pidPath), { recursive: true });
|
|
150
|
+
await fsp.writeFile(pidPath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
|
|
151
|
+
return record;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function removeListenerPidRecord(
|
|
155
|
+
sessionId,
|
|
156
|
+
agentId,
|
|
157
|
+
{ targetPath = process.cwd(), onlyForPid = null } = {},
|
|
158
|
+
) {
|
|
159
|
+
const pidPath = resolveListenerPidPath(sessionId, agentId, { targetPath });
|
|
160
|
+
if (onlyForPid != null) {
|
|
161
|
+
const existing = await readListenerPidRecord(sessionId, agentId, { targetPath });
|
|
162
|
+
if (existing && Number(existing.pid) !== Number(onlyForPid)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
await fsp.unlink(pidPath);
|
|
168
|
+
return true;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function getListenerProcessStatus(
|
|
175
|
+
sessionId,
|
|
176
|
+
agentId,
|
|
177
|
+
{ targetPath = process.cwd(), _readProcessCommandLine = readProcessCommandLine } = {},
|
|
178
|
+
) {
|
|
179
|
+
const record = await readListenerPidRecord(sessionId, agentId, { targetPath });
|
|
180
|
+
if (!record) {
|
|
181
|
+
return {
|
|
182
|
+
running: false,
|
|
183
|
+
pid: null,
|
|
184
|
+
stale: false,
|
|
185
|
+
record: null,
|
|
186
|
+
listenerKey: normalizeListenerProcessKey(agentId),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const alive = isProcessAlive(record.pid);
|
|
190
|
+
if (!alive) {
|
|
191
|
+
return {
|
|
192
|
+
running: false,
|
|
193
|
+
pid: null,
|
|
194
|
+
stale: true,
|
|
195
|
+
record,
|
|
196
|
+
listenerKey: normalizeListenerProcessKey(agentId),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const commandLine = await _readProcessCommandLine(record.pid);
|
|
200
|
+
if (!listenerCommandLineMatches(commandLine, { sessionId, agentId })) {
|
|
201
|
+
return {
|
|
202
|
+
running: false,
|
|
203
|
+
pid: null,
|
|
204
|
+
stale: true,
|
|
205
|
+
reused: Boolean(commandLine),
|
|
206
|
+
unverified: !commandLine,
|
|
207
|
+
record,
|
|
208
|
+
listenerKey: normalizeListenerProcessKey(agentId),
|
|
209
|
+
commandLine,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
running: true,
|
|
214
|
+
pid: Number(record.pid),
|
|
215
|
+
stale: false,
|
|
216
|
+
record,
|
|
217
|
+
listenerKey: normalizeListenerProcessKey(agentId),
|
|
218
|
+
commandLine,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function requestListenerProcessStop(
|
|
223
|
+
pid,
|
|
224
|
+
{ signal = "SIGTERM", timeoutMs = 3000, pollIntervalMs = 100 } = {},
|
|
225
|
+
) {
|
|
226
|
+
const numericPid = Number(pid);
|
|
227
|
+
if (!Number.isInteger(numericPid) || numericPid <= 0) {
|
|
228
|
+
return { requested: false, stopped: true, reason: "invalid_pid" };
|
|
229
|
+
}
|
|
230
|
+
if (numericPid === process.pid) {
|
|
231
|
+
return { requested: false, stopped: false, reason: "self" };
|
|
232
|
+
}
|
|
233
|
+
if (!isProcessAlive(numericPid)) {
|
|
234
|
+
return { requested: false, stopped: true, reason: "not_running" };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
process.kill(numericPid, signal);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error?.code === "ESRCH") {
|
|
241
|
+
return { requested: false, stopped: true, reason: "not_running" };
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
requested: false,
|
|
245
|
+
stopped: false,
|
|
246
|
+
reason: normalizeString(error?.code) || "signal_failed",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const deadlineMs = Date.now() + Math.max(1, Number(timeoutMs) || 3000);
|
|
251
|
+
const sleepMs = Math.max(10, Number(pollIntervalMs) || 100);
|
|
252
|
+
while (Date.now() < deadlineMs) {
|
|
253
|
+
if (!isProcessAlive(numericPid)) {
|
|
254
|
+
return { requested: true, stopped: true, reason: "stopped" };
|
|
255
|
+
}
|
|
256
|
+
await sleep(sleepMs);
|
|
257
|
+
}
|
|
258
|
+
return { requested: true, stopped: false, reason: "timeout" };
|
|
259
|
+
}
|
package/src/session/listener.js
CHANGED
|
@@ -305,6 +305,8 @@ export async function listenSessionEvents({
|
|
|
305
305
|
const activeWindowMs =
|
|
306
306
|
Math.max(1, normalizePositiveInteger(activeWindowSeconds, DEFAULT_ACTIVE_WINDOW_SECONDS)) *
|
|
307
307
|
1000;
|
|
308
|
+
const autoStreamIdleTimeoutMs =
|
|
309
|
+
normalizedTransport === "auto" ? Math.max(5_000, Math.min(idleSleepMs, 30_000)) : 0;
|
|
308
310
|
|
|
309
311
|
if (fromNow) {
|
|
310
312
|
const latest = await _pollLatest(normalizedSessionId, {
|
|
@@ -507,9 +509,6 @@ export async function listenSessionEvents({
|
|
|
507
509
|
streamHeartbeatInFlight = false;
|
|
508
510
|
}
|
|
509
511
|
}, streamHeartbeatMs);
|
|
510
|
-
if (streamHeartbeatTimer && typeof streamHeartbeatTimer.unref === "function") {
|
|
511
|
-
streamHeartbeatTimer.unref();
|
|
512
|
-
}
|
|
513
512
|
}
|
|
514
513
|
|
|
515
514
|
await notifyLifecycle(
|
|
@@ -527,6 +526,7 @@ export async function listenSessionEvents({
|
|
|
527
526
|
const streamResult = await _stream(normalizedSessionId, {
|
|
528
527
|
targetPath,
|
|
529
528
|
since: cursor,
|
|
529
|
+
idleTimeoutMs: autoStreamIdleTimeoutMs,
|
|
530
530
|
signal,
|
|
531
531
|
onEvent: async (event) => {
|
|
532
532
|
await processEventBatch([event], normalizeString(event?.cursor) || cursor);
|
package/src/session/listeners.js
CHANGED
|
@@ -147,9 +147,15 @@ export function summarizeListeners(events = [], { nowMs = Date.now(), staleAfter
|
|
|
147
147
|
*/
|
|
148
148
|
export async function fetchSessionListeners(
|
|
149
149
|
sessionId,
|
|
150
|
-
{
|
|
150
|
+
{
|
|
151
|
+
targetPath = process.cwd(),
|
|
152
|
+
limit = 200,
|
|
153
|
+
nowMs = Date.now,
|
|
154
|
+
forceCircuitProbe = true,
|
|
155
|
+
poll = pollSessionEventsBefore,
|
|
156
|
+
} = {}
|
|
151
157
|
) {
|
|
152
|
-
const result = await poll(sessionId, { targetPath, limit });
|
|
158
|
+
const result = await poll(sessionId, { targetPath, limit, forceCircuitProbe });
|
|
153
159
|
if (!result?.ok) {
|
|
154
160
|
return { ok: false, reason: normalizeString(result?.reason) || "fetch_failed", listeners: [] };
|
|
155
161
|
}
|
|
@@ -4,10 +4,12 @@ import {
|
|
|
4
4
|
DEFAULT_PRICE_BOOK_VERSION,
|
|
5
5
|
estimateModelCost,
|
|
6
6
|
} from "../cost/tracker.js";
|
|
7
|
+
import { estimateTokens } from "../cost/tokenizer.js";
|
|
7
8
|
|
|
8
9
|
const SESSION_USAGE_EVENT = "session_usage";
|
|
9
10
|
export const BILLING_SESSION_USAGE_SCHEMA = "billing/v1";
|
|
10
11
|
export const LOCAL_SESSION_USAGE_SCHEMA = "session_usage/local-v1";
|
|
12
|
+
export const ESTIMATED_MESSAGE_USAGE_SCHEMA = "session_usage/estimated-message-v1";
|
|
11
13
|
const LEGACY_SESSION_USAGE_SCHEMAS = new Set(["", "session_usage/v0"]);
|
|
12
14
|
const SUPPORTED_SESSION_USAGE_SCHEMAS = new Set([
|
|
13
15
|
BILLING_SESSION_USAGE_SCHEMA,
|
|
@@ -38,6 +40,19 @@ const USAGE_HINT_KEYS = [
|
|
|
38
40
|
"cost_usd",
|
|
39
41
|
"cost",
|
|
40
42
|
];
|
|
43
|
+
const ESTIMATED_MESSAGE_EVENT_KINDS = new Set([
|
|
44
|
+
"agent_response",
|
|
45
|
+
"session_message",
|
|
46
|
+
"session_say",
|
|
47
|
+
]);
|
|
48
|
+
const HUMAN_AGENT_IDS = new Set([
|
|
49
|
+
"cli-user",
|
|
50
|
+
"human",
|
|
51
|
+
"user",
|
|
52
|
+
"you",
|
|
53
|
+
]);
|
|
54
|
+
const DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_WINDOW_MS = 5 * 60 * 1000;
|
|
55
|
+
const DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_SEQUENCE_WINDOW = 3;
|
|
41
56
|
|
|
42
57
|
function n(value) {
|
|
43
58
|
return String(value == null ? "" : value).trim();
|
|
@@ -94,6 +109,12 @@ function roundUsd(value) {
|
|
|
94
109
|
return Math.round(Number(value || 0) * 1_000_000) / 1_000_000;
|
|
95
110
|
}
|
|
96
111
|
|
|
112
|
+
function positiveInteger(value, fallbackValue) {
|
|
113
|
+
const parsed = Number(value);
|
|
114
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallbackValue;
|
|
115
|
+
return Math.floor(parsed);
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
export function createSessionUsageLedgerId({
|
|
98
119
|
sessionId = "",
|
|
99
120
|
agentId = "",
|
|
@@ -116,6 +137,52 @@ function fallbackIdempotencyKey({ sessionId, event, agentId, action, model, tota
|
|
|
116
137
|
return `event:${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
|
|
117
138
|
}
|
|
118
139
|
|
|
140
|
+
function messageEstimateIdempotencyKey({ sessionId, event, agentId, model, text }) {
|
|
141
|
+
const payload = object(event?.payload);
|
|
142
|
+
const explicit =
|
|
143
|
+
n(payload.clientMessageId || payload.client_message_id) ||
|
|
144
|
+
n(event?.eventId || event?.event_id) ||
|
|
145
|
+
n(event?.idempotencyToken || event?.idempotency_token) ||
|
|
146
|
+
n(event?.cursor);
|
|
147
|
+
if (explicit) return `estimated-message:${explicit}`;
|
|
148
|
+
const sequence = n(event?.sequenceId ?? event?.sequence_id);
|
|
149
|
+
if (sequence) return `estimated-message:seq:${sequence}`;
|
|
150
|
+
const timestamp = n(event?.ts || event?.timestamp);
|
|
151
|
+
const source = [sessionId, timestamp, agentId, model, text].join("\x1f");
|
|
152
|
+
return `estimated-message:${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isLikelyHumanAgent({ agentId = "", role = "", model = "" } = {}) {
|
|
156
|
+
const id = n(agentId).toLowerCase();
|
|
157
|
+
const normalizedRole = n(role).toLowerCase();
|
|
158
|
+
const normalizedModel = n(model).toLowerCase();
|
|
159
|
+
if (!id && !normalizedModel) return true;
|
|
160
|
+
if (HUMAN_AGENT_IDS.has(id)) return true;
|
|
161
|
+
if (id.startsWith("human-") || id.startsWith("user-")) return true;
|
|
162
|
+
if (normalizedModel === "human") return true;
|
|
163
|
+
const agentLikeRole = ["coder", "reviewer", "tester", "observer", "orchestrator"].includes(normalizedRole);
|
|
164
|
+
const agentLikeId = /(codex|claude|gpt|gemini|grok|senti|kai-chen|agent|bot|warden|architect|builder)/i.test(id);
|
|
165
|
+
const agentLikeModel = /(gpt|claude|gemini|grok|codex|sonnet|opus|haiku)/i.test(normalizedModel);
|
|
166
|
+
if (normalizedRole === "human") return true;
|
|
167
|
+
if (normalizedRole === "participant" && !agentLikeId && !agentLikeModel) return true;
|
|
168
|
+
if (!agentLikeRole && !agentLikeId && !agentLikeModel && (!normalizedModel || normalizedModel === "unknown")) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function responseText(payload = {}) {
|
|
175
|
+
if (payload.response && typeof payload.response === "object" && !Array.isArray(payload.response)) {
|
|
176
|
+
return n(payload.response.text);
|
|
177
|
+
}
|
|
178
|
+
return n(payload.response);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function messageText(event = {}) {
|
|
182
|
+
const payload = object(event?.payload);
|
|
183
|
+
return n(payload.message) || responseText(payload) || n(payload.text);
|
|
184
|
+
}
|
|
185
|
+
|
|
119
186
|
function providerCostFromPriceBook({ model, inputTokens, outputTokens, explicitProviderCost }) {
|
|
120
187
|
if (explicitProviderCost != null) {
|
|
121
188
|
return { providerCostUsd: explicitProviderCost, unpriced: false };
|
|
@@ -217,9 +284,139 @@ export function buildUsageLedgerEntry(
|
|
|
217
284
|
unpriced,
|
|
218
285
|
timestamp: n(event?.ts || event?.timestamp),
|
|
219
286
|
sequenceId: nonNegativeInt(event?.sequenceId ?? event?.sequence_id),
|
|
287
|
+
estimated: false,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function buildEstimatedMessageLedgerEntry(
|
|
292
|
+
event,
|
|
293
|
+
{
|
|
294
|
+
sessionId = "",
|
|
295
|
+
priceBookVersion = DEFAULT_PRICE_BOOK_VERSION,
|
|
296
|
+
billingTier = "estimated",
|
|
297
|
+
includeHumanMessages = false,
|
|
298
|
+
action = "estimated_agent_message",
|
|
299
|
+
} = {},
|
|
300
|
+
) {
|
|
301
|
+
const kind = n(event?.event || event?.type);
|
|
302
|
+
if (!ESTIMATED_MESSAGE_EVENT_KINDS.has(kind)) return null;
|
|
303
|
+
|
|
304
|
+
const payload = object(event?.payload);
|
|
305
|
+
if (hasUsageHints(payload) || hasUsageHints(payload.usage)) return null;
|
|
306
|
+
|
|
307
|
+
const text = messageText(event);
|
|
308
|
+
if (!text) return null;
|
|
309
|
+
|
|
310
|
+
const agent = object(event?.agent);
|
|
311
|
+
const agentId = n(agent.id || event?.agentId) || "unknown";
|
|
312
|
+
const model = n(agent.model || event?.agentModel || payload.model) || "unknown";
|
|
313
|
+
const role = n(agent.role || payload.role);
|
|
314
|
+
const lowerAgentId = agentId.toLowerCase();
|
|
315
|
+
if (lowerAgentId === "senti" || lowerAgentId === "kai-chen") {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
if (!includeHumanMessages && isLikelyHumanAgent({ agentId, role, model })) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const outputTokens = estimateTokens(text, {
|
|
323
|
+
model,
|
|
324
|
+
provider: n(agent.provider || payload.provider),
|
|
325
|
+
});
|
|
326
|
+
if (outputTokens <= 0) return null;
|
|
327
|
+
|
|
328
|
+
const actionName = n(action) || "estimated_agent_message";
|
|
329
|
+
const idempotencyKey = messageEstimateIdempotencyKey({
|
|
330
|
+
sessionId,
|
|
331
|
+
event,
|
|
332
|
+
agentId,
|
|
333
|
+
model,
|
|
334
|
+
text,
|
|
335
|
+
});
|
|
336
|
+
const ledgerEntryId = createSessionUsageLedgerId({
|
|
337
|
+
sessionId,
|
|
338
|
+
agentId,
|
|
339
|
+
action: actionName,
|
|
340
|
+
idempotencyKey,
|
|
341
|
+
});
|
|
342
|
+
const { providerCostUsd, unpriced } = providerCostFromPriceBook({
|
|
343
|
+
model,
|
|
344
|
+
inputTokens: 0,
|
|
345
|
+
outputTokens,
|
|
346
|
+
explicitProviderCost: null,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
ledgerEntryId,
|
|
351
|
+
idempotencyKey,
|
|
352
|
+
schema: ESTIMATED_MESSAGE_USAGE_SCHEMA,
|
|
353
|
+
sessionId: n(sessionId),
|
|
354
|
+
agentId,
|
|
355
|
+
action: actionName,
|
|
356
|
+
model,
|
|
357
|
+
priceBookVersion: n(priceBookVersion) || DEFAULT_PRICE_BOOK_VERSION,
|
|
358
|
+
billingTier: n(billingTier) || "estimated",
|
|
359
|
+
provider: n(agent.provider || payload.provider),
|
|
360
|
+
inputTokens: 0,
|
|
361
|
+
outputTokens,
|
|
362
|
+
totalTokens: outputTokens,
|
|
363
|
+
providerCostUsd: roundUsd(providerCostUsd),
|
|
364
|
+
customerCostUsd: null,
|
|
365
|
+
unpriced,
|
|
366
|
+
timestamp: n(event?.ts || event?.timestamp),
|
|
367
|
+
sequenceId: nonNegativeInt(event?.sequenceId ?? event?.sequence_id),
|
|
368
|
+
estimated: true,
|
|
220
369
|
};
|
|
221
370
|
}
|
|
222
371
|
|
|
372
|
+
function collectRealUsageAnchors(events = [], options = {}) {
|
|
373
|
+
const anchors = new Map();
|
|
374
|
+
for (const event of events) {
|
|
375
|
+
const entry = buildUsageLedgerEntry(event, options);
|
|
376
|
+
if (!entry || entry.estimated) continue;
|
|
377
|
+
if (!anchors.has(entry.agentId)) anchors.set(entry.agentId, []);
|
|
378
|
+
anchors.get(entry.agentId).push({
|
|
379
|
+
timestampMs: Date.parse(n(entry.timestamp)),
|
|
380
|
+
sequenceId: Number.isFinite(entry.sequenceId) ? entry.sequenceId : null,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return anchors;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function hasNearbyRealUsageAnchor(entry, anchors, options = {}) {
|
|
387
|
+
if (!entry || !entry.estimated || !anchors || anchors.size === 0) return false;
|
|
388
|
+
const agentAnchors = anchors.get(entry.agentId);
|
|
389
|
+
if (!agentAnchors || agentAnchors.length === 0) return false;
|
|
390
|
+
const windowMs = positiveInteger(
|
|
391
|
+
options.estimatedMessageUsageDedupeWindowMs,
|
|
392
|
+
DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_WINDOW_MS,
|
|
393
|
+
);
|
|
394
|
+
const sequenceWindow = positiveInteger(
|
|
395
|
+
options.estimatedMessageUsageDedupeSequenceWindow,
|
|
396
|
+
DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_SEQUENCE_WINDOW,
|
|
397
|
+
);
|
|
398
|
+
const entryTimestampMs = Date.parse(n(entry.timestamp));
|
|
399
|
+
const hasEntryTimestamp = Number.isFinite(entryTimestampMs);
|
|
400
|
+
const entrySequenceId = Number.isFinite(entry.sequenceId) ? entry.sequenceId : null;
|
|
401
|
+
return agentAnchors.some((anchor) => {
|
|
402
|
+
if (
|
|
403
|
+
entrySequenceId !== null &&
|
|
404
|
+
Number.isFinite(anchor.sequenceId) &&
|
|
405
|
+
Math.abs(entrySequenceId - anchor.sequenceId) <= sequenceWindow
|
|
406
|
+
) {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
if (
|
|
410
|
+
hasEntryTimestamp &&
|
|
411
|
+
Number.isFinite(anchor.timestampMs) &&
|
|
412
|
+
Math.abs(entryTimestampMs - anchor.timestampMs) <= windowMs
|
|
413
|
+
) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
223
420
|
function newRollup(label) {
|
|
224
421
|
return {
|
|
225
422
|
label,
|
|
@@ -231,6 +428,7 @@ function newRollup(label) {
|
|
|
231
428
|
customerCostUsd: 0,
|
|
232
429
|
hasCustomerCost: false,
|
|
233
430
|
unpriced: 0,
|
|
431
|
+
estimatedEntries: 0,
|
|
234
432
|
};
|
|
235
433
|
}
|
|
236
434
|
|
|
@@ -245,6 +443,7 @@ function addToRollup(rollup, entry) {
|
|
|
245
443
|
rollup.hasCustomerCost = true;
|
|
246
444
|
}
|
|
247
445
|
if (entry.unpriced) rollup.unpriced += 1;
|
|
446
|
+
if (entry.estimated) rollup.estimatedEntries += 1;
|
|
248
447
|
}
|
|
249
448
|
|
|
250
449
|
function finalizeRollup(rollup) {
|
|
@@ -257,6 +456,9 @@ export function buildSessionUsageLedger(events = [], options = {}) {
|
|
|
257
456
|
if (!Array.isArray(events)) {
|
|
258
457
|
throw new Error("events must be an array.");
|
|
259
458
|
}
|
|
459
|
+
const realUsageAnchors = options.includeEstimatedMessages
|
|
460
|
+
? collectRealUsageAnchors(events, options)
|
|
461
|
+
: new Map();
|
|
260
462
|
const entries = [];
|
|
261
463
|
const totals = newRollup("session");
|
|
262
464
|
const perAgent = new Map();
|
|
@@ -266,8 +468,13 @@ export function buildSessionUsageLedger(events = [], options = {}) {
|
|
|
266
468
|
let duplicatesSkipped = 0;
|
|
267
469
|
|
|
268
470
|
for (const event of events) {
|
|
269
|
-
const entry =
|
|
471
|
+
const entry =
|
|
472
|
+
buildUsageLedgerEntry(event, options) ||
|
|
473
|
+
(options.includeEstimatedMessages
|
|
474
|
+
? buildEstimatedMessageLedgerEntry(event, options)
|
|
475
|
+
: null);
|
|
270
476
|
if (!entry) continue;
|
|
477
|
+
if (hasNearbyRealUsageAnchor(entry, realUsageAnchors, options)) continue;
|
|
271
478
|
const dedupeKeys = [
|
|
272
479
|
entry.idempotencyKey ? `idem:${entry.idempotencyKey}` : "",
|
|
273
480
|
entry.ledgerEntryId ? `ledger:${entry.ledgerEntryId}` : "",
|
package/src/session/recap.js
CHANGED
|
@@ -22,6 +22,23 @@ const DEFAULT_TASK_SUMMARY_LIMIT = 3;
|
|
|
22
22
|
const DEFAULT_WORK_PLAN_SUMMARY_LIMIT = 5;
|
|
23
23
|
const MAX_WORK_PLAN_BYTES = 128_000;
|
|
24
24
|
const WORK_PLAN_RELATIVE_PATH = "tasks/todo.md";
|
|
25
|
+
const HISTORICAL_WORK_PLAN_MIN_COMPLETED = 25;
|
|
26
|
+
const HISTORICAL_WORK_PLAN_MIN_TOTAL = 50;
|
|
27
|
+
const HISTORICAL_WORK_PLAN_COMPLETED_RATIO = 0.75;
|
|
28
|
+
const GENERIC_WORK_PLAN_SECTIONS = new Set([
|
|
29
|
+
"plan",
|
|
30
|
+
"plans",
|
|
31
|
+
"todo",
|
|
32
|
+
"todos",
|
|
33
|
+
"to do",
|
|
34
|
+
"tasks",
|
|
35
|
+
"work items",
|
|
36
|
+
"workitems",
|
|
37
|
+
"workstream",
|
|
38
|
+
"workstreams",
|
|
39
|
+
"backlog",
|
|
40
|
+
"next",
|
|
41
|
+
]);
|
|
25
42
|
const RECAP_SOURCE_IGNORED_EVENTS = new Set([
|
|
26
43
|
"agent_heartbeat",
|
|
27
44
|
"agent_join",
|
|
@@ -458,6 +475,41 @@ function shortWorkPlanText(value) {
|
|
|
458
475
|
return `${text.slice(0, 97)}...`;
|
|
459
476
|
}
|
|
460
477
|
|
|
478
|
+
function normalizedWorkPlanSection(value = "") {
|
|
479
|
+
return normalizeString(value)
|
|
480
|
+
.toLowerCase()
|
|
481
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
482
|
+
.replace(/\s+/g, " ")
|
|
483
|
+
.trim();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function isGenericWorkPlanSection(value = "") {
|
|
487
|
+
const normalized = normalizedWorkPlanSection(value);
|
|
488
|
+
if (!normalized) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
if (GENERIC_WORK_PLAN_SECTIONS.has(normalized)) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
return /^(active\s+)?(plan|todo|tasks|backlog)(\s+\d+)?$/.test(normalized);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function shouldSuppressHistoricalWorkPlanDetails(summary = emptyWorkPlanSummary()) {
|
|
498
|
+
const total = Number(summary.total || 0);
|
|
499
|
+
const open = Number(summary.open || 0);
|
|
500
|
+
const completed = Number(summary.completed || 0);
|
|
501
|
+
if (summary.truncated || open <= 0 || completed < HISTORICAL_WORK_PLAN_MIN_COMPLETED) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
if (total < HISTORICAL_WORK_PLAN_MIN_TOTAL) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
if (!isGenericWorkPlanSection(summary.currentSection)) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
return completed / Math.max(1, total) >= HISTORICAL_WORK_PLAN_COMPLETED_RATIO;
|
|
511
|
+
}
|
|
512
|
+
|
|
461
513
|
function summarizeWorkPlanMarkdown(
|
|
462
514
|
raw = "",
|
|
463
515
|
{ limit = DEFAULT_WORK_PLAN_SUMMARY_LIMIT, truncated = false, sourceContext = {} } = {}
|
|
@@ -507,6 +559,10 @@ function summarizeWorkPlanMarkdown(
|
|
|
507
559
|
.filter((record) => record.status === "open")
|
|
508
560
|
.slice(-normalizedLimit);
|
|
509
561
|
summary.recent = records.slice(-normalizedLimit);
|
|
562
|
+
if (!summary.detailSuppressed && shouldSuppressHistoricalWorkPlanDetails(summary)) {
|
|
563
|
+
summary.detailSuppressed = true;
|
|
564
|
+
summary.detailSuppressionReason = "historical_generic_plan_section";
|
|
565
|
+
}
|
|
510
566
|
if (summary.detailSuppressed) {
|
|
511
567
|
summary.currentSection = "";
|
|
512
568
|
summary.recentOpen = [];
|
|
@@ -785,7 +841,15 @@ function buildWorkPlanText(workPlan = emptyWorkPlanSummary()) {
|
|
|
785
841
|
const sourceReason = normalizeString(workPlan.sourceReason);
|
|
786
842
|
const sourceReasonText = sourceReason ? ` (${sourceReason})` : "";
|
|
787
843
|
if (workPlan.detailSuppressed || workPlan.truncated) {
|
|
788
|
-
|
|
844
|
+
const suppressionReason = normalizeString(workPlan.detailSuppressionReason);
|
|
845
|
+
const sourceWindowText = workPlan.truncated
|
|
846
|
+
? `recent ${sourceText} window`
|
|
847
|
+
: sourceText;
|
|
848
|
+
const suppressionText =
|
|
849
|
+
suppressionReason === "historical_generic_plan_section"
|
|
850
|
+
? "Current/next items suppressed because this generic plan is mostly historical completed work."
|
|
851
|
+
: "Current/next items suppressed because the plan file is large.";
|
|
852
|
+
return `Plan: ${open} open / ${completed} done in ${sourceWindowText}${sourceReasonText}. ${suppressionText}`;
|
|
789
853
|
}
|
|
790
854
|
const currentSection = normalizeString(workPlan.currentSection);
|
|
791
855
|
const currentText = currentSection ? ` Current: ${currentSection}.` : "";
|
|
@@ -814,7 +878,7 @@ function roundCurrency(value) {
|
|
|
814
878
|
}
|
|
815
879
|
|
|
816
880
|
function normalizeUsageSummary(events = []) {
|
|
817
|
-
const aggregate = aggregateSessionUsage(events);
|
|
881
|
+
const aggregate = aggregateSessionUsage(events, { includeEstimatedMessages: true });
|
|
818
882
|
const totals = {
|
|
819
883
|
totalTokens: Number(aggregate.totals.totalTokens || 0),
|
|
820
884
|
inputTokens: Number(aggregate.totals.inputTokens || 0),
|