@kontourai/flow-agents 3.2.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.
- package/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +173 -0
- package/CHANGELOG.md +54 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/assignment-provider.js +10 -1
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
- package/build/src/cli/workflow-sidecar.d.ts +310 -1
- package/build/src/cli/workflow-sidecar.js +1914 -126
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +54 -1
- package/build/src/lib/flow-resolver.js +112 -5
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +30 -3
- package/context/contracts/artifact-contract.md +16 -2
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +194 -22
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/coordination-guide.md +370 -0
- package/docs/decisions/agent-coordination.md +26 -9
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +5 -3
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/trust-reconcile.md +42 -9
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +6 -2
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +23 -9
- package/docs/integrations/flow-agents-console.md +275 -0
- package/docs/integrations/index.md +4 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-artifact-lifecycle.md +38 -1
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +2 -0
- package/evals/ci/run-baseline.sh +78 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
- package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_checkpoint_signing.sh +10 -2
- package/evals/integration/test_ci_actor_identity.sh +221 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +389 -2
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_reconcile_preflight.sh +304 -0
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_takeover_protocol.sh +340 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_verify_hold.sh +910 -0
- package/evals/integration/test_veritas_governance_kit.sh +257 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +37 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/continue-work/SKILL.md +2 -0
- package/kits/builder/skills/deliver/SKILL.md +115 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +33 -2
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +113 -7
- package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
- package/kits/veritas-governance/kit.json +19 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +214 -253
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/actor-identity.js +82 -0
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +194 -22
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/lib/reconcile-shape.js +381 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/assignment-provider.ts +12 -1
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +72 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
- package/src/cli/workflow-sidecar.ts +1980 -119
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +117 -4
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- package/src/tools/validate-source-tree.ts +29 -3
|
@@ -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
|
-
|
|
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
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
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
|
|
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
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
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 -
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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; }
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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' "$
|
|
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
|
|
27
|
-
grep -c
|
|
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
|
|
34
|
-
grep -c
|
|
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
|
-
#
|
|
51
|
-
#
|
|
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"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
[[ -
|
|
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 $
|
|
79
|
-
| (if $version == "" then
|
|
80
|
-
| ($
|
|
81
|
-
|
|
|
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
|
-
$
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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" ]]
|
|
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
|
|
Binary file
|
|
@@ -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.
|