sentinelayer-cli 0.17.0 → 0.18.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/README.md +16 -6
- package/package.json +3 -2
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/omargate.js +1 -0
- package/src/commands/session.js +302 -25
- package/src/cost/tracker.js +3 -1
- package/src/events/schema.js +21 -0
- package/src/legacy-cli.js +24 -1
- package/src/review/investor-dd-devtestbot.js +83 -8
- package/src/review/investor-dd-file-loop.js +83 -6
- package/src/review/investor-dd-orchestrator.js +42 -1
- package/src/review/investor-dd-progress.js +351 -0
- package/src/review/investor-dd-usage.js +227 -0
- package/src/scan/generator.js +8 -1
- package/src/session/daemon.js +341 -2
- package/src/session/pricing-ledger.js +260 -0
- package/src/session/recap.js +288 -69
- package/src/session/sync.js +1 -4
- package/src/session/usage.js +72 -50
package/src/session/recap.js
CHANGED
|
@@ -17,6 +17,19 @@ const DEFAULT_RECAP_INTERVAL_MS = 300_000;
|
|
|
17
17
|
const DEFAULT_RECAP_INACTIVITY_MS = 600_000;
|
|
18
18
|
const DEFAULT_RECAP_ACTIVITY_THRESHOLD = 5;
|
|
19
19
|
const DEFAULT_TASK_SUMMARY_LIMIT = 3;
|
|
20
|
+
const RECAP_SOURCE_IGNORED_EVENTS = new Set([
|
|
21
|
+
"agent_heartbeat",
|
|
22
|
+
"agent_join",
|
|
23
|
+
"agent_status",
|
|
24
|
+
"context_briefing",
|
|
25
|
+
"daemon_alert",
|
|
26
|
+
"session_ack",
|
|
27
|
+
"session_checkpoint",
|
|
28
|
+
"session_reaction",
|
|
29
|
+
"session_recap",
|
|
30
|
+
"session_usage",
|
|
31
|
+
"session_view",
|
|
32
|
+
]);
|
|
20
33
|
const ACTIVE_TASK_STATUSES = new Set(["PENDING", "ACCEPTED", "BLOCKED"]);
|
|
21
34
|
const TASK_STATUS_KEYS = {
|
|
22
35
|
PENDING: "pending",
|
|
@@ -64,6 +77,184 @@ function isRecapEvent(event = {}) {
|
|
|
64
77
|
return payload.ephemeral === true && normalizeString(payload.style) === RECAP_STYLE;
|
|
65
78
|
}
|
|
66
79
|
|
|
80
|
+
function eventSequenceId(event = {}) {
|
|
81
|
+
const parsed = Number(event.sequenceId ?? event.sequence_id);
|
|
82
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return Math.floor(parsed);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function eventCursor(event = {}) {
|
|
89
|
+
return normalizeString(event.cursor || event.eventId || event.idempotencyToken || event.ts);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isMeaningfulRecapSourceEvent(event = {}) {
|
|
93
|
+
if (isRecapEvent(event)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const eventName = normalizeString(event.event).toLowerCase();
|
|
97
|
+
if (!eventName || RECAP_SOURCE_IGNORED_EVENTS.has(eventName)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isEventAfterRecapAnchor(event = {}, state = {}) {
|
|
104
|
+
const anchorSequence = Number(state.lastSourceSequenceId);
|
|
105
|
+
const sequence = eventSequenceId(event);
|
|
106
|
+
if (Number.isFinite(anchorSequence) && anchorSequence > 0 && sequence !== null) {
|
|
107
|
+
return sequence > anchorSequence;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const anchorAt = normalizeString(state.lastSourceEventAt);
|
|
111
|
+
if (!anchorAt) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
const anchorEpoch = toEpoch(anchorAt, anchorAt);
|
|
115
|
+
const eventEpoch = toEpoch(event.ts, anchorAt);
|
|
116
|
+
if (eventEpoch > anchorEpoch) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (eventEpoch < anchorEpoch) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const anchorCursor = normalizeString(state.lastSourceCursor);
|
|
124
|
+
if (!anchorCursor) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return eventCursor(event) !== anchorCursor;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function rememberRecapSource(state, event = {}, nowIso = new Date().toISOString()) {
|
|
131
|
+
state.lastSourceEventAt = normalizeIsoTimestamp(event.ts, nowIso);
|
|
132
|
+
state.lastSourceSequenceId = eventSequenceId(event);
|
|
133
|
+
state.lastSourceCursor = eventCursor(event);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolvePeriodicRecapTrigger(state, events = [], nowIso = new Date().toISOString()) {
|
|
137
|
+
const sortedEvents = sortEventsByConversationTime(events, nowIso);
|
|
138
|
+
const sourceEvents = sortedEvents.filter(isMeaningfulRecapSourceEvent);
|
|
139
|
+
const unrecappedSourceEvents = sourceEvents.filter((event) => isEventAfterRecapAnchor(event, state));
|
|
140
|
+
const latestSourceEvent = sourceEvents.length > 0 ? sourceEvents[sourceEvents.length - 1] : null;
|
|
141
|
+
const latestUnrecappedSourceEvent =
|
|
142
|
+
unrecappedSourceEvents.length > 0
|
|
143
|
+
? unrecappedSourceEvents[unrecappedSourceEvents.length - 1]
|
|
144
|
+
: null;
|
|
145
|
+
const nowEpoch = toEpoch(nowIso, nowIso);
|
|
146
|
+
const latestSourceEventAt = latestSourceEvent
|
|
147
|
+
? normalizeIsoTimestamp(latestSourceEvent.ts, nowIso)
|
|
148
|
+
: null;
|
|
149
|
+
const latestUnrecappedSourceEventAt = latestUnrecappedSourceEvent
|
|
150
|
+
? normalizeIsoTimestamp(latestUnrecappedSourceEvent.ts, nowIso)
|
|
151
|
+
: null;
|
|
152
|
+
const sourceIdleMs = latestSourceEvent
|
|
153
|
+
? Math.max(0, nowEpoch - toEpoch(latestSourceEvent.ts, nowIso))
|
|
154
|
+
: null;
|
|
155
|
+
const unrecappedSourceIdleMs = latestUnrecappedSourceEvent
|
|
156
|
+
? Math.max(0, nowEpoch - toEpoch(latestUnrecappedSourceEvent.ts, nowIso))
|
|
157
|
+
: null;
|
|
158
|
+
const sinceLastRecapMs = state.lastRecapAt
|
|
159
|
+
? Math.max(0, nowEpoch - toEpoch(state.lastRecapAt, nowIso))
|
|
160
|
+
: null;
|
|
161
|
+
const policy = {
|
|
162
|
+
intervalMs: state.intervalMs,
|
|
163
|
+
inactivityMs: state.inactivityMs,
|
|
164
|
+
activityThreshold: state.newEventThreshold,
|
|
165
|
+
sourceEventCount: unrecappedSourceEvents.length,
|
|
166
|
+
totalSourceEventCount: sourceEvents.length,
|
|
167
|
+
latestSourceEventAt,
|
|
168
|
+
latestUnrecappedSourceEventAt,
|
|
169
|
+
sourceIdleMs,
|
|
170
|
+
unrecappedSourceIdleMs,
|
|
171
|
+
lastRecapAt: state.lastRecapAt,
|
|
172
|
+
lastSourceEventAt: state.lastSourceEventAt,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (!latestSourceEvent) {
|
|
176
|
+
return {
|
|
177
|
+
shouldEmit: false,
|
|
178
|
+
shouldStop: false,
|
|
179
|
+
stopAfterEmit: false,
|
|
180
|
+
mode: "",
|
|
181
|
+
reason: "recap_no_source_events",
|
|
182
|
+
sourceEvent: null,
|
|
183
|
+
policy,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!latestUnrecappedSourceEvent) {
|
|
188
|
+
return {
|
|
189
|
+
shouldEmit: false,
|
|
190
|
+
shouldStop: sourceIdleMs !== null && sourceIdleMs >= state.inactivityMs,
|
|
191
|
+
stopAfterEmit: false,
|
|
192
|
+
mode: "",
|
|
193
|
+
reason: "recap_no_new_source_events",
|
|
194
|
+
sourceEvent: null,
|
|
195
|
+
policy,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (unrecappedSourceIdleMs !== null && unrecappedSourceIdleMs >= state.inactivityMs) {
|
|
200
|
+
return {
|
|
201
|
+
shouldEmit: true,
|
|
202
|
+
shouldStop: false,
|
|
203
|
+
stopAfterEmit: true,
|
|
204
|
+
mode: "inactivity",
|
|
205
|
+
reason: "",
|
|
206
|
+
sourceEvent: latestUnrecappedSourceEvent,
|
|
207
|
+
policy,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!state.lastRecapAt) {
|
|
212
|
+
return {
|
|
213
|
+
shouldEmit: true,
|
|
214
|
+
shouldStop: false,
|
|
215
|
+
stopAfterEmit: false,
|
|
216
|
+
mode: "initial",
|
|
217
|
+
reason: "",
|
|
218
|
+
sourceEvent: latestUnrecappedSourceEvent,
|
|
219
|
+
policy,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (unrecappedSourceEvents.length >= state.newEventThreshold) {
|
|
224
|
+
return {
|
|
225
|
+
shouldEmit: true,
|
|
226
|
+
shouldStop: false,
|
|
227
|
+
stopAfterEmit: false,
|
|
228
|
+
mode: "activity_threshold",
|
|
229
|
+
reason: "",
|
|
230
|
+
sourceEvent: latestUnrecappedSourceEvent,
|
|
231
|
+
policy,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (sinceLastRecapMs !== null && sinceLastRecapMs >= state.intervalMs) {
|
|
236
|
+
return {
|
|
237
|
+
shouldEmit: true,
|
|
238
|
+
shouldStop: false,
|
|
239
|
+
stopAfterEmit: false,
|
|
240
|
+
mode: "periodic",
|
|
241
|
+
reason: "",
|
|
242
|
+
sourceEvent: latestUnrecappedSourceEvent,
|
|
243
|
+
policy,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
shouldEmit: false,
|
|
249
|
+
shouldStop: false,
|
|
250
|
+
stopAfterEmit: false,
|
|
251
|
+
mode: "",
|
|
252
|
+
reason: "recap_cadence_wait",
|
|
253
|
+
sourceEvent: latestUnrecappedSourceEvent,
|
|
254
|
+
policy,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
67
258
|
function parseFindingSeverity(text = "") {
|
|
68
259
|
const normalized = normalizeString(text);
|
|
69
260
|
if (!normalized) {
|
|
@@ -704,8 +895,8 @@ export async function shouldEmitRecap(
|
|
|
704
895
|
tail: 0,
|
|
705
896
|
since: normalizeString(lastReadAt) || null,
|
|
706
897
|
});
|
|
707
|
-
const
|
|
708
|
-
if (
|
|
898
|
+
const isRelevantSourceEvent = (event = {}) => {
|
|
899
|
+
if (!isMeaningfulRecapSourceEvent(event)) {
|
|
709
900
|
return false;
|
|
710
901
|
}
|
|
711
902
|
const sourceAgent = normalizeString(event.agent?.id || event.agentId).toLowerCase();
|
|
@@ -713,20 +904,23 @@ export async function shouldEmitRecap(
|
|
|
713
904
|
return true;
|
|
714
905
|
}
|
|
715
906
|
return sourceAgent !== normalizedAgentId;
|
|
716
|
-
}
|
|
717
|
-
|
|
907
|
+
};
|
|
908
|
+
const relevantSinceRead = eventsSinceRead.filter(isRelevantSourceEvent);
|
|
909
|
+
if (relevantSinceRead.length >= threshold) {
|
|
718
910
|
return true;
|
|
719
911
|
}
|
|
720
912
|
|
|
721
913
|
const latest = await readStream(normalizedSessionId, {
|
|
722
914
|
targetPath,
|
|
723
|
-
tail:
|
|
915
|
+
tail: 200,
|
|
724
916
|
});
|
|
725
|
-
const
|
|
726
|
-
|
|
917
|
+
const latestRelevant = sortEventsByConversationTime(latest, normalizedNow)
|
|
918
|
+
.filter(isRelevantSourceEvent)
|
|
919
|
+
.at(-1);
|
|
920
|
+
if (!latestRelevant) {
|
|
727
921
|
return false;
|
|
728
922
|
}
|
|
729
|
-
const idleMs = Math.max(0, toEpoch(normalizedNow, normalizedNow) - toEpoch(
|
|
923
|
+
const idleMs = Math.max(0, toEpoch(normalizedNow, normalizedNow) - toEpoch(latestRelevant.ts, normalizedNow));
|
|
730
924
|
return idleMs >= normalizedInactivityMs;
|
|
731
925
|
}
|
|
732
926
|
|
|
@@ -735,6 +929,7 @@ export function emitPeriodicRecap(
|
|
|
735
929
|
{
|
|
736
930
|
intervalMs = DEFAULT_RECAP_INTERVAL_MS,
|
|
737
931
|
inactivityMs = DEFAULT_RECAP_INACTIVITY_MS,
|
|
932
|
+
newEventThreshold = DEFAULT_RECAP_ACTIVITY_THRESHOLD,
|
|
738
933
|
maxEvents = DEFAULT_RECAP_MAX_EVENTS,
|
|
739
934
|
targetPath = process.cwd(),
|
|
740
935
|
nowProvider = () => new Date().toISOString(),
|
|
@@ -751,6 +946,10 @@ export function emitPeriodicRecap(
|
|
|
751
946
|
inactivityMs,
|
|
752
947
|
DEFAULT_RECAP_INACTIVITY_MS
|
|
753
948
|
);
|
|
949
|
+
const normalizedNewEventThreshold = normalizePositiveInteger(
|
|
950
|
+
newEventThreshold,
|
|
951
|
+
DEFAULT_RECAP_ACTIVITY_THRESHOLD
|
|
952
|
+
);
|
|
754
953
|
const normalizedMaxEvents = normalizePositiveInteger(maxEvents, DEFAULT_RECAP_MAX_EVENTS);
|
|
755
954
|
const key = buildRecapKey(normalizedSessionId, normalizedTargetPath);
|
|
756
955
|
const existing = ACTIVE_RECAP_EMITTERS.get(key);
|
|
@@ -764,13 +963,18 @@ export function emitPeriodicRecap(
|
|
|
764
963
|
startedAt: normalizeIsoTimestamp(nowProvider(), new Date().toISOString()),
|
|
765
964
|
intervalMs: normalizedIntervalMs,
|
|
766
965
|
inactivityMs: normalizedInactivityMs,
|
|
966
|
+
newEventThreshold: normalizedNewEventThreshold,
|
|
767
967
|
maxEvents: normalizedMaxEvents,
|
|
768
968
|
targetPath: normalizedTargetPath,
|
|
769
969
|
sessionId: normalizedSessionId,
|
|
770
970
|
timer: null,
|
|
971
|
+
inFlight: false,
|
|
771
972
|
lastRecapAt: null,
|
|
772
973
|
lastSourceEventAt: null,
|
|
974
|
+
lastSourceSequenceId: null,
|
|
975
|
+
lastSourceCursor: null,
|
|
773
976
|
lastRecapEvent: null,
|
|
977
|
+
lastDecision: null,
|
|
774
978
|
stoppedReason: null,
|
|
775
979
|
};
|
|
776
980
|
|
|
@@ -794,77 +998,87 @@ export function emitPeriodicRecap(
|
|
|
794
998
|
sessionId: state.sessionId,
|
|
795
999
|
reason: state.stoppedReason,
|
|
796
1000
|
lastRecapAt: state.lastRecapAt,
|
|
1001
|
+
lastDecision: state.lastDecision,
|
|
797
1002
|
};
|
|
798
1003
|
};
|
|
799
1004
|
|
|
800
|
-
const tickNow = async () => {
|
|
801
|
-
if (!state.running) {
|
|
802
|
-
return null;
|
|
803
|
-
}
|
|
804
|
-
const nowIso = normalizeIsoTimestamp(nowProvider(), new Date().toISOString());
|
|
805
|
-
const nowEpoch = toEpoch(nowIso, nowIso);
|
|
806
|
-
const events = await readStream(state.sessionId, {
|
|
807
|
-
targetPath: state.targetPath,
|
|
808
|
-
tail: state.maxEvents,
|
|
809
|
-
});
|
|
810
|
-
const nonRecapEvents = events.filter((event) => !isRecapEvent(event));
|
|
811
|
-
const latestSourceEvent = nonRecapEvents.length > 0 ? nonRecapEvents[nonRecapEvents.length - 1] : null;
|
|
812
|
-
if (!latestSourceEvent) {
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const latestSourceEpoch = toEpoch(latestSourceEvent.ts, nowIso);
|
|
817
|
-
const idleMs = Math.max(0, nowEpoch - latestSourceEpoch);
|
|
818
|
-
if (idleMs >= state.inactivityMs) {
|
|
819
|
-
stop("inactive");
|
|
1005
|
+
const tickNow = async (overrideNowIso = "") => {
|
|
1006
|
+
if (!state.running || state.inFlight) {
|
|
820
1007
|
return null;
|
|
821
1008
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1009
|
+
state.inFlight = true;
|
|
1010
|
+
try {
|
|
1011
|
+
const nowIso = normalizeIsoTimestamp(
|
|
1012
|
+
overrideNowIso || nowProvider(),
|
|
1013
|
+
new Date().toISOString()
|
|
1014
|
+
);
|
|
1015
|
+
const events = await readStream(state.sessionId, {
|
|
1016
|
+
targetPath: state.targetPath,
|
|
1017
|
+
tail: state.maxEvents,
|
|
1018
|
+
});
|
|
1019
|
+
const trigger = resolvePeriodicRecapTrigger(state, events, nowIso);
|
|
1020
|
+
state.lastDecision = {
|
|
1021
|
+
emitted: false,
|
|
1022
|
+
mode: trigger.mode,
|
|
1023
|
+
reason: trigger.reason,
|
|
1024
|
+
policy: trigger.policy,
|
|
1025
|
+
};
|
|
1026
|
+
if (!trigger.shouldEmit) {
|
|
1027
|
+
if (trigger.shouldStop) {
|
|
1028
|
+
stop("inactive");
|
|
1029
|
+
}
|
|
832
1030
|
return null;
|
|
833
1031
|
}
|
|
834
|
-
}
|
|
835
1032
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1033
|
+
const recap = await buildSessionRecap(state.sessionId, {
|
|
1034
|
+
targetPath: state.targetPath,
|
|
1035
|
+
maxEvents: state.maxEvents,
|
|
1036
|
+
nowIso,
|
|
1037
|
+
});
|
|
1038
|
+
const text = buildPeriodicText(recap);
|
|
1039
|
+
const event = createAgentEvent({
|
|
1040
|
+
event: "session_recap",
|
|
1041
|
+
agentId: SENTI_AGENT_ID,
|
|
1042
|
+
agentModel: SENTI_MODEL,
|
|
1043
|
+
sessionId: state.sessionId,
|
|
1044
|
+
ts: nowIso,
|
|
1045
|
+
payload: {
|
|
1046
|
+
mode: trigger.mode,
|
|
1047
|
+
recap: text,
|
|
1048
|
+
ephemeral: true,
|
|
1049
|
+
style: RECAP_STYLE,
|
|
1050
|
+
generatedAt: nowIso,
|
|
1051
|
+
sourceEventCount: trigger.policy.sourceEventCount,
|
|
1052
|
+
latestSourceEventAt: trigger.policy.latestUnrecappedSourceEventAt,
|
|
1053
|
+
policy: trigger.policy,
|
|
1054
|
+
summary: recap.summary,
|
|
1055
|
+
},
|
|
1056
|
+
});
|
|
1057
|
+
const persisted = await appendToStream(state.sessionId, event, {
|
|
1058
|
+
targetPath: state.targetPath,
|
|
1059
|
+
});
|
|
1060
|
+
state.lastRecapAt = nowIso;
|
|
1061
|
+
rememberRecapSource(state, trigger.sourceEvent, nowIso);
|
|
1062
|
+
state.lastRecapEvent = persisted;
|
|
1063
|
+
state.lastDecision = {
|
|
1064
|
+
emitted: true,
|
|
1065
|
+
mode: trigger.mode,
|
|
1066
|
+
reason: "",
|
|
1067
|
+
eventId: persisted.eventId || null,
|
|
1068
|
+
sourceEventCount: trigger.policy.sourceEventCount,
|
|
1069
|
+
policy: trigger.policy,
|
|
1070
|
+
};
|
|
863
1071
|
|
|
864
|
-
|
|
865
|
-
|
|
1072
|
+
if (typeof onEmit === "function") {
|
|
1073
|
+
await onEmit(persisted, recap);
|
|
1074
|
+
}
|
|
1075
|
+
if (trigger.stopAfterEmit) {
|
|
1076
|
+
stop("inactive");
|
|
1077
|
+
}
|
|
1078
|
+
return persisted;
|
|
1079
|
+
} finally {
|
|
1080
|
+
state.inFlight = false;
|
|
866
1081
|
}
|
|
867
|
-
return persisted;
|
|
868
1082
|
};
|
|
869
1083
|
|
|
870
1084
|
const handle = {
|
|
@@ -880,8 +1094,12 @@ export function emitPeriodicRecap(
|
|
|
880
1094
|
running: state.running,
|
|
881
1095
|
intervalMs: state.intervalMs,
|
|
882
1096
|
inactivityMs: state.inactivityMs,
|
|
1097
|
+
newEventThreshold: state.newEventThreshold,
|
|
1098
|
+
inFlight: state.inFlight,
|
|
883
1099
|
lastRecapAt: state.lastRecapAt,
|
|
884
1100
|
lastSourceEventAt: state.lastSourceEventAt,
|
|
1101
|
+
lastSourceSequenceId: state.lastSourceSequenceId,
|
|
1102
|
+
lastDecision: state.lastDecision,
|
|
885
1103
|
stoppedReason: state.stoppedReason,
|
|
886
1104
|
}),
|
|
887
1105
|
};
|
|
@@ -906,6 +1124,7 @@ export function getPeriodicRecapEmitter(sessionId, { targetPath = process.cwd()
|
|
|
906
1124
|
|
|
907
1125
|
export {
|
|
908
1126
|
ACTIVE_RECAP_EMITTERS,
|
|
1127
|
+
DEFAULT_RECAP_ACTIVITY_THRESHOLD,
|
|
909
1128
|
DEFAULT_RECAP_INACTIVITY_MS,
|
|
910
1129
|
DEFAULT_RECAP_INTERVAL_MS,
|
|
911
1130
|
DEFAULT_RECAP_MAX_EVENTS,
|
package/src/session/sync.js
CHANGED
|
@@ -1901,10 +1901,7 @@ export async function probeSessionAccess(
|
|
|
1901
1901
|
}
|
|
1902
1902
|
|
|
1903
1903
|
export function resetSessionSyncStateForTests() {
|
|
1904
|
-
|
|
1905
|
-
outboundCircuit.openedAtMs = 0;
|
|
1906
|
-
inboundCircuit.consecutiveFailures = 0;
|
|
1907
|
-
inboundCircuit.openedAtMs = 0;
|
|
1904
|
+
__resetCircuitStateForTests();
|
|
1908
1905
|
sessionIngestWindowBySessionId.clear();
|
|
1909
1906
|
humanRelayWindowBySessionId.clear();
|
|
1910
1907
|
autoGrantAttemptedAgentIds.clear();
|
package/src/session/usage.js
CHANGED
|
@@ -41,6 +41,12 @@ import process from "node:process";
|
|
|
41
41
|
import { randomUUID } from "node:crypto";
|
|
42
42
|
|
|
43
43
|
import { createAgentEvent } from "../events/schema.js";
|
|
44
|
+
import { estimateModelCost } from "../cost/tracker.js";
|
|
45
|
+
import {
|
|
46
|
+
DEFAULT_PRICE_BOOK_VERSION,
|
|
47
|
+
buildSessionUsageLedger,
|
|
48
|
+
createSessionUsageLedgerId,
|
|
49
|
+
} from "./pricing-ledger.js";
|
|
44
50
|
import { resolveSessionPaths } from "./paths.js";
|
|
45
51
|
import { appendToStream } from "./stream.js";
|
|
46
52
|
|
|
@@ -55,19 +61,8 @@ function num(value) {
|
|
|
55
61
|
return Number.isFinite(v) && v >= 0 ? v : 0;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
function
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function firstUsageNumber(payload = {}, keys = []) {
|
|
63
|
-
const usage = plainObject(payload.usage);
|
|
64
|
-
for (const key of keys) {
|
|
65
|
-
const direct = num(payload[key]);
|
|
66
|
-
if (direct > 0) return direct;
|
|
67
|
-
const nested = num(usage[key]);
|
|
68
|
-
if (nested > 0) return nested;
|
|
69
|
-
}
|
|
70
|
-
return 0;
|
|
64
|
+
function money(value) {
|
|
65
|
+
return Math.round(num(value) * 1_000_000) / 1_000_000;
|
|
71
66
|
}
|
|
72
67
|
|
|
73
68
|
function clipText(text, max = 4000) {
|
|
@@ -76,6 +71,17 @@ function clipText(text, max = 4000) {
|
|
|
76
71
|
return `${s.slice(0, max)}…`;
|
|
77
72
|
}
|
|
78
73
|
|
|
74
|
+
function computedCost({ model, inputTokens, outputTokens }) {
|
|
75
|
+
try {
|
|
76
|
+
return {
|
|
77
|
+
costUsd: estimateModelCost({ modelId: model, inputTokens, outputTokens }),
|
|
78
|
+
unpriced: false,
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return { costUsd: 0, unpriced: inputTokens + outputTokens > 0 };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
/**
|
|
80
86
|
* Emit a `session_usage` event into the session's NDJSON stream.
|
|
81
87
|
*
|
|
@@ -87,7 +93,12 @@ function clipText(text, max = 4000) {
|
|
|
87
93
|
* @param {number} [params.inputTokens]
|
|
88
94
|
* @param {number} [params.outputTokens]
|
|
89
95
|
* @param {number} [params.costUsd]
|
|
96
|
+
* @param {number} [params.customerCostUsd]
|
|
90
97
|
* @param {number} [params.durationMs]
|
|
98
|
+
* @param {string} [params.action]
|
|
99
|
+
* @param {string} [params.provider]
|
|
100
|
+
* @param {string} [params.billingTier]
|
|
101
|
+
* @param {string} [params.priceBookVersion]
|
|
91
102
|
* @param {string} [params.prompt] full prompt text (clipped)
|
|
92
103
|
* @param {string} [params.response] full response text (clipped)
|
|
93
104
|
* @param {string} [params.interactionId] opaque id for cross-event correlation
|
|
@@ -102,8 +113,13 @@ export async function emitLLMInteraction(
|
|
|
102
113
|
role = "",
|
|
103
114
|
inputTokens = 0,
|
|
104
115
|
outputTokens = 0,
|
|
105
|
-
costUsd =
|
|
116
|
+
costUsd = undefined,
|
|
117
|
+
customerCostUsd = undefined,
|
|
106
118
|
durationMs = 0,
|
|
119
|
+
action = "agent_message",
|
|
120
|
+
provider = "",
|
|
121
|
+
billingTier = "unknown",
|
|
122
|
+
priceBookVersion = DEFAULT_PRICE_BOOK_VERSION,
|
|
107
123
|
prompt = "",
|
|
108
124
|
response = "",
|
|
109
125
|
interactionId = "",
|
|
@@ -121,20 +137,44 @@ export async function emitLLMInteraction(
|
|
|
121
137
|
const inT = Math.floor(num(inputTokens));
|
|
122
138
|
const outT = Math.floor(num(outputTokens));
|
|
123
139
|
const totalT = inT + outT;
|
|
124
|
-
const
|
|
140
|
+
const model = n(agentModel) || "unknown";
|
|
141
|
+
const providedCost = costUsd != null && costUsd !== "";
|
|
142
|
+
const estimate = providedCost
|
|
143
|
+
? { costUsd: num(costUsd), unpriced: false }
|
|
144
|
+
: computedCost({ model, inputTokens: inT, outputTokens: outT });
|
|
145
|
+
const cost = money(estimate.costUsd);
|
|
146
|
+
const customerCost = customerCostUsd == null || customerCostUsd === "" ? null : money(customerCostUsd);
|
|
147
|
+
const actionName = n(action) || "agent_message";
|
|
148
|
+
const tier = n(billingTier) || "unknown";
|
|
149
|
+
const priceBook = n(priceBookVersion) || DEFAULT_PRICE_BOOK_VERSION;
|
|
150
|
+
const ledgerEntryId = createSessionUsageLedgerId({
|
|
151
|
+
sessionId: paths.sessionId,
|
|
152
|
+
agentId: aid,
|
|
153
|
+
action: actionName,
|
|
154
|
+
idempotencyKey: id,
|
|
155
|
+
});
|
|
125
156
|
|
|
126
157
|
const promptText = clipText(prompt);
|
|
127
158
|
const responseText = clipText(response);
|
|
128
159
|
|
|
129
160
|
const payload = {
|
|
130
161
|
interactionId: id,
|
|
162
|
+
idempotencyKey: id,
|
|
163
|
+
ledgerEntryId,
|
|
131
164
|
agentId: aid,
|
|
132
|
-
model
|
|
165
|
+
model,
|
|
133
166
|
role: n(role) || "observer",
|
|
167
|
+
action: actionName,
|
|
168
|
+
provider: n(provider) || undefined,
|
|
169
|
+
billingTier: tier,
|
|
170
|
+
priceBookVersion: priceBook,
|
|
134
171
|
inputTokens: inT,
|
|
135
172
|
outputTokens: outT,
|
|
136
173
|
totalTokens: totalT,
|
|
137
174
|
costUsd: cost,
|
|
175
|
+
providerCostUsd: cost,
|
|
176
|
+
customerCostUsd: customerCost ?? undefined,
|
|
177
|
+
unpriced: estimate.unpriced,
|
|
138
178
|
durationMs: Math.max(0, Math.floor(num(durationMs))),
|
|
139
179
|
prompt: { tokens: inT, chars: promptText.length },
|
|
140
180
|
response: {
|
|
@@ -147,15 +187,24 @@ export async function emitLLMInteraction(
|
|
|
147
187
|
usage: {
|
|
148
188
|
totalTokens: totalT,
|
|
149
189
|
costUsd: cost,
|
|
190
|
+
providerCostUsd: cost,
|
|
191
|
+
customerCostUsd: customerCost ?? undefined,
|
|
150
192
|
inputTokens: inT,
|
|
151
193
|
outputTokens: outT,
|
|
194
|
+
action: actionName,
|
|
195
|
+
provider: n(provider) || undefined,
|
|
196
|
+
billingTier: tier,
|
|
197
|
+
priceBookVersion: priceBook,
|
|
198
|
+
ledgerEntryId,
|
|
199
|
+
idempotencyKey: id,
|
|
200
|
+
unpriced: estimate.unpriced,
|
|
152
201
|
},
|
|
153
202
|
};
|
|
154
203
|
|
|
155
204
|
const envelope = createAgentEvent({
|
|
156
205
|
event: SESSION_USAGE_EVENT,
|
|
157
206
|
agentId: aid,
|
|
158
|
-
agentModel:
|
|
207
|
+
agentModel: model,
|
|
159
208
|
sessionId: paths.sessionId,
|
|
160
209
|
payload,
|
|
161
210
|
ts,
|
|
@@ -165,6 +214,7 @@ export async function emitLLMInteraction(
|
|
|
165
214
|
return {
|
|
166
215
|
event: SESSION_USAGE_EVENT,
|
|
167
216
|
interactionId: id,
|
|
217
|
+
ledgerEntryId,
|
|
168
218
|
totalTokens: totalT,
|
|
169
219
|
costUsd: cost,
|
|
170
220
|
};
|
|
@@ -181,6 +231,7 @@ export async function emitLLMInteraction(
|
|
|
181
231
|
* }}
|
|
182
232
|
*/
|
|
183
233
|
export function aggregateSessionUsage(events = []) {
|
|
234
|
+
const ledger = buildSessionUsageLedger(events);
|
|
184
235
|
const perAgent = new Map();
|
|
185
236
|
const totals = {
|
|
186
237
|
totalTokens: 0,
|
|
@@ -189,37 +240,8 @@ export function aggregateSessionUsage(events = []) {
|
|
|
189
240
|
costUsd: 0,
|
|
190
241
|
interactions: 0,
|
|
191
242
|
};
|
|
192
|
-
for (const
|
|
193
|
-
|
|
194
|
-
const payload = event.payload || {};
|
|
195
|
-
const agentId = n(payload.agentId || event.agent?.id);
|
|
196
|
-
if (!agentId) continue;
|
|
197
|
-
const model = n(payload.model || event.agent?.model) || "unknown";
|
|
198
|
-
const inputTokens = firstUsageNumber(payload, [
|
|
199
|
-
"inputTokens",
|
|
200
|
-
"input_tokens",
|
|
201
|
-
"tokensIn",
|
|
202
|
-
"tokens_in",
|
|
203
|
-
]);
|
|
204
|
-
const outputTokens = firstUsageNumber(payload, [
|
|
205
|
-
"outputTokens",
|
|
206
|
-
"output_tokens",
|
|
207
|
-
"tokensOut",
|
|
208
|
-
"tokens_out",
|
|
209
|
-
]);
|
|
210
|
-
const explicitTotalTokens = firstUsageNumber(payload, [
|
|
211
|
-
"totalTokens",
|
|
212
|
-
"total_tokens",
|
|
213
|
-
"tokens",
|
|
214
|
-
]);
|
|
215
|
-
const totalTokens = explicitTotalTokens || inputTokens + outputTokens;
|
|
216
|
-
const costUsd = firstUsageNumber(payload, [
|
|
217
|
-
"costUsd",
|
|
218
|
-
"cost_usd",
|
|
219
|
-
"providerCostUsd",
|
|
220
|
-
"provider_cost_usd",
|
|
221
|
-
"cost",
|
|
222
|
-
]);
|
|
243
|
+
for (const entry of ledger.entries) {
|
|
244
|
+
const { agentId, model, inputTokens, outputTokens, totalTokens, providerCostUsd } = entry;
|
|
223
245
|
if (!perAgent.has(agentId)) {
|
|
224
246
|
perAgent.set(agentId, {
|
|
225
247
|
agentId,
|
|
@@ -238,13 +260,13 @@ export function aggregateSessionUsage(events = []) {
|
|
|
238
260
|
record.totalTokens += totalTokens;
|
|
239
261
|
record.inputTokens += inputTokens;
|
|
240
262
|
record.outputTokens += outputTokens;
|
|
241
|
-
record.costUsd +=
|
|
263
|
+
record.costUsd += providerCostUsd;
|
|
242
264
|
record.interactions += 1;
|
|
243
265
|
|
|
244
266
|
totals.totalTokens += totalTokens;
|
|
245
267
|
totals.inputTokens += inputTokens;
|
|
246
268
|
totals.outputTokens += outputTokens;
|
|
247
|
-
totals.costUsd +=
|
|
269
|
+
totals.costUsd += providerCostUsd;
|
|
248
270
|
totals.interactions += 1;
|
|
249
271
|
}
|
|
250
272
|
totals.costUsd = Math.round(totals.costUsd * 1_000_000) / 1_000_000;
|