@kontourai/flow-agents 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +41 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +54 -1
  34. package/build/src/lib/flow-resolver.js +112 -5
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +117 -4
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -2,14 +2,78 @@
2
2
  # config.sh — Load telemetry configuration with defaults
3
3
 
4
4
  TELEMETRY_DIR="${TELEMETRY_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
5
- TELEMETRY_CONFIG_FILE="${TELEMETRY_CONFIG_FILE:-${TELEMETRY_DIR}/telemetry.conf}"
5
+ # TELEMETRY_DIR is <workspace>/scripts/telemetry, so the workspace root is
6
+ # two levels up.
7
+ TELEMETRY_WORKSPACE_ROOT="${TELEMETRY_WORKSPACE_ROOT:-$(cd "${TELEMETRY_DIR}/../.." && pwd)}"
8
+
9
+ # A workspace/global conf only counts as operator-created (and thus trusted
10
+ # with credentials) if it is mode 600 and owned by the current user.
11
+ # install-console-config.sh always chmod 600s the conf it writes; git can
12
+ # only store 644/755 (never 600), so this gate rejects a conf smuggled in via
13
+ # clone/tarball/PR/supply-chain even if it happens to land at the expected
14
+ # path. Portable across macOS/BSD stat (-f) and GNU/Linux stat (-c).
15
+ telemetry_conf_trusted() {
16
+ local file="$1" mode owner
17
+ # Reject symlinks outright: stat below reports the link's own mode/owner
18
+ # (lstat semantics, no -L), but the config file is later read by following
19
+ # the link, so a symlink lets `chmod -h 600 <link>` pass this gate while
20
+ # the read lands on a different, untrusted target. Never trust a link.
21
+ [[ -L "$file" ]] && return 1
22
+ [[ -f "$file" ]] || return 1
23
+ mode=$(stat -f '%Lp' "$file" 2>/dev/null) || mode=$(stat -c '%a' "$file" 2>/dev/null)
24
+ owner=$(stat -f '%u' "$file" 2>/dev/null) || owner=$(stat -c '%u' "$file" 2>/dev/null)
25
+ [[ "$mode" == "600" && -n "$owner" && "$owner" == "$(id -u)" ]]
26
+ }
27
+
28
+ # A candidate that exists on disk but fails telemetry_conf_trusted would
29
+ # otherwise be silently ignored (telemetry stays fail-open with no signal to
30
+ # the operator). Print a one-time, visible warning naming the path and the
31
+ # fix whenever this happens. Never warn when the file is simply absent, and
32
+ # never change which file TELEMETRY_CONFIG_FILE resolves to below -- this is
33
+ # purely additive/observational.
34
+ telemetry_conf_warn_untrusted() {
35
+ local file="$1"
36
+ # `-e` catches a regular file; `-L` additionally catches a symlink that
37
+ # `-e` would miss (a symlink to a directory, or a dangling symlink) --
38
+ # both of those are rejected by telemetry_conf_trusted's `-L` check above
39
+ # but must still surface this warning, not silently emit none.
40
+ [[ -e "$file" || -L "$file" ]] || return 0
41
+ printf 'warning: config.sh: %s exists but is not trusted (must be mode 600, owned by the current user, and not a symlink); telemetry will ignore it and stay fail-open. Fix with: chmod 600 "%s"\n' "$file" "$file" >&2
42
+ }
43
+
44
+ # Config file resolution order: (1) explicit TELEMETRY_CONFIG_FILE env always
45
+ # wins; (2) a gitignored per-workspace conf at .kontourai/telemetry-console.conf
46
+ # if present and operator-trusted (project-specific override); (3) a
47
+ # gitignored user-global conf at ~/.flow-agents/telemetry-console.conf if
48
+ # present and operator-trusted, matching the existing ~/.flow-agents
49
+ # install-home convention (see scripts/discover-agents.sh,
50
+ # scripts/context-budget/budget-scan.sh) so one machine-wide install can carry owner
51
+ # credentials without per-workspace wiring; (4) the shipped telemetry.conf
52
+ # default. The shipped scripts/telemetry/telemetry.conf is tracked and ships
53
+ # into every packaged dist bundle, so it must never carry owner credentials.
54
+ # A candidate that exists but fails the trust gate falls through silently
55
+ # (telemetry stays fail-open).
56
+ TELEMETRY_LOCAL_CONFIG_FILE="${TELEMETRY_WORKSPACE_ROOT}/.kontourai/telemetry-console.conf"
57
+ TELEMETRY_GLOBAL_CONFIG_FILE="${HOME:-}/.flow-agents/telemetry-console.conf"
58
+ if [[ -n "${TELEMETRY_CONFIG_FILE:-}" ]]; then
59
+ :
60
+ elif telemetry_conf_trusted "$TELEMETRY_LOCAL_CONFIG_FILE"; then
61
+ TELEMETRY_CONFIG_FILE="$TELEMETRY_LOCAL_CONFIG_FILE"
62
+ else
63
+ telemetry_conf_warn_untrusted "$TELEMETRY_LOCAL_CONFIG_FILE"
64
+ if [[ -n "${HOME:-}" ]] && telemetry_conf_trusted "$TELEMETRY_GLOBAL_CONFIG_FILE"; then
65
+ TELEMETRY_CONFIG_FILE="$TELEMETRY_GLOBAL_CONFIG_FILE"
66
+ else
67
+ [[ -n "${HOME:-}" ]] && telemetry_conf_warn_untrusted "$TELEMETRY_GLOBAL_CONFIG_FILE"
68
+ TELEMETRY_CONFIG_FILE="${TELEMETRY_DIR}/telemetry.conf"
69
+ fi
70
+ fi
6
71
 
7
72
  # Defaults
8
73
  TELEMETRY_ENABLED="${TELEMETRY_ENABLED:-true}"
9
- # TELEMETRY_DIR is <workspace>/scripts/telemetry, so the workspace root is
10
- # two levels up. Local runtime telemetry defaults under .kontourai/telemetry;
11
- # explicit TELEMETRY_DATA_DIR still wins.
12
- TELEMETRY_DATA_DIR="${TELEMETRY_DATA_DIR:-$(cd "${TELEMETRY_DIR}/../.." && pwd)/.kontourai/telemetry}"
74
+ # Local runtime telemetry defaults under .kontourai/telemetry; explicit
75
+ # TELEMETRY_DATA_DIR still wins.
76
+ TELEMETRY_DATA_DIR="${TELEMETRY_DATA_DIR:-${TELEMETRY_WORKSPACE_ROOT}/.kontourai/telemetry}"
13
77
  TELEMETRY_SESSION_DIR="${TELEMETRY_SESSION_DIR:-${TELEMETRY_DATA_DIR}/sessions}"
14
78
  TELEMETRY_ENRICH_SYSTEM="${TELEMETRY_ENRICH_SYSTEM:-true}"
15
79
  TELEMETRY_ENRICH_WORKSPACE="${TELEMETRY_ENRICH_WORKSPACE:-true}"
@@ -37,9 +101,16 @@ CONSOLE_TELEMETRY_URL="${CONSOLE_TELEMETRY_URL:-${CONSOLE_URL:-}}"
37
101
  CONSOLE_TELEMETRY_ENDPOINT_URL="${CONSOLE_TELEMETRY_ENDPOINT_URL:-}"
38
102
  CONSOLE_TELEMETRY_TOKEN="${CONSOLE_TELEMETRY_TOKEN:-${CONSOLE_AUTH_TOKEN:-}}"
39
103
  CONSOLE_TENANT_ID="${CONSOLE_TENANT_ID:-}"
104
+ # Economics relay (#469): a caller-pre-set env var is preserved as the starting point; the
105
+ # config-file key (parsed below) can override it, and if no key is present at all the
106
+ # default-on rule below turns it on once a console telemetry sink resolves.
107
+ FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY="${FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY:-}"
108
+ FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="${FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL:-}"
109
+ # Set (non-empty) only when the config file carries an explicit console_economics_relay key —
110
+ # distinguishes "operator said 0/1" from "key absent" for the default-on rule below.
111
+ console_economics_relay_raw=""
40
112
  # Pricing registry source (consumed by lib/pricing.sh). Explicit file/URL win;
41
- # otherwise the URL is derived from the console below so all runtimes read one
42
- # live pricing source. Falls back to the bundled pricing.json offline.
113
+ # otherwise lib/pricing.sh uses the bundled pricing.json offline.
43
114
  TELEMETRY_PRICING_FILE="${TELEMETRY_PRICING_FILE:-${FLOW_AGENTS_PRICING_FILE:-}}"
44
115
  TELEMETRY_PRICING_URL="${TELEMETRY_PRICING_URL:-${FLOW_AGENTS_PRICING_URL:-}}"
45
116
 
@@ -82,6 +153,17 @@ if [[ -f "$TELEMETRY_CONFIG_FILE" ]]; then
82
153
  console_telemetry_token) CONSOLE_TELEMETRY_TOKEN="$value" ;;
83
154
  console_tenant_id) CONSOLE_TENANT_ID="$value" ;;
84
155
  console_telemetry_redact) CONSOLE_TELEMETRY_REDACT="$value" ;;
156
+ console_economics_relay)
157
+ case "$(echo "$value" | tr '[:upper:]' '[:lower:]')" in
158
+ 1|true|yes|on) console_economics_relay_raw="1" ;;
159
+ 0|false|no|off) console_economics_relay_raw="0" ;;
160
+ *)
161
+ printf 'warning: config.sh: unrecognized console_economics_relay value %q; treating as off\n' "$value" >&2
162
+ console_economics_relay_raw="$value"
163
+ ;;
164
+ esac
165
+ ;;
166
+ console_economics_endpoint_url) FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$value" ;;
85
167
  console_pricing_url) TELEMETRY_PRICING_URL="$value" ;;
86
168
  pricing_url) TELEMETRY_PRICING_URL="$value" ;;
87
169
  pricing_file) TELEMETRY_PRICING_FILE="$value" ;;
@@ -92,12 +174,20 @@ fi
92
174
 
93
175
  CONSOLE_TELEMETRY_REDACT="${CONSOLE_TELEMETRY_REDACT:-${TELEMETRY_CHANNEL_ANALYTICS_REDACT}}"
94
176
 
95
- # Derive the live pricing source from the console when not set explicitly, the
96
- # same way the transport derives /api/telemetry/records. One live source for
97
- # bash/Python/TS runtimes; lib/pricing.sh caches it and falls back to bundled.
98
- if [[ -z "${TELEMETRY_PRICING_URL:-}" && -n "${CONSOLE_TELEMETRY_URL:-}" ]]; then
99
- TELEMETRY_PRICING_URL="${CONSOLE_TELEMETRY_URL%/}/api/telemetry/pricing"
177
+ # Economics relay default-on rule (#469, opt-out not silent-off): an explicit
178
+ # console_economics_relay config key always wins. Otherwise, once a console telemetry sink is
179
+ # resolved (console_telemetry_url/console_telemetry_endpoint_url non-empty) the relay defaults ON
180
+ # unless a caller already pre-set FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY in the environment, which is
181
+ # left untouched. economics-record.sh's own opt-in gate (unchanged) reads this exact variable.
182
+ if [[ -n "$console_economics_relay_raw" ]]; then
183
+ FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY="$console_economics_relay_raw"
184
+ elif [[ -z "$FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY" \
185
+ && ( -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_TELEMETRY_ENDPOINT_URL:-}" ) ]]; then
186
+ FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
100
187
  fi
101
188
 
189
+ # Pricing URL is explicit-only (env or config). Do not derive the console
190
+ # pricing endpoint by default; the bundled registry is the reliable offline floor.
191
+
102
192
  # Ensure directories exist
103
193
  mkdir -p "$TELEMETRY_DATA_DIR" "$TELEMETRY_SESSION_DIR" 2>/dev/null
@@ -10,6 +10,11 @@
10
10
 
11
11
  PRICING_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
 
13
+ pricing_registry_valid_json() {
14
+ command -v jq >/dev/null 2>&1 || return 1
15
+ jq -e '.current_version and .versions' >/dev/null 2>&1
16
+ }
17
+
13
18
  # Echo the raw registry JSON. Returns non-zero if nothing resolvable.
14
19
  pricing_registry() {
15
20
  local f="${TELEMETRY_PRICING_FILE:-${FLOW_AGENTS_PRICING_FILE:-}}"
@@ -21,18 +26,21 @@ pricing_registry() {
21
26
  local ttl="${TELEMETRY_PRICING_TTL_SEC:-3600}"
22
27
  if [[ -f "$cache" ]]; then
23
28
  local mtime now age
24
- mtime=$(stat -f %m "$cache" 2>/dev/null || stat -c %Y "$cache" 2>/dev/null || echo 0)
29
+ mtime=$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null || echo 0)
30
+ [[ "$mtime" =~ ^[0-9]+$ ]] || mtime=0
25
31
  now=$(date +%s)
26
32
  age=$(( now - mtime ))
27
- if [[ "$age" -lt "$ttl" ]]; then cat "$cache"; return 0; fi
33
+ if [[ "$age" -lt "$ttl" ]] && pricing_registry_valid_json < "$cache"; then cat "$cache"; return 0; fi
28
34
  fi
29
35
  if curl -fsS --max-time 5 "$url" -o "${cache}.tmp" 2>/dev/null && [[ -s "${cache}.tmp" ]]; then
30
- mv "${cache}.tmp" "$cache"
31
- cat "$cache"
32
- return 0
36
+ if pricing_registry_valid_json < "${cache}.tmp"; then
37
+ mv "${cache}.tmp" "$cache"
38
+ cat "$cache"
39
+ return 0
40
+ fi
33
41
  fi
34
42
  rm -f "${cache}.tmp" 2>/dev/null
35
- [[ -f "$cache" ]] && { cat "$cache"; return 0; } # stale cache beats nothing
43
+ [[ -f "$cache" ]] && pricing_registry_valid_json < "$cache" && { cat "$cache"; return 0; }
36
44
  fi
37
45
 
38
46
  local bundled
@@ -60,4 +60,7 @@ session_end() {
60
60
 
61
61
  session_cleanup() {
62
62
  find "${TELEMETRY_SESSION_DIR}" -name "*.session" -mtime +1 -delete 2>/dev/null || true
63
+ # Bound the per-cwd project-label cache (see console_project_label) so a project rename
64
+ # (package.json name / git remote) self-heals within the same window instead of caching forever.
65
+ find "${TELEMETRY_SESSION_DIR}" -name "project-label.*" -mtime +1 -delete 2>/dev/null || true
63
66
  }
@@ -46,26 +46,38 @@ console_telemetry_timeout_seconds() {
46
46
  fi
47
47
  }
48
48
 
49
- console_telemetry_emit() {
50
- local event="$1"
51
- local endpoint_url
52
- endpoint_url=$(console_telemetry_endpoint_url)
49
+ # console_post_json <endpoint_url> <body> [connect_timeout] [max_time] [tmp_dir]
50
+ # Shared best-effort JSON POST to a Console records endpoint. BOTH the telemetry mirror
51
+ # (console_telemetry_emit) and the liveness relay (#295, scripts/liveness/relay.sh) go through this
52
+ # ONE core so endpoint-allow, auth (Bearer + tenant), timeouts, temp-file handling, and the detached
53
+ # fire can never drift between the two paths (the #356 shared-not-forked discipline). Reads
54
+ # CONSOLE_TELEMETRY_TOKEN / CONSOLE_TENANT_ID from the environment. Never blocks or fails the caller.
55
+ console_post_json() {
56
+ local endpoint_url="$1"
57
+ local body="$2"
58
+ local connect_timeout max_time tmp_dir curl_config curl_body
53
59
  [[ -z "$endpoint_url" ]] && return
54
- console_telemetry_endpoint_allowed "$endpoint_url" || return
55
-
56
- local processed_event
57
- processed_event=$(redact_event "$event" "${CONSOLE_TELEMETRY_REDACT:-${TELEMETRY_CHANNEL_ANALYTICS_REDACT:-}}")
58
-
59
- local curl_config curl_body connect_timeout max_time
60
- connect_timeout=$(console_telemetry_timeout_seconds "${CONSOLE_TELEMETRY_CONNECT_TIMEOUT_SECONDS:-2}" 2 30)
61
- max_time=$(console_telemetry_timeout_seconds "${CONSOLE_TELEMETRY_MAX_TIME_SECONDS:-5}" 5 60)
62
- curl_config=$(mktemp "${TELEMETRY_SESSION_DIR}/console-curl.XXXXXX") || return
63
- curl_body=$(mktemp "${TELEMETRY_SESSION_DIR}/console-body.XXXXXX") || {
60
+ if ! console_telemetry_endpoint_allowed "$endpoint_url"; then
61
+ # Dropped silently before today's change (no signal that the event never
62
+ # left the machine). Warn once per shell process (guarded by a plain,
63
+ # non-local var so it survives across repeated console_post_json calls
64
+ # within the same sourced shell) without changing the drop itself.
65
+ if [[ -z "${_CONSOLE_TELEMETRY_ENDPOINT_WARNED:-}" ]]; then
66
+ printf 'warning: transport.sh: console endpoint dropped by the allowlist (must be https://, or http://localhost|127.0.0.1): %s\n' "$endpoint_url" >&2
67
+ _CONSOLE_TELEMETRY_ENDPOINT_WARNED=1
68
+ fi
69
+ return
70
+ fi
71
+ connect_timeout=$(console_telemetry_timeout_seconds "${3:-2}" 2 30)
72
+ max_time=$(console_telemetry_timeout_seconds "${4:-5}" 5 60)
73
+ tmp_dir="${5:-${TELEMETRY_SESSION_DIR:-${TMPDIR:-/tmp}}}"
74
+ curl_config=$(mktemp "${tmp_dir%/}/console-curl.XXXXXX") || return
75
+ curl_body=$(mktemp "${tmp_dir%/}/console-body.XXXXXX") || {
64
76
  rm -f "$curl_config"
65
77
  return
66
78
  }
67
79
  chmod 600 "$curl_config" "$curl_body" 2>/dev/null
68
- printf '%s' "$processed_event" > "$curl_body" || {
80
+ printf '%s' "$body" > "$curl_body" || {
69
81
  rm -f "$curl_config" "$curl_body"
70
82
  return
71
83
  }
@@ -94,6 +106,112 @@ console_telemetry_emit() {
94
106
  ) &
95
107
  }
96
108
 
109
+ # Derive a coarse, path-free project label for console attribution, most-stable-first so the SAME
110
+ # project resolves to the SAME label across developers and machines (folder names differ between
111
+ # clones and worktrees; the project manifest and git remote do not). Precedence:
112
+ # 1. FLOW_AGENTS_PROJECT — explicit operator override, always wins
113
+ # 2. nearest package.json name — walking up from cwd (monorepo-granular, committed => consistent)
114
+ # 3. git remote origin org/repo — repo-level identity, stable across clones
115
+ # 4. git toplevel dir basename — repo dir even from a worktree/subdir
116
+ # 5. cwd basename — last resort
117
+ # Path-free by construction (never the full local path). Cached per cwd under the telemetry data
118
+ # dir so the git/manifest reads run once per project, not per event; session_cleanup bounds the
119
+ # cache lifetime so a project rename (package.json name / git remote) self-heals within a day.
120
+ # Failure signals via empty output; the sole caller wraps the call as `$(...) || proj=""` so even if
121
+ # an internal command fails under `set -e`, telemetry is relayed unchanged (see
122
+ # console_telemetry_emit). Do not call this unwrapped from a `set -e` context.
123
+ console_project_label() {
124
+ local cwd="$1"
125
+ [[ -z "$cwd" || ! -d "$cwd" ]] && return 0
126
+ [[ -n "${FLOW_AGENTS_PROJECT:-}" ]] && { printf '%s' "$FLOW_AGENTS_PROJECT"; return 0; }
127
+
128
+ local cache="" key
129
+ if [[ -n "${TELEMETRY_SESSION_DIR:-}" && -d "${TELEMETRY_SESSION_DIR:-}" ]]; then
130
+ key=$(printf '%s' "$cwd" | cksum | cut -d' ' -f1)
131
+ cache="${TELEMETRY_SESSION_DIR%/}/project-label.${key}"
132
+ [[ -s "$cache" ]] && { cat "$cache"; return 0; }
133
+ fi
134
+
135
+ local label="" dir name url top after path repo rest org
136
+ dir="$cwd"
137
+ while [[ -n "$dir" && "$dir" != "/" ]]; do
138
+ if [[ -f "$dir/package.json" ]]; then
139
+ name=$(jq -r '.name // empty' "$dir/package.json" 2>/dev/null) || name=""
140
+ [[ -n "$name" ]] && { label="$name"; break; }
141
+ fi
142
+ dir=$(dirname "$dir")
143
+ done
144
+ if [[ -z "$label" ]]; then
145
+ url=$(git -C "$cwd" config --get remote.origin.url 2>/dev/null) || url=""
146
+ if [[ -n "$url" ]]; then
147
+ # Reduce a git remote to a coarse, host-free org/repo. Strip scheme+host STRUCTURALLY (not by
148
+ # a dot-in-host heuristic) so a self-hosted or dot-less host, a port, or a user@ prefix can
149
+ # never leak into the label; a remote with no org tier (single path segment) falls through.
150
+ url="${url%/}"; url="${url%.git}"
151
+ path=""
152
+ if [[ "$url" == *"://"* ]]; then
153
+ # Only network VCS schemes carry an org/repo path; file:// and other local schemes fall through.
154
+ if [[ "$url" =~ ^(https?|ssh|git|ftps?):// ]]; then
155
+ after="${url#*://}" # [user@]host[:port]/org/repo
156
+ [[ "$after" == */* ]] && path="${after#*/}" # drop host[:port] (and any user@), keep path
157
+ fi
158
+ elif [[ "$url" == *:* && "${url%%:*}" != */* ]]; then
159
+ path="${url#*:}" # scp form [user@]host:org/repo
160
+ elif [[ "$url" != /* ]]; then
161
+ path="$url" # bare relative org/repo shorthand (not a local abs path)
162
+ fi
163
+ if [[ "$path" == */* ]]; then
164
+ repo="${path##*/}"; rest="${path%/*}"; org="${rest##*/}" # last two path segments
165
+ [[ -n "$org" && -n "$repo" ]] && label="$org/$repo"
166
+ fi
167
+ fi
168
+ fi
169
+ if [[ -z "$label" ]]; then
170
+ top=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || top=""
171
+ [[ -n "$top" ]] && label=$(basename "$top")
172
+ fi
173
+ [[ -z "$label" ]] && label=$(basename "$cwd")
174
+
175
+ if [[ -n "$cache" ]]; then
176
+ printf '%s' "$label" > "${cache}.tmp.$$" 2>/dev/null && mv "${cache}.tmp.$$" "$cache" 2>/dev/null
177
+ fi
178
+ printf '%s' "$label"
179
+ }
180
+
181
+ console_telemetry_emit() {
182
+ local event="$1"
183
+ local endpoint_url
184
+ endpoint_url=$(console_telemetry_endpoint_url)
185
+ [[ -z "$endpoint_url" ]] && return
186
+
187
+ # Attribution: stamp a coarse, path-free project label (see console_project_label) before redaction
188
+ # so the console buckets by project consistently across developers. The full context.cwd is still
189
+ # redacted below — only the label leaves the machine. Every substitution is `|| var=""`-guarded so
190
+ # that even under `set -e` any failure (bad JSON, missing cwd, no git) relays the event unchanged.
191
+ local ev_cwd proj labeled_event
192
+ ev_cwd=$(printf '%s' "$event" | jq -r '.context.cwd // empty' 2>/dev/null) || ev_cwd=""
193
+ if [[ -n "$ev_cwd" ]]; then
194
+ proj=$(console_project_label "$ev_cwd" 2>/dev/null) || proj=""
195
+ if [[ -n "$proj" ]]; then
196
+ labeled_event=$(printf '%s' "$event" | jq -c --arg p "$proj" '
197
+ if ((.context.project // "") | length) == 0 then .context.project = $p else . end' 2>/dev/null) || labeled_event=""
198
+ [[ -n "$labeled_event" ]] && event="$labeled_event"
199
+ fi
200
+ fi
201
+
202
+ local processed_event
203
+ processed_event=$(redact_event "$event" "${CONSOLE_TELEMETRY_REDACT:-${TELEMETRY_CHANNEL_ANALYTICS_REDACT:-}}")
204
+
205
+ # Delegate the endpoint-allow gate, auth, timeouts, temp files, and detached POST to the shared
206
+ # core. Timeouts/tmp-dir are passed explicitly so telemetry behavior is byte-for-byte unchanged.
207
+ console_post_json \
208
+ "$endpoint_url" \
209
+ "$processed_event" \
210
+ "${CONSOLE_TELEMETRY_CONNECT_TIMEOUT_SECONDS:-2}" \
211
+ "${CONSOLE_TELEMETRY_MAX_TIME_SECONDS:-5}" \
212
+ "${TELEMETRY_SESSION_DIR:-}"
213
+ }
214
+
97
215
  transport_emit() {
98
216
  local event="$1"
99
217
  [[ -z "$event" ]] && return
@@ -20,18 +20,49 @@ usage_get_model() {
20
20
  echo "unknown"
21
21
  }
22
22
 
23
+ # Given the JSON object produced by usage_parse_transcript (has a top-level
24
+ # `by_model[]` array), return the runtime-agnostic session model: the model
25
+ # with the most total tokens (input+output+cache_creation+cache_read). This
26
+ # lets any runtime that exposes a transcript (Claude Code, Codex, ...) resolve
27
+ # a real model name instead of the kiro-only ~/.kiro/agents lookup, which
28
+ # never matches non-kiro agent names (e.g. Claude Code's fixed "dev" hook
29
+ # arg) and falls through to "unknown". Returns empty string when $1 is
30
+ # null/empty or has no by_model entries, so the caller falls back to
31
+ # usage_get_model().
32
+ usage_model_from_transcript_usage() {
33
+ local transcript_usage="$1"
34
+ [[ -z "$transcript_usage" || "$transcript_usage" == "null" ]] && { echo ""; return; }
35
+ echo "$transcript_usage" | jq -r '
36
+ (.by_model // [])
37
+ | map({model, total: ((.input_tokens // 0) + (.output_tokens // 0) + (.cache_creation_input_tokens // 0) + (.cache_read_input_tokens // 0))})
38
+ | sort_by([-.total, .model])
39
+ | .[0].model // empty
40
+ ' 2>/dev/null
41
+ }
42
+
23
43
  # Count tool invocations for a session
24
44
  usage_count_tool_calls() {
25
45
  local session_id="$1" jsonl_path="$2"
26
- [[ ! -f "$jsonl_path" ]] && echo 0 && return
27
- grep -c "\"session_id\":\"${session_id}\".*\"event_type\":\"tool.invoke\"" "$jsonl_path" 2>/dev/null || echo 0
46
+ [[ ! -f "$jsonl_path" ]] && { echo 0; return; }
47
+ # grep -c prints "0" (not empty) on zero matches but still exits non-zero,
48
+ # so `grep -c ... || echo 0` double-emits "0\n0" here — that malformed
49
+ # value then breaks the caller's `jq --argjson tc "$tool_count"`, silently
50
+ # discarding the *entire* session.usage event whenever a session has no
51
+ # prior tool.invoke lines yet (a common, not-rare case). Capture the count
52
+ # first and only fall back when it's genuinely empty.
53
+ local count
54
+ count=$(grep -c "\"session_id\":\"${session_id}\".*\"event_type\":\"tool.invoke\"" "$jsonl_path" 2>/dev/null)
55
+ echo "${count:-0}"
28
56
  }
29
57
 
30
58
  # Count subagent delegations for a session
31
59
  usage_count_delegations() {
32
60
  local session_id="$1" jsonl_path="$2"
33
- [[ ! -f "$jsonl_path" ]] && echo 0 && return
34
- grep -c "\"session_id\":\"${session_id}\".*\"event_type\":\"agent.delegate\"" "$jsonl_path" 2>/dev/null || echo 0
61
+ [[ ! -f "$jsonl_path" ]] && { echo 0; return; }
62
+ # See usage_count_tool_calls above for why this can't be `grep -c ... || echo 0`.
63
+ local count
64
+ count=$(grep -c "\"session_id\":\"${session_id}\".*\"event_type\":\"agent.delegate\"" "$jsonl_path" 2>/dev/null)
65
+ echo "${count:-0}"
35
66
  }
36
67
 
37
68
  # Parse a runtime transcript (JSONL) into real per-model token + cost usage.
@@ -47,8 +78,13 @@ usage_count_delegations() {
47
78
  # estimated_cost_usd} ],
48
79
  # input_tokens, output_tokens, cache_creation_input_tokens,
49
80
  # cache_read_input_tokens, estimated_cost_usd, pricing_version }
50
- # Prints nothing (non-zero) when the transcript is missing/unparseable so the
51
- # caller can fall back to null usage. Never blocks agent work.
81
+ # Tokens are source-of-truth and survive independently of pricing: if the
82
+ # pricing registry is unavailable (or an explicit version arg doesn't exist in
83
+ # it), by_model[]/input_tokens/output_tokens/etc. are still emitted — only
84
+ # estimated_cost_usd and pricing_version degrade to null (the console
85
+ # recomputes cost authoritatively from tokens). Prints nothing (non-zero) only
86
+ # when the transcript itself is missing/empty/unreadable or truly carries zero
87
+ # usage, so the caller can fall back to null usage. Never blocks agent work.
52
88
  # Expected transcript usage path (Claude Code / Anthropic usage object). Bumped
53
89
  # if the on-disk schema changes so drift is logged rather than silently zeroed.
54
90
  USAGE_TRANSCRIPT_SCHEMA="message.usage.input_tokens"
@@ -65,21 +101,68 @@ usage_log_drift() {
65
101
  fi
66
102
  }
67
103
 
104
+ # Opt-in persistent diagnostics for usage_parse_transcript's no-usage/degraded
105
+ # outcomes (gated by TELEMETRY_USAGE_DEBUG=1 so it never fires by default).
106
+ # Goes to TELEMETRY_DRIFT_LOG if set, else stderr. Never fatal, never blocks —
107
+ # purely so intermittent live "session.usage has null tokens" reports can be
108
+ # distinguished (empty transcript_path vs missing file vs no pricing registry
109
+ # vs a real zero-token parse) instead of being an unexplained black box.
110
+ usage_log_debug() {
111
+ [[ "${TELEMETRY_USAGE_DEBUG:-}" == "1" ]] || return 0
112
+ local reason="$1"
113
+ local msg="[telemetry] usage_parse_transcript: ${reason}"
114
+ if [[ -n "${TELEMETRY_DRIFT_LOG:-}" ]]; then
115
+ echo "$msg" >> "${TELEMETRY_DRIFT_LOG}" 2>/dev/null || echo "$msg" >&2
116
+ else
117
+ echo "$msg" >&2
118
+ fi
119
+ }
120
+
68
121
  usage_parse_transcript() {
69
122
  local transcript="$1" version="${2:-}"
70
- [[ -z "$transcript" || ! -f "$transcript" ]] && return 1
71
- command -v jq >/dev/null 2>&1 || return 1
72
- local registry
73
- registry="$(pricing_registry)" || return 1
74
- [[ -z "$registry" ]] && return 1
123
+ if [[ -z "$transcript" ]]; then
124
+ usage_log_debug "no usage reason: empty transcript_path"
125
+ return 1
126
+ fi
127
+ if [[ ! -f "$transcript" ]]; then
128
+ usage_log_debug "no usage — reason: transcript file missing (${transcript})"
129
+ return 1
130
+ fi
131
+ if ! command -v jq >/dev/null 2>&1; then
132
+ usage_log_debug "no usage — reason: jq unavailable"
133
+ return 1
134
+ fi
135
+
136
+ # Pricing registry is best-effort: when unavailable, tokens are still
137
+ # extracted below and only the cost fields degrade to null (defect #2 fix —
138
+ # previously this hard-returned 1 here, discarding token extraction
139
+ # entirely whenever pricing_registry() failed).
140
+ local registry has_registry
141
+ registry="$(pricing_registry 2>/dev/null)"
142
+ if [[ $? -eq 0 && -n "$registry" ]]; then
143
+ has_registry=true
144
+ else
145
+ has_registry=false
146
+ registry='null'
147
+ usage_log_debug "pricing registry unavailable (${transcript}) — extracting tokens without cost"
148
+ fi
149
+ # A non-empty registry can still be malformed (corrupt/truncated pricing.json,
150
+ # or a bad remote 200) — validate it parses before handing it to `jq -n
151
+ # --argjson`, which would otherwise abort the whole parse and discard tokens
152
+ # (the exact intermittent-null-tokens defect this function exists to fix).
153
+ if [[ "$has_registry" == true ]] && ! jq -e . >/dev/null 2>&1 <<<"$registry"; then
154
+ has_registry=false
155
+ registry='null'
156
+ usage_log_debug "pricing registry unparseable (${transcript}) — extracting tokens without cost"
157
+ fi
75
158
 
76
159
  local out
77
- out="$(jq -n --argjson registry "$registry" --arg version "$version" '
78
- $registry as $reg
79
- | (if $version == "" then ($reg.current_version) else $version end) as $ver
80
- | ($reg.versions[$ver]) as $p
81
- | if $p == null then empty else . end
82
- | ($p.cache_multipliers) as $cm
160
+ out="$(jq -n --argjson registry "$registry" --argjson has_registry "$has_registry" --arg version "$version" '
161
+ ($has_registry and ($registry != null)) as $has_reg
162
+ | (if $has_reg then (if $version == "" then $registry.current_version else $version end) else null end) as $ver
163
+ | (if $has_reg and ($ver != null) then ($registry.versions[$ver]) else null end) as $p
164
+ | ($p != null) as $priced
165
+ | (if $priced then $p.cache_multipliers else null end) as $cm
83
166
  | (reduce inputs as $l ({};
84
167
  ($l.message.usage) as $u
85
168
  | if $u then
@@ -92,8 +175,8 @@ usage_parse_transcript() {
92
175
  | ($agg | to_entries
93
176
  | map(
94
177
  .key as $m | .value as $u
95
- | (($p.models[$m]) // $p.default) as $rate
96
- | (if ([$m] | inside($p.zero_cost_models)) then 0 else 1 end) as $billable
178
+ | (if $priced then (($p.models[$m]) // $p.default) else null end) as $rate
179
+ | (if $priced then (if ([$m] | inside($p.zero_cost_models)) then 0 else 1 end) else null end) as $billable
97
180
  | {
98
181
  model: $m,
99
182
  input_tokens: ($u.input // 0),
@@ -101,12 +184,14 @@ usage_parse_transcript() {
101
184
  cache_creation_input_tokens: ($u.cache_creation // 0),
102
185
  cache_read_input_tokens: ($u.cache_read // 0),
103
186
  estimated_cost_usd: (
104
- $billable * (
105
- ($u.input // 0) * $rate.input
106
- + ($u.output // 0) * $rate.output
107
- + ($u.cache_creation // 0) * $rate.input * $cm.write_5m
108
- + ($u.cache_read // 0) * $rate.input * $cm.read
109
- ) / 1000000
187
+ if $priced then
188
+ $billable * (
189
+ ($u.input // 0) * $rate.input
190
+ + ($u.output // 0) * $rate.output
191
+ + ($u.cache_creation // 0) * $rate.input * $cm.write_5m
192
+ + ($u.cache_read // 0) * $rate.input * $cm.read
193
+ ) / 1000000
194
+ else null end
110
195
  )
111
196
  })) as $by_model
112
197
  | {
@@ -115,20 +200,28 @@ usage_parse_transcript() {
115
200
  output_tokens: ([$by_model[].output_tokens] | add // 0),
116
201
  cache_creation_input_tokens: ([$by_model[].cache_creation_input_tokens] | add // 0),
117
202
  cache_read_input_tokens: ([$by_model[].cache_read_input_tokens] | add // 0),
118
- estimated_cost_usd: (([$by_model[].estimated_cost_usd] | add // 0) * 1000000 | round / 1000000),
119
- pricing_version: $ver
203
+ estimated_cost_usd: (if $priced then (([$by_model[].estimated_cost_usd] | add // 0) * 1000000 | round / 1000000) else null end),
204
+ pricing_version: (if $priced then $ver else null end)
120
205
  }
121
206
  ' < "$transcript" 2>/dev/null)"
122
207
 
123
- [[ -z "$out" ]] && return 1
208
+ if [[ -z "$out" ]]; then
209
+ usage_log_debug "no usage — reason: jq parse failed (${transcript})"
210
+ return 1
211
+ fi
124
212
 
125
213
  # Drift / emptiness check: if we parsed zero tokens but the transcript clearly
126
214
  # contains usage data, the schema drifted — warn and fall back to null usage.
215
+ # This only discards tokens when there truly are none (total == 0); it never
216
+ # discards a non-zero token extraction, including the pricing-unavailable
217
+ # case above.
127
218
  local total
128
219
  total="$(printf '%s' "$out" | jq -r '((.input_tokens // 0) + (.output_tokens // 0) + (.cache_creation_input_tokens // 0) + (.cache_read_input_tokens // 0))' 2>/dev/null)"
129
220
  if [[ -z "$total" || "$total" == "0" ]]; then
130
221
  if grep -q '"input_tokens"' "$transcript" 2>/dev/null; then
131
222
  usage_log_drift "$transcript"
223
+ else
224
+ usage_log_debug "no usage — reason: parsed 0 tokens (no usage entries found in ${transcript})"
132
225
  fi
133
226
  return 1
134
227
  fi
@@ -8,6 +8,12 @@ channel.analytics.redact=tool.input,tool.output,turn.prompt_text,delegation.targ
8
8
  # The transport derives /api/telemetry/records from console_telemetry_url.
9
9
  # console_telemetry_token=
10
10
  # console_tenant_id=
11
+ # Kit-economics relay (#469): once console_telemetry_url/console_telemetry_endpoint_url
12
+ # resolves, the per-run kit-economics record relay defaults ON (opt-out, not silent-off).
13
+ # Set console_economics_relay=0 to opt out while keeping the telemetry mirror; or run
14
+ # install-console-config.sh --no-economics-relay to write this at install time.
15
+ # console_economics_relay=0
16
+ # console_economics_endpoint_url=
11
17
  # Live pricing registry source. If unset, derived from console_telemetry_url as
12
18
  # <console>/api/telemetry/pricing so bash/Python/TS runtimes read one live
13
19
  # source; lib/pricing.sh caches it and falls back to bundled pricing.json.