@newrelic/preflight 1.68.0 → 1.70.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 +1 -1
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +76 -6
- package/dist/config.js.map +1 -1
- package/dist/dashboard/routes/api-handler.d.ts +1 -0
- package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
- package/dist/dashboard/routes/api-handler.js +109 -91
- package/dist/dashboard/routes/api-handler.js.map +1 -1
- package/dist/data/dashboards/ai-coding-assistant-adoption-cost.json +291 -1
- package/dist/data/dashboards/ai-coding-assistant-team-view.json +6 -6
- package/dist/hooks/billed-turn-ledger.d.ts +46 -0
- package/dist/hooks/billed-turn-ledger.d.ts.map +1 -0
- package/dist/hooks/billed-turn-ledger.js +274 -0
- package/dist/hooks/billed-turn-ledger.js.map +1 -0
- package/dist/hooks/collector-script.d.ts.map +1 -1
- package/dist/hooks/collector-script.js +5 -1
- package/dist/hooks/collector-script.js.map +1 -1
- package/dist/hooks/event-processor.d.ts +3 -7
- package/dist/hooks/event-processor.d.ts.map +1 -1
- package/dist/hooks/event-processor.js +21 -7
- package/dist/hooks/event-processor.js.map +1 -1
- package/dist/hooks/parent-transcript-watcher.d.ts +6 -5
- package/dist/hooks/parent-transcript-watcher.d.ts.map +1 -1
- package/dist/hooks/parent-transcript-watcher.js +64 -62
- package/dist/hooks/parent-transcript-watcher.js.map +1 -1
- package/dist/hooks/throttled-warning.d.ts +16 -0
- package/dist/hooks/throttled-warning.d.ts.map +1 -0
- package/dist/hooks/throttled-warning.js +32 -0
- package/dist/hooks/throttled-warning.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -8
- package/dist/index.js.map +1 -1
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +53 -0
- package/dist/install/cli.js.map +1 -1
- package/dist/install/diagnostics.d.ts.map +1 -1
- package/dist/install/diagnostics.js +17 -3
- package/dist/install/diagnostics.js.map +1 -1
- package/dist/install/keychain.d.ts +18 -0
- package/dist/install/keychain.d.ts.map +1 -0
- package/dist/install/keychain.js +60 -0
- package/dist/install/keychain.js.map +1 -0
- package/dist/install/kiro-install-helper.d.ts +27 -0
- package/dist/install/kiro-install-helper.d.ts.map +1 -0
- package/dist/install/kiro-install-helper.js +53 -0
- package/dist/install/kiro-install-helper.js.map +1 -0
- package/dist/install/setup-wizard.d.ts +4 -0
- package/dist/install/setup-wizard.d.ts.map +1 -1
- package/dist/install/setup-wizard.js +94 -9
- package/dist/install/setup-wizard.js.map +1 -1
- package/dist/metrics/cost-tracker-seed.d.ts +16 -0
- package/dist/metrics/cost-tracker-seed.d.ts.map +1 -1
- package/dist/metrics/cost-tracker-seed.js +42 -7
- package/dist/metrics/cost-tracker-seed.js.map +1 -1
- package/dist/metrics/cost-tracker.d.ts +11 -3
- package/dist/metrics/cost-tracker.d.ts.map +1 -1
- package/dist/metrics/cost-tracker.js +4 -1
- package/dist/metrics/cost-tracker.js.map +1 -1
- package/dist/metrics/git-activity-recorder.d.ts +3 -3
- package/dist/metrics/git-activity-recorder.d.ts.map +1 -1
- package/dist/metrics/git-activity-recorder.js +3 -3
- package/dist/metrics/git-activity-recorder.js.map +1 -1
- package/dist/metrics/git-workspace-identity.d.ts +6 -1
- package/dist/metrics/git-workspace-identity.d.ts.map +1 -1
- package/dist/metrics/git-workspace-identity.js +56 -32
- package/dist/metrics/git-workspace-identity.js.map +1 -1
- package/dist/metrics/git-workspace-reporter.d.ts +4 -3
- package/dist/metrics/git-workspace-reporter.d.ts.map +1 -1
- package/dist/metrics/git-workspace-reporter.js +4 -3
- package/dist/metrics/git-workspace-reporter.js.map +1 -1
- package/dist/metrics/skill-activation-tracker.d.ts +27 -0
- package/dist/metrics/skill-activation-tracker.d.ts.map +1 -0
- package/dist/metrics/skill-activation-tracker.js +92 -0
- package/dist/metrics/skill-activation-tracker.js.map +1 -0
- package/dist/metrics/turn-cost-attributor.d.ts +34 -16
- package/dist/metrics/turn-cost-attributor.d.ts.map +1 -1
- package/dist/metrics/turn-cost-attributor.js +95 -60
- package/dist/metrics/turn-cost-attributor.js.map +1 -1
- package/dist/secret-command.d.ts +5 -3
- package/dist/secret-command.d.ts.map +1 -1
- package/dist/secret-command.js +17 -11
- package/dist/secret-command.js.map +1 -1
- package/dist/shared/pricing-data.d.ts.map +1 -1
- package/dist/shared/pricing-data.js +223 -32
- package/dist/shared/pricing-data.js.map +1 -1
- package/dist/shared/pricing.d.ts +25 -3
- package/dist/shared/pricing.d.ts.map +1 -1
- package/dist/shared/pricing.js +46 -9
- package/dist/shared/pricing.js.map +1 -1
- package/dist/shared/tokens.d.ts +16 -0
- package/dist/shared/tokens.d.ts.map +1 -1
- package/dist/shared/tokens.js +18 -0
- package/dist/shared/tokens.js.map +1 -1
- package/dist/shared/transport/otlp-shared.d.ts.map +1 -1
- package/dist/shared/transport/otlp-shared.js +7 -14
- package/dist/shared/transport/otlp-shared.js.map +1 -1
- package/dist/storage/session-store.d.ts +24 -1
- package/dist/storage/session-store.d.ts.map +1 -1
- package/dist/storage/session-store.js +34 -4
- package/dist/storage/session-store.js.map +1 -1
- package/dist/storage/types.d.ts +13 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js.map +1 -1
- package/dist/tools/session-stats.js +1 -1
- package/dist/tools/session-stats.js.map +1 -1
- package/dist/transport/nr-ingest.d.ts +4 -0
- package/dist/transport/nr-ingest.d.ts.map +1 -1
- package/dist/transport/nr-ingest.js +43 -19
- package/dist/transport/nr-ingest.js.map +1 -1
- package/dist/transport/tier-types.d.ts +11 -1
- package/dist/transport/tier-types.d.ts.map +1 -1
- package/dist/transport/tier-types.js +28 -6
- package/dist/transport/tier-types.js.map +1 -1
- package/dist/web/assets/{index-lvdXUntr.js → index-U9DPZ-Tb.js} +3 -3
- package/dist/web/index.html +1 -1
- package/examples/local-alert-rules.json +28 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "AI Coding Assistant — Adoption & Cost",
|
|
3
|
-
"description": "Org-wide adoption, spend, tool
|
|
3
|
+
"description": "Org-wide adoption, spend, tool, skill, and MCP usage, and team leaderboard built entirely from Preflight events. Works on every supported platform with no additional telemetry.",
|
|
4
4
|
"permissions": "PUBLIC_READ_WRITE",
|
|
5
5
|
"pages": [
|
|
6
6
|
{
|
|
@@ -930,6 +930,296 @@
|
|
|
930
930
|
}
|
|
931
931
|
]
|
|
932
932
|
},
|
|
933
|
+
{
|
|
934
|
+
"name": "Skills",
|
|
935
|
+
"description": null,
|
|
936
|
+
"widgets": [
|
|
937
|
+
{
|
|
938
|
+
"title": "Reading Skill Data",
|
|
939
|
+
"layout": {
|
|
940
|
+
"column": 1,
|
|
941
|
+
"row": 1,
|
|
942
|
+
"width": 12,
|
|
943
|
+
"height": 1
|
|
944
|
+
},
|
|
945
|
+
"visualization": {
|
|
946
|
+
"id": "viz.markdown"
|
|
947
|
+
},
|
|
948
|
+
"rawConfiguration": {
|
|
949
|
+
"text": "A skill run opens on a named `Skill` call or a `/<skill>` prompt and lasts until the next prompt or the next skill. Every tool call inside the run carries `skill_name`, `skill_source` (`tool` or `slash`), and `skill_run_id`, so runs, time, and cost here cover all the work a skill drove, not only the call that loaded it. Cost comes from `AiTurnCost`, which splits each turn evenly across its tool calls; a run whose replies made no tool calls has no rows. These widgets need a Preflight release with #895. Earlier events carry only `skillName` on the Skill call itself."
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
"title": "Spend in Skills (7d)",
|
|
954
|
+
"layout": {
|
|
955
|
+
"column": 1,
|
|
956
|
+
"row": 2,
|
|
957
|
+
"width": 3,
|
|
958
|
+
"height": 3
|
|
959
|
+
},
|
|
960
|
+
"visualization": {
|
|
961
|
+
"id": "viz.billboard"
|
|
962
|
+
},
|
|
963
|
+
"rawConfiguration": {
|
|
964
|
+
"facet": {
|
|
965
|
+
"showOtherSeries": false
|
|
966
|
+
},
|
|
967
|
+
"nrqlQueries": [
|
|
968
|
+
{
|
|
969
|
+
"accountIds": [],
|
|
970
|
+
"query": "FROM AiTurnCost SELECT filter(sum(cost_usd), WHERE skill_name IS NOT NULL) / sum(cost_usd) * 100 AS 'Spend in Skills %' SINCE 7 days ago"
|
|
971
|
+
}
|
|
972
|
+
],
|
|
973
|
+
"platformOptions": {
|
|
974
|
+
"ignoreTimeRange": false
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
"title": "Skill Runs (7d)",
|
|
980
|
+
"layout": {
|
|
981
|
+
"column": 4,
|
|
982
|
+
"row": 2,
|
|
983
|
+
"width": 3,
|
|
984
|
+
"height": 3
|
|
985
|
+
},
|
|
986
|
+
"visualization": {
|
|
987
|
+
"id": "viz.billboard"
|
|
988
|
+
},
|
|
989
|
+
"rawConfiguration": {
|
|
990
|
+
"facet": {
|
|
991
|
+
"showOtherSeries": false
|
|
992
|
+
},
|
|
993
|
+
"nrqlQueries": [
|
|
994
|
+
{
|
|
995
|
+
"accountIds": [],
|
|
996
|
+
"query": "FROM AiToolCall SELECT uniqueCount(skill_run_id) AS 'Runs' WHERE skill_name IS NOT NULL SINCE 7 days ago"
|
|
997
|
+
}
|
|
998
|
+
],
|
|
999
|
+
"platformOptions": {
|
|
1000
|
+
"ignoreTimeRange": false
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
"title": "Skill Users (7d)",
|
|
1006
|
+
"layout": {
|
|
1007
|
+
"column": 7,
|
|
1008
|
+
"row": 2,
|
|
1009
|
+
"width": 3,
|
|
1010
|
+
"height": 3
|
|
1011
|
+
},
|
|
1012
|
+
"visualization": {
|
|
1013
|
+
"id": "viz.billboard"
|
|
1014
|
+
},
|
|
1015
|
+
"rawConfiguration": {
|
|
1016
|
+
"facet": {
|
|
1017
|
+
"showOtherSeries": false
|
|
1018
|
+
},
|
|
1019
|
+
"nrqlQueries": [
|
|
1020
|
+
{
|
|
1021
|
+
"accountIds": [],
|
|
1022
|
+
"query": "FROM AiToolCall SELECT uniqueCount(developer) AS 'Users' WHERE skill_name IS NOT NULL SINCE 7 days ago"
|
|
1023
|
+
}
|
|
1024
|
+
],
|
|
1025
|
+
"platformOptions": {
|
|
1026
|
+
"ignoreTimeRange": false
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
"title": "How Skills Are Invoked (7d)",
|
|
1032
|
+
"layout": {
|
|
1033
|
+
"column": 10,
|
|
1034
|
+
"row": 2,
|
|
1035
|
+
"width": 3,
|
|
1036
|
+
"height": 3
|
|
1037
|
+
},
|
|
1038
|
+
"visualization": {
|
|
1039
|
+
"id": "viz.pie"
|
|
1040
|
+
},
|
|
1041
|
+
"rawConfiguration": {
|
|
1042
|
+
"facet": {
|
|
1043
|
+
"showOtherSeries": false
|
|
1044
|
+
},
|
|
1045
|
+
"legend": {
|
|
1046
|
+
"enabled": true
|
|
1047
|
+
},
|
|
1048
|
+
"nrqlQueries": [
|
|
1049
|
+
{
|
|
1050
|
+
"accountIds": [],
|
|
1051
|
+
"query": "FROM AiToolCall SELECT uniqueCount(skill_run_id) AS 'Runs' WHERE skill_name IS NOT NULL FACET skill_source SINCE 7 days ago"
|
|
1052
|
+
}
|
|
1053
|
+
],
|
|
1054
|
+
"platformOptions": {
|
|
1055
|
+
"ignoreTimeRange": false
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
"title": "Top Skills by Runs (7d)",
|
|
1061
|
+
"layout": {
|
|
1062
|
+
"column": 1,
|
|
1063
|
+
"row": 5,
|
|
1064
|
+
"width": 6,
|
|
1065
|
+
"height": 3
|
|
1066
|
+
},
|
|
1067
|
+
"visualization": {
|
|
1068
|
+
"id": "viz.bar"
|
|
1069
|
+
},
|
|
1070
|
+
"rawConfiguration": {
|
|
1071
|
+
"facet": {
|
|
1072
|
+
"showOtherSeries": false
|
|
1073
|
+
},
|
|
1074
|
+
"nrqlQueries": [
|
|
1075
|
+
{
|
|
1076
|
+
"accountIds": [],
|
|
1077
|
+
"query": "FROM AiToolCall SELECT uniqueCount(skill_run_id) AS 'Runs' WHERE skill_name IS NOT NULL FACET skill_name SINCE 7 days ago LIMIT 25"
|
|
1078
|
+
}
|
|
1079
|
+
],
|
|
1080
|
+
"platformOptions": {
|
|
1081
|
+
"ignoreTimeRange": false
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
"title": "Daily Skill Runs (7d)",
|
|
1087
|
+
"layout": {
|
|
1088
|
+
"column": 7,
|
|
1089
|
+
"row": 5,
|
|
1090
|
+
"width": 6,
|
|
1091
|
+
"height": 3
|
|
1092
|
+
},
|
|
1093
|
+
"visualization": {
|
|
1094
|
+
"id": "viz.line"
|
|
1095
|
+
},
|
|
1096
|
+
"rawConfiguration": {
|
|
1097
|
+
"facet": {
|
|
1098
|
+
"showOtherSeries": false
|
|
1099
|
+
},
|
|
1100
|
+
"legend": {
|
|
1101
|
+
"enabled": true
|
|
1102
|
+
},
|
|
1103
|
+
"yAxisLeft": {
|
|
1104
|
+
"zero": true
|
|
1105
|
+
},
|
|
1106
|
+
"nrqlQueries": [
|
|
1107
|
+
{
|
|
1108
|
+
"accountIds": [],
|
|
1109
|
+
"query": "FROM AiToolCall SELECT uniqueCount(skill_run_id) AS 'Runs' WHERE skill_name IS NOT NULL FACET skill_name SINCE 7 days ago TIMESERIES 1 day LIMIT 10"
|
|
1110
|
+
}
|
|
1111
|
+
],
|
|
1112
|
+
"platformOptions": {
|
|
1113
|
+
"ignoreTimeRange": false
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
"title": "Skill Cost (7d)",
|
|
1119
|
+
"layout": {
|
|
1120
|
+
"column": 1,
|
|
1121
|
+
"row": 8,
|
|
1122
|
+
"width": 6,
|
|
1123
|
+
"height": 4
|
|
1124
|
+
},
|
|
1125
|
+
"visualization": {
|
|
1126
|
+
"id": "viz.table"
|
|
1127
|
+
},
|
|
1128
|
+
"rawConfiguration": {
|
|
1129
|
+
"facet": {
|
|
1130
|
+
"showOtherSeries": false
|
|
1131
|
+
},
|
|
1132
|
+
"nrqlQueries": [
|
|
1133
|
+
{
|
|
1134
|
+
"accountIds": [],
|
|
1135
|
+
"query": "FROM AiToolCall, AiTurnCost SELECT uniqueCount(skill_run_id) AS 'Runs', sum(cost_usd) AS 'Cost (USD)', sum(cost_usd) / uniqueCount(skill_run_id) AS 'USD / Run', sum(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens) AS 'Tokens' WHERE skill_name IS NOT NULL FACET skill_name SINCE 7 days ago LIMIT 25"
|
|
1136
|
+
}
|
|
1137
|
+
],
|
|
1138
|
+
"platformOptions": {
|
|
1139
|
+
"ignoreTimeRange": false
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
"title": "Skill Adoption (7d)",
|
|
1145
|
+
"layout": {
|
|
1146
|
+
"column": 7,
|
|
1147
|
+
"row": 8,
|
|
1148
|
+
"width": 6,
|
|
1149
|
+
"height": 4
|
|
1150
|
+
},
|
|
1151
|
+
"visualization": {
|
|
1152
|
+
"id": "viz.table"
|
|
1153
|
+
},
|
|
1154
|
+
"rawConfiguration": {
|
|
1155
|
+
"facet": {
|
|
1156
|
+
"showOtherSeries": false
|
|
1157
|
+
},
|
|
1158
|
+
"nrqlQueries": [
|
|
1159
|
+
{
|
|
1160
|
+
"accountIds": [],
|
|
1161
|
+
"query": "FROM AiToolCall SELECT uniqueCount(developer) AS 'Users', uniqueCount(skill_run_id) AS 'Runs', count(*) AS 'Tool Calls', filter(sum(duration_ms), WHERE tool != 'Agent') / 3600000 AS 'Hours', percentage(count(*), WHERE success = false) AS 'Error %' WHERE skill_name IS NOT NULL FACET skill_name SINCE 7 days ago LIMIT 25"
|
|
1162
|
+
}
|
|
1163
|
+
],
|
|
1164
|
+
"platformOptions": {
|
|
1165
|
+
"ignoreTimeRange": false
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
"title": "Skills by Developer (7d)",
|
|
1171
|
+
"layout": {
|
|
1172
|
+
"column": 1,
|
|
1173
|
+
"row": 12,
|
|
1174
|
+
"width": 6,
|
|
1175
|
+
"height": 4
|
|
1176
|
+
},
|
|
1177
|
+
"visualization": {
|
|
1178
|
+
"id": "viz.table"
|
|
1179
|
+
},
|
|
1180
|
+
"rawConfiguration": {
|
|
1181
|
+
"facet": {
|
|
1182
|
+
"showOtherSeries": false
|
|
1183
|
+
},
|
|
1184
|
+
"nrqlQueries": [
|
|
1185
|
+
{
|
|
1186
|
+
"accountIds": [],
|
|
1187
|
+
"query": "FROM AiToolCall SELECT uniqueCount(skill_run_id) AS 'Runs', filter(sum(duration_ms), WHERE tool != 'Agent') / 3600000 AS 'Hours' WHERE skill_name IS NOT NULL FACET developer, skill_name SINCE 7 days ago LIMIT 100"
|
|
1188
|
+
}
|
|
1189
|
+
],
|
|
1190
|
+
"platformOptions": {
|
|
1191
|
+
"ignoreTimeRange": false
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
"title": "Tools Driven by Skills (7d)",
|
|
1197
|
+
"layout": {
|
|
1198
|
+
"column": 7,
|
|
1199
|
+
"row": 12,
|
|
1200
|
+
"width": 6,
|
|
1201
|
+
"height": 4
|
|
1202
|
+
},
|
|
1203
|
+
"visualization": {
|
|
1204
|
+
"id": "viz.table"
|
|
1205
|
+
},
|
|
1206
|
+
"rawConfiguration": {
|
|
1207
|
+
"facet": {
|
|
1208
|
+
"showOtherSeries": false
|
|
1209
|
+
},
|
|
1210
|
+
"nrqlQueries": [
|
|
1211
|
+
{
|
|
1212
|
+
"accountIds": [],
|
|
1213
|
+
"query": "FROM AiToolCall SELECT count(*) AS 'Calls', percentage(count(*), WHERE success = false) AS 'Error %' WHERE skill_name IS NOT NULL AND tool != 'Skill' FACET skill_name, tool SINCE 7 days ago LIMIT 50"
|
|
1214
|
+
}
|
|
1215
|
+
],
|
|
1216
|
+
"platformOptions": {
|
|
1217
|
+
"ignoreTimeRange": false
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
]
|
|
1222
|
+
},
|
|
933
1223
|
{
|
|
934
1224
|
"name": "Team Leaderboard",
|
|
935
1225
|
"description": null,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "AI Coding Assistant
|
|
2
|
+
"name": "AI Coding Assistant — Team View",
|
|
3
3
|
"description": "Team-level dashboard for AI coding assistant observability. Shows cost rollups, efficiency scores by developer, trend lines, and optimization insights across sessions. Requires AiToolCall, AiCodingTask, AiAntiPattern custom events and ai.* metrics.",
|
|
4
4
|
"permissions": "PUBLIC_READ_WRITE",
|
|
5
5
|
"pages": [
|
|
6
6
|
{
|
|
7
|
-
"name": "AI Coding Assistant
|
|
7
|
+
"name": "AI Coding Assistant — Team View",
|
|
8
8
|
"description": "Team summary, developer comparison, trends, and optimization insights",
|
|
9
9
|
"widgets": [
|
|
10
10
|
{
|
|
@@ -308,7 +308,7 @@
|
|
|
308
308
|
}
|
|
309
309
|
},
|
|
310
310
|
{
|
|
311
|
-
"title": "Skill
|
|
311
|
+
"title": "Skill Runs (7d)",
|
|
312
312
|
"layout": {
|
|
313
313
|
"column": 1,
|
|
314
314
|
"row": 13,
|
|
@@ -322,7 +322,7 @@
|
|
|
322
322
|
"nrqlQueries": [
|
|
323
323
|
{
|
|
324
324
|
"accountIds": [],
|
|
325
|
-
"query": "SELECT
|
|
325
|
+
"query": "SELECT uniqueCount(skill_run_id) AS 'Runs' FROM AiToolCall WHERE skill_name IS NOT NULL AND developer = {{developer}} AND session_id = {{session_id}} FACET skill_name SINCE 7 DAYS AGO LIMIT 20"
|
|
326
326
|
}
|
|
327
327
|
]
|
|
328
328
|
}
|
|
@@ -342,7 +342,7 @@
|
|
|
342
342
|
"nrqlQueries": [
|
|
343
343
|
{
|
|
344
344
|
"accountIds": [],
|
|
345
|
-
"query": "SELECT sum(duration_ms) / 3600000 AS 'Hours' FROM AiToolCall WHERE
|
|
345
|
+
"query": "SELECT filter(sum(duration_ms), WHERE tool != 'Agent') / 3600000 AS 'Hours' FROM AiToolCall WHERE skill_name IS NOT NULL AND developer = {{developer}} AND session_id = {{session_id}} FACET skill_name SINCE 7 DAYS AGO LIMIT 20"
|
|
346
346
|
}
|
|
347
347
|
]
|
|
348
348
|
}
|
|
@@ -362,7 +362,7 @@
|
|
|
362
362
|
"nrqlQueries": [
|
|
363
363
|
{
|
|
364
364
|
"accountIds": [],
|
|
365
|
-
"query": "SELECT sum(cost_usd) AS 'USD' FROM AiTurnCost WHERE
|
|
365
|
+
"query": "SELECT sum(cost_usd) AS 'USD' FROM AiTurnCost WHERE skill_name IS NOT NULL AND developer = {{developer}} AND session_id = {{session_id}} FACET skill_name SINCE 7 DAYS AGO LIMIT 20"
|
|
366
366
|
}
|
|
367
367
|
]
|
|
368
368
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append order in an O_APPEND file is the cross-process arbiter, so two
|
|
3
|
+
* racing claimants both read back the same winner without a lock. A message
|
|
4
|
+
* id maps to one file per generation, chosen by a hash of the id and the
|
|
5
|
+
* claim's wall clock rather than the turn's timestamp, so every line of a
|
|
6
|
+
* message is arbitrated in the same file. Each ledger instance claims under
|
|
7
|
+
* its own random claimant id, so two processes tailing the same session
|
|
8
|
+
* still resolve to one winner.
|
|
9
|
+
*/
|
|
10
|
+
export interface BillableTurn {
|
|
11
|
+
readonly messageId: string;
|
|
12
|
+
readonly timestampMs: number;
|
|
13
|
+
/** The source line had no timestamp, so the retention horizon cannot judge the turn. */
|
|
14
|
+
readonly undated?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface BilledTurnLedgerOptions {
|
|
17
|
+
readonly storagePath: string;
|
|
18
|
+
readonly retainDays?: number | null;
|
|
19
|
+
readonly now?: () => number;
|
|
20
|
+
}
|
|
21
|
+
export declare class BilledTurnLedger {
|
|
22
|
+
private readonly dir;
|
|
23
|
+
private readonly retainDays;
|
|
24
|
+
private readonly now;
|
|
25
|
+
private readonly claimant;
|
|
26
|
+
private readonly files;
|
|
27
|
+
private readonly undelivered;
|
|
28
|
+
constructor(options: BilledTurnLedgerOptions);
|
|
29
|
+
/**
|
|
30
|
+
* Appends, in order, the turns this session owes: the first line of each
|
|
31
|
+
* message this instance claimed first and has not delivered yet. A turn
|
|
32
|
+
* claimed first by any other instance, of any session, is already billed.
|
|
33
|
+
* When `append` throws, that turn and the rest stay owed to this session,
|
|
34
|
+
* so the next deliver() redoing the read appends them, and the error is
|
|
35
|
+
* rethrown.
|
|
36
|
+
*/
|
|
37
|
+
deliver<T extends BillableTurn>(sessionId: string, turns: readonly T[], append: (turn: T) => void): void;
|
|
38
|
+
private claim;
|
|
39
|
+
private claimantOf;
|
|
40
|
+
/** Generations on disk per shard. Drops the index of any file no longer listed. */
|
|
41
|
+
private listGenerations;
|
|
42
|
+
private syncFiles;
|
|
43
|
+
private syncFile;
|
|
44
|
+
}
|
|
45
|
+
export declare function pruneBilledTurns(storagePath: string, retainDays: number, nowMs: number): number;
|
|
46
|
+
//# sourceMappingURL=billed-turn-ledger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"billed-turn-ledger.d.ts","sourceRoot":"","sources":["../../src/hooks/billed-turn-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAmBH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wFAAwF;IACxF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAoBD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IAKtD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;gBAE7C,OAAO,EAAE,uBAAuB;IAM5C;;;;;;;OAOG;IACH,OAAO,CAAC,CAAC,SAAS,YAAY,EAC5B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,GACxB,IAAI;IAOP,OAAO,CAAC,KAAK;IAqEb,OAAO,CAAC,UAAU;IAQlB,mFAAmF;IACnF,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,QAAQ;CAmBjB;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAiB/F"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append order in an O_APPEND file is the cross-process arbiter, so two
|
|
3
|
+
* racing claimants both read back the same winner without a lock. A message
|
|
4
|
+
* id maps to one file per generation, chosen by a hash of the id and the
|
|
5
|
+
* claim's wall clock rather than the turn's timestamp, so every line of a
|
|
6
|
+
* message is arbitrated in the same file. Each ledger instance claims under
|
|
7
|
+
* its own random claimant id, so two processes tailing the same session
|
|
8
|
+
* still resolve to one winner.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
11
|
+
import { appendFileSync, closeSync, fstatSync, mkdirSync, openSync, readdirSync, readSync, unlinkSync, } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { createLogger } from '../shared/index.js';
|
|
14
|
+
const logger = createLogger('billed-turn-ledger');
|
|
15
|
+
const LEDGER_DIR = 'billed-turns';
|
|
16
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const GENERATION_MS = 7 * DAY_MS;
|
|
18
|
+
const LEDGER_FILE = /^g(\d+)-([0-9a-f])\.jsonl$/;
|
|
19
|
+
const TORN_LINE_TERMINATOR = '\n';
|
|
20
|
+
const HEAD_BYTES = 256;
|
|
21
|
+
const NEWLINE = 0x0a;
|
|
22
|
+
export class BilledTurnLedger {
|
|
23
|
+
dir;
|
|
24
|
+
retainDays;
|
|
25
|
+
now;
|
|
26
|
+
claimant = randomUUID();
|
|
27
|
+
files = new Map();
|
|
28
|
+
// The session each message this instance won is still owed to: a claim
|
|
29
|
+
// that threw after some of its lines landed, a turn whose append threw, or
|
|
30
|
+
// one whose claim line vanished before it was read back. Only this map lets
|
|
31
|
+
// the ledger's own claims out, and only to the session they were won for.
|
|
32
|
+
undelivered = new Map();
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.dir = join(options.storagePath, LEDGER_DIR);
|
|
35
|
+
this.retainDays = options.retainDays ?? null;
|
|
36
|
+
this.now = options.now ?? Date.now;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Appends, in order, the turns this session owes: the first line of each
|
|
40
|
+
* message this instance claimed first and has not delivered yet. A turn
|
|
41
|
+
* claimed first by any other instance, of any session, is already billed.
|
|
42
|
+
* When `append` throws, that turn and the rest stay owed to this session,
|
|
43
|
+
* so the next deliver() redoing the read appends them, and the error is
|
|
44
|
+
* rethrown.
|
|
45
|
+
*/
|
|
46
|
+
deliver(sessionId, turns, append) {
|
|
47
|
+
for (const turn of this.claim(sessionId, turns)) {
|
|
48
|
+
append(turn);
|
|
49
|
+
this.undelivered.delete(turn.messageId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
claim(sessionId, turns) {
|
|
53
|
+
const nowMs = this.now();
|
|
54
|
+
const horizon = horizonOf(nowMs, this.retainDays);
|
|
55
|
+
const firstLines = new Map();
|
|
56
|
+
let refused = 0;
|
|
57
|
+
for (const turn of turns) {
|
|
58
|
+
if (firstLines.has(turn.messageId))
|
|
59
|
+
continue;
|
|
60
|
+
const beyond = !turn.undated && Number.isFinite(turn.timestampMs) && turn.timestampMs < horizon;
|
|
61
|
+
if (beyond)
|
|
62
|
+
refused++;
|
|
63
|
+
firstLines.set(turn.messageId, beyond ? null : turn);
|
|
64
|
+
}
|
|
65
|
+
if (refused > 0) {
|
|
66
|
+
logger.debug('Refused turns older than the retention horizon', {
|
|
67
|
+
refused,
|
|
68
|
+
retainDays: this.retainDays,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const live = [...firstLines.values()].filter((turn) => turn !== null);
|
|
72
|
+
if (live.length === 0)
|
|
73
|
+
return [];
|
|
74
|
+
// Two processes either side of a generation boundary can both win a
|
|
75
|
+
// brand-new message claimed within the same few milliseconds.
|
|
76
|
+
const home = Math.floor(nowMs / GENERATION_MS);
|
|
77
|
+
const generations = this.listGenerations();
|
|
78
|
+
const filesOf = (messageId) => {
|
|
79
|
+
const shard = shardOf(messageId);
|
|
80
|
+
const gens = new Set([...(generations.get(shard) ?? []), home]);
|
|
81
|
+
return [...gens].sort((a, b) => a - b).map((gen) => fileName(gen, shard));
|
|
82
|
+
};
|
|
83
|
+
this.syncFiles(live.flatMap((turn) => filesOf(turn.messageId)));
|
|
84
|
+
const owed = [];
|
|
85
|
+
const linesByFile = new Map();
|
|
86
|
+
for (const turn of live) {
|
|
87
|
+
const id = turn.messageId;
|
|
88
|
+
const claimant = this.claimantOf(id, filesOf(id));
|
|
89
|
+
if (claimant === undefined) {
|
|
90
|
+
const file = fileName(home, shardOf(id));
|
|
91
|
+
const line = TORN_LINE_TERMINATOR +
|
|
92
|
+
JSON.stringify({ m: id, s: sessionId, c: this.claimant, t: nowMs });
|
|
93
|
+
linesByFile.set(file, (linesByFile.get(file) ?? '') + line);
|
|
94
|
+
this.undelivered.set(id, sessionId);
|
|
95
|
+
}
|
|
96
|
+
else if (claimant !== this.claimant) {
|
|
97
|
+
this.undelivered.delete(id);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
else if (this.undelivered.get(id) !== sessionId) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
owed.push(turn);
|
|
104
|
+
}
|
|
105
|
+
if (owed.length === 0)
|
|
106
|
+
return [];
|
|
107
|
+
if (linesByFile.size > 0)
|
|
108
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
109
|
+
for (const [file, lines] of linesByFile) {
|
|
110
|
+
appendFileSync(join(this.dir, file), lines + '\n', { mode: 0o600 });
|
|
111
|
+
}
|
|
112
|
+
this.syncFiles(owed.flatMap((turn) => filesOf(turn.messageId)));
|
|
113
|
+
return owed.filter((turn) => {
|
|
114
|
+
const claimant = this.claimantOf(turn.messageId, filesOf(turn.messageId));
|
|
115
|
+
if (claimant !== undefined && claimant !== this.claimant) {
|
|
116
|
+
this.undelivered.delete(turn.messageId);
|
|
117
|
+
}
|
|
118
|
+
return claimant === this.claimant;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
claimantOf(messageId, files) {
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const claimant = this.files.get(file)?.claimants.get(messageId);
|
|
124
|
+
if (claimant !== undefined)
|
|
125
|
+
return claimant;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
/** Generations on disk per shard. Drops the index of any file no longer listed. */
|
|
130
|
+
listGenerations() {
|
|
131
|
+
const listed = new Set();
|
|
132
|
+
const generations = new Map();
|
|
133
|
+
for (const name of readLedgerDir(this.dir)) {
|
|
134
|
+
const parsed = parseFileName(name);
|
|
135
|
+
if (parsed === null)
|
|
136
|
+
continue;
|
|
137
|
+
listed.add(name);
|
|
138
|
+
generations.set(parsed.shard, [...(generations.get(parsed.shard) ?? []), parsed.generation]);
|
|
139
|
+
}
|
|
140
|
+
for (const name of this.files.keys()) {
|
|
141
|
+
if (!listed.has(name))
|
|
142
|
+
this.files.delete(name);
|
|
143
|
+
}
|
|
144
|
+
return generations;
|
|
145
|
+
}
|
|
146
|
+
syncFiles(files) {
|
|
147
|
+
for (const file of new Set(files)) {
|
|
148
|
+
let fd;
|
|
149
|
+
try {
|
|
150
|
+
fd = openSync(join(this.dir, file), 'r');
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (err.code !== 'ENOENT')
|
|
154
|
+
throw err;
|
|
155
|
+
this.files.delete(file);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
this.syncFile(file, fd);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
closeSync(fd);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
syncFile(file, fd) {
|
|
167
|
+
const { size } = fstatSync(fd);
|
|
168
|
+
const head = readAt(fd, 0, Math.min(size, HEAD_BYTES));
|
|
169
|
+
let index = this.files.get(file);
|
|
170
|
+
if (index === undefined || size < index.offset || !onePrefixesOther(head, index.head)) {
|
|
171
|
+
index = { head, offset: 0, partial: Buffer.alloc(0), claimants: new Map() };
|
|
172
|
+
this.files.set(file, index);
|
|
173
|
+
}
|
|
174
|
+
else if (head.length > index.head.length) {
|
|
175
|
+
index.head = head;
|
|
176
|
+
}
|
|
177
|
+
if (size <= index.offset)
|
|
178
|
+
return;
|
|
179
|
+
const tail = readAt(fd, index.offset, size);
|
|
180
|
+
const bytes = Buffer.concat([index.partial, tail]);
|
|
181
|
+
const end = bytes.lastIndexOf(NEWLINE) + 1;
|
|
182
|
+
foldLines(bytes.subarray(0, end), index.claimants, file);
|
|
183
|
+
index.partial = Buffer.from(bytes.subarray(end));
|
|
184
|
+
index.offset += tail.length;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export function pruneBilledTurns(storagePath, retainDays, nowMs) {
|
|
188
|
+
const dir = join(storagePath, LEDGER_DIR);
|
|
189
|
+
const horizon = horizonOf(nowMs, retainDays);
|
|
190
|
+
let deleted = 0;
|
|
191
|
+
for (const name of readLedgerDir(dir)) {
|
|
192
|
+
const parsed = parseFileName(name);
|
|
193
|
+
if (parsed === null || (parsed.generation + 1) * GENERATION_MS > horizon)
|
|
194
|
+
continue;
|
|
195
|
+
try {
|
|
196
|
+
unlinkSync(join(dir, name));
|
|
197
|
+
deleted++;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
if (err.code !== 'ENOENT') {
|
|
201
|
+
logger.warn('Failed to prune billed-turn ledger file', { file: name, error: String(err) });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return deleted;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Turns dated before the horizon are refused and generations ending by it
|
|
209
|
+
* are pruned. A turn is claimed no earlier than its own timestamp, so a
|
|
210
|
+
* pruned generation only ever held claims for turns the horizon refuses.
|
|
211
|
+
*/
|
|
212
|
+
function horizonOf(nowMs, retainDays) {
|
|
213
|
+
if (retainDays === null || !(retainDays > 0))
|
|
214
|
+
return -Infinity;
|
|
215
|
+
return nowMs - retainDays * DAY_MS;
|
|
216
|
+
}
|
|
217
|
+
function shardOf(messageId) {
|
|
218
|
+
return createHash('sha256').update(messageId).digest('hex')[0];
|
|
219
|
+
}
|
|
220
|
+
function fileName(generation, shard) {
|
|
221
|
+
return `g${generation}-${shard}.jsonl`;
|
|
222
|
+
}
|
|
223
|
+
function parseFileName(name) {
|
|
224
|
+
const match = LEDGER_FILE.exec(name);
|
|
225
|
+
return match ? { generation: Number(match[1]), shard: match[2] } : null;
|
|
226
|
+
}
|
|
227
|
+
function readLedgerDir(dir) {
|
|
228
|
+
try {
|
|
229
|
+
return readdirSync(dir);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
if (err.code === 'ENOENT')
|
|
233
|
+
return [];
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function onePrefixesOther(a, b) {
|
|
238
|
+
const n = Math.min(a.length, b.length);
|
|
239
|
+
return a.subarray(0, n).equals(b.subarray(0, n));
|
|
240
|
+
}
|
|
241
|
+
function readAt(fd, start, end) {
|
|
242
|
+
const buf = Buffer.allocUnsafe(Math.max(0, end - start));
|
|
243
|
+
let read = 0;
|
|
244
|
+
while (read < buf.length) {
|
|
245
|
+
const n = readSync(fd, buf, read, buf.length - read, start + read);
|
|
246
|
+
if (n === 0)
|
|
247
|
+
break;
|
|
248
|
+
read += n;
|
|
249
|
+
}
|
|
250
|
+
return buf.subarray(0, read);
|
|
251
|
+
}
|
|
252
|
+
function foldLines(bytes, claimants, file) {
|
|
253
|
+
let start = 0;
|
|
254
|
+
while (start < bytes.length) {
|
|
255
|
+
const end = bytes.indexOf(NEWLINE, start);
|
|
256
|
+
const line = bytes.subarray(start, end);
|
|
257
|
+
start = end + 1;
|
|
258
|
+
if (line.length === 0)
|
|
259
|
+
continue;
|
|
260
|
+
let parsed;
|
|
261
|
+
try {
|
|
262
|
+
parsed = JSON.parse(line.toString('utf-8'));
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
logger.debug('Skipped a torn billed-turn ledger line', { file, bytes: line.length });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (typeof parsed.m !== 'string')
|
|
269
|
+
continue;
|
|
270
|
+
if (!claimants.has(parsed.m))
|
|
271
|
+
claimants.set(parsed.m, typeof parsed.c === 'string' ? parsed.c : '');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=billed-turn-ledger.js.map
|