okstra 0.200.1 → 0.201.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/docs/cli.md +24 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +7 -3
- package/runtime/bin/okstra-spawn-followups.py +2 -2
- package/runtime/prompts/duties/technical-verification-worker.md +44 -0
- package/runtime/prompts/launch.template.md +7 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -2
- package/runtime/prompts/lead/plan-body-verification.md +3 -1
- package/runtime/prompts/lead/report-writer.md +8 -2
- package/runtime/prompts/lead/team-contract.md +6 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +7 -1
- package/runtime/prompts/profiles/final-verification.md +5 -0
- package/runtime/prompts/profiles/forbidden-actions.json +6 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +7 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/technical-verification.md +53 -0
- package/runtime/prompts/wizard/prompts.ko.json +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +42 -9
- package/runtime/python/okstra_ctl/agent/invocation.py +14 -6
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +4 -3
- package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +83 -22
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +44 -2
- package/runtime/python/okstra_ctl/conformance.py +2 -20
- package/runtime/python/okstra_ctl/dispatch_core.py +25 -5
- package/runtime/python/okstra_ctl/dispatch_state.py +2 -0
- package/runtime/python/okstra_ctl/domain/provider.py +0 -1
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +64 -7
- package/runtime/python/okstra_ctl/implementation_options.py +58 -45
- package/runtime/python/okstra_ctl/model_pool.py +2 -5
- package/runtime/python/okstra_ctl/next_phase.py +3 -0
- package/runtime/python/okstra_ctl/plan_items.py +15 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +9 -3
- package/runtime/python/okstra_ctl/qa_commands.py +30 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +11 -8
- package/runtime/python/okstra_ctl/render.py +3 -0
- package/runtime/python/okstra_ctl/render_final_report.py +1 -0
- package/runtime/python/okstra_ctl/report_assembly.py +8 -2
- package/runtime/python/okstra_ctl/report_contract.py +3 -0
- package/runtime/python/okstra_ctl/report_corrections.py +209 -93
- package/runtime/python/okstra_ctl/report_finalize.py +25 -8
- package/runtime/python/okstra_ctl/report_html/router.py +2 -0
- package/runtime/python/okstra_ctl/report_html/view_models/technical_verification.py +21 -0
- package/runtime/python/okstra_ctl/report_projections.py +4 -3
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +178 -26
- package/runtime/python/okstra_ctl/run.py +82 -0
- package/runtime/python/okstra_ctl/team.py +4 -1
- package/runtime/python/okstra_ctl/technical_verification.py +195 -0
- package/runtime/python/okstra_ctl/usage_identity.py +54 -0
- package/runtime/python/okstra_ctl/usage_report.py +22 -8
- package/runtime/python/okstra_ctl/verification_target.py +74 -0
- package/runtime/python/okstra_ctl/wizard/__init__.py +1 -1
- package/runtime/python/okstra_ctl/wizard/cli.py +2 -1
- package/runtime/python/okstra_ctl/wizard/confirmation.py +38 -2
- package/runtime/python/okstra_ctl/wizard/engine.py +3 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -0
- package/runtime/python/okstra_ctl/wizard/outcome.py +63 -0
- package/runtime/python/okstra_ctl/wizard/picker_navigation.py +2 -2
- package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
- package/runtime/python/okstra_ctl/wizard/render.py +8 -55
- package/runtime/python/okstra_ctl/wizard/roles.py +11 -7
- package/runtime/python/okstra_ctl/wizard/sources.py +28 -2
- package/runtime/python/okstra_ctl/wizard/state.py +13 -6
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +8 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +52 -39
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
- package/runtime/python/okstra_ctl/workflow.py +8 -0
- package/runtime/python/okstra_ctl/write_policy.py +23 -0
- package/runtime/python/okstra_token_usage/blocks.py +50 -1
- package/runtime/python/okstra_token_usage/claude.py +42 -21
- package/runtime/python/okstra_token_usage/codex.py +17 -0
- package/runtime/python/okstra_token_usage/collect.py +307 -164
- package/runtime/python/okstra_token_usage/cursor.py +2 -3
- package/runtime/python/okstra_token_usage/pricing.py +1 -0
- package/runtime/python/okstra_token_usage/report.py +35 -30
- package/runtime/python/okstra_token_usage/task_totals.py +3 -12
- package/runtime/schemas/final-report-v2.0.schema.json +298 -7
- package/runtime/schemas/final-report-v3.0.schema.json +298 -7
- package/runtime/schemas/report-narrative-v3.0.schema.json +1 -0
- package/runtime/schemas/report-writer-corrections-v1.0.schema.json +30 -3
- package/runtime/skills/okstra-run/SKILL.md +10 -2
- package/runtime/skills/okstra-setup/SKILL.md +42 -7
- package/runtime/templates/report-writer-prompt-preamble.md +7 -3
- package/runtime/templates/reports/html/i18n/en.json +11 -0
- package/runtime/templates/reports/html/i18n/ko.json +11 -0
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +7 -3
- package/runtime/templates/reports/html/tasks/technical-verification.template.html +35 -0
- package/runtime/templates/reports/md/tasks/technical-verification.template.md +5 -0
- package/runtime/validators/validate-run.py +9 -4
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
from okstra_project.dirs import OKSTRA_RELATIVE
|
|
11
11
|
|
|
12
|
-
from .blocks import na_block, usage_block
|
|
12
|
+
from .blocks import accounting_workers, na_block, usage_block
|
|
13
13
|
from .claude import (
|
|
14
14
|
claude_code_status_total,
|
|
15
15
|
claude_session_totals,
|
|
@@ -21,6 +21,7 @@ from .codex import (
|
|
|
21
21
|
codex_session_is_worker,
|
|
22
22
|
codex_session_total,
|
|
23
23
|
codex_session_window_total,
|
|
24
|
+
codex_wrapper_session_ids,
|
|
24
25
|
find_codex_sessions,
|
|
25
26
|
)
|
|
26
27
|
from .antigravity import (
|
|
@@ -37,7 +38,11 @@ from .grok import (
|
|
|
37
38
|
)
|
|
38
39
|
from .paths import claude_project_dir, find_session_jsonl, utc_now
|
|
39
40
|
from .pricing import antigravity_cost_usd, provider_billable_equivalent, provider_cost_usd
|
|
40
|
-
from okstra_ctl.
|
|
41
|
+
from okstra_ctl.usage_identity import (
|
|
42
|
+
unrostered_usage_workers,
|
|
43
|
+
usage_dispatch_records,
|
|
44
|
+
usage_session_ids,
|
|
45
|
+
)
|
|
41
46
|
from okstra_ctl.models import provider_wrappers
|
|
42
47
|
from okstra_ctl.wrapper_status import (
|
|
43
48
|
log_path_for_prompt,
|
|
@@ -248,7 +253,7 @@ def _recorded_run_end(state: dict, manifest: dict | None) -> str | None:
|
|
|
248
253
|
return None
|
|
249
254
|
ends: list[str] = []
|
|
250
255
|
blocks = [state.get("leadUsage")] + [
|
|
251
|
-
worker.get("usage") for worker in (state
|
|
256
|
+
worker.get("usage") for worker in accounting_workers(state)
|
|
252
257
|
]
|
|
253
258
|
for block in blocks:
|
|
254
259
|
if not isinstance(block, dict) or block.get("source") == "unavailable":
|
|
@@ -586,7 +591,7 @@ def _codex_worker_windows(project_root: Path, state: dict) -> dict[str, list[tup
|
|
|
586
591
|
windows.setdefault(worker_id, []).append((started_at, timestamp))
|
|
587
592
|
running_workers = {
|
|
588
593
|
str(worker.get("workerId") or "").strip()
|
|
589
|
-
for worker in state
|
|
594
|
+
for worker in accounting_workers(state)
|
|
590
595
|
if isinstance(worker, dict) and worker.get("status") in {"running", "in-progress"}
|
|
591
596
|
}
|
|
592
597
|
if running_workers:
|
|
@@ -663,7 +668,7 @@ def _cli_session_totals(
|
|
|
663
668
|
)
|
|
664
669
|
elif provider == "grok":
|
|
665
670
|
total = grok_session_total(session_path)
|
|
666
|
-
elif provider
|
|
671
|
+
elif provider in {"zai", "claude"}:
|
|
667
672
|
total = claude_code_status_total(session_path)
|
|
668
673
|
else:
|
|
669
674
|
continue
|
|
@@ -697,10 +702,16 @@ def _cli_usage_block(provider: str, totals: dict, session_paths: list[Path]) ->
|
|
|
697
702
|
if totals.get("model"):
|
|
698
703
|
block["model"] = totals["model"]
|
|
699
704
|
block["cliModel"] = totals["model"]
|
|
700
|
-
if provider in _THREE_RATE_PROVIDERS:
|
|
705
|
+
if provider in _THREE_RATE_PROVIDERS or provider == "zai":
|
|
706
|
+
input_tokens = totals.get("inputTokens", 0) or 0
|
|
707
|
+
if provider == "zai":
|
|
708
|
+
# GLM 상태는 캐시 토큰을 입력과 별도로 보고한다. 공통 단가는
|
|
709
|
+
# 캐시 포함 입력을 받으며, 캐시 생성은 일반 입력 단가로 계산한다.
|
|
710
|
+
cached_input = totals.get("cacheReadTokens", 0) or 0
|
|
711
|
+
input_tokens += (totals.get("cacheCreationTokens", 0) or 0) + cached_input
|
|
701
712
|
rates = (
|
|
702
713
|
totals.get("model"),
|
|
703
|
-
|
|
714
|
+
input_tokens,
|
|
704
715
|
cached_input,
|
|
705
716
|
totals.get("outputTokens", 0) or 0,
|
|
706
717
|
)
|
|
@@ -919,11 +930,9 @@ def _worker_cli_usage_block(
|
|
|
919
930
|
used_fallback = not windows
|
|
920
931
|
if used_fallback:
|
|
921
932
|
windows = fallback_windows
|
|
922
|
-
session_paths
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
if path not in session_paths:
|
|
926
|
-
session_paths.append(path)
|
|
933
|
+
session_paths, attribution = _worker_cli_sessions(
|
|
934
|
+
provider, project_root, worker, records, windows, status_paths,
|
|
935
|
+
)
|
|
927
936
|
status_path = status_paths[-1] if status_paths else None
|
|
928
937
|
if provider == "zai":
|
|
929
938
|
session_paths = list(dict.fromkeys(
|
|
@@ -942,6 +951,7 @@ def _worker_cli_usage_block(
|
|
|
942
951
|
sessions=session_paths,
|
|
943
952
|
fallback_window_used=used_fallback,
|
|
944
953
|
)
|
|
954
|
+
block.update(attribution)
|
|
945
955
|
durations = [
|
|
946
956
|
wrapper_execution(path).get("durationMs")
|
|
947
957
|
for path in status_paths
|
|
@@ -953,6 +963,97 @@ def _worker_cli_usage_block(
|
|
|
953
963
|
return block
|
|
954
964
|
|
|
955
965
|
|
|
966
|
+
def _worker_cli_sessions(
|
|
967
|
+
provider: str, project_root: Path, worker: dict, records: list[dict],
|
|
968
|
+
windows: list[tuple[str, str]], status_paths: list[Path | None],
|
|
969
|
+
) -> tuple[list[Path], dict]:
|
|
970
|
+
candidates = list(dict.fromkeys(
|
|
971
|
+
path for cwd in _worker_session_cwds(project_root, records)
|
|
972
|
+
for path in _cli_sessions_for_windows(provider, cwd, windows)
|
|
973
|
+
))
|
|
974
|
+
if provider != "codex":
|
|
975
|
+
return candidates, {}
|
|
976
|
+
wanted = {str(record["sessionId"]) for record in records if record.get("sessionId")}
|
|
977
|
+
for raw in _worker_prompt_paths(worker, records):
|
|
978
|
+
prompt = _resolve_project_path(project_root, raw)
|
|
979
|
+
if prompt is not None:
|
|
980
|
+
wanted.update(codex_wrapper_session_ids(log_path_for_prompt(prompt)))
|
|
981
|
+
for path in status_paths:
|
|
982
|
+
status = read_wrapper_status(path) if path is not None else None
|
|
983
|
+
if status is not None and status.raw.get("sessionId"):
|
|
984
|
+
wanted.add(str(status.raw["sessionId"]))
|
|
985
|
+
if wanted:
|
|
986
|
+
selected = [path for path in candidates if codex_session_ids(path) & wanted]
|
|
987
|
+
found = {session_id for path in selected for session_id in codex_session_ids(path)}
|
|
988
|
+
return selected, {"attribution": "session-id", "requestedSessionIds": sorted(wanted),
|
|
989
|
+
"missingSessionIds": sorted(wanted - found)}
|
|
990
|
+
unique_windows = all(
|
|
991
|
+
len({path for cwd in _worker_session_cwds(project_root, records)
|
|
992
|
+
for path in _cli_sessions_for_windows(provider, cwd, [window])}) <= 1
|
|
993
|
+
for window in windows
|
|
994
|
+
)
|
|
995
|
+
if unique_windows:
|
|
996
|
+
return candidates, {"attribution": "legacy-window"}
|
|
997
|
+
return [], {
|
|
998
|
+
"attribution": "ambiguous", "unattributedCliSessionPaths": [str(path) for path in candidates],
|
|
999
|
+
"cliNote": "Several Codex sessions match the wrapper window; no session identity was recorded.",
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _remove_shared_codex_usage(state: dict) -> None:
|
|
1004
|
+
"""같은 세션의 시간대 추정을 확정된 소유자보다 우선하지 않는다."""
|
|
1005
|
+
rows = [row for row in accounting_workers(state)
|
|
1006
|
+
if (row.get("usage") or {}).get("source") == "codex-cli"]
|
|
1007
|
+
owners: dict[str, list[dict]] = {}
|
|
1008
|
+
for row in rows:
|
|
1009
|
+
for path in row["usage"].get("cliSessionPaths") or []:
|
|
1010
|
+
owners.setdefault(path, []).append(row)
|
|
1011
|
+
exclusions = []
|
|
1012
|
+
for row in rows:
|
|
1013
|
+
block = row["usage"]
|
|
1014
|
+
rejected = []
|
|
1015
|
+
for path in block.get("cliSessionPaths") or []:
|
|
1016
|
+
shared = owners[path]
|
|
1017
|
+
if len(shared) < 2:
|
|
1018
|
+
continue
|
|
1019
|
+
exact = [item for item in shared if item["usage"].get("attribution") == "session-id"]
|
|
1020
|
+
if len(exact) != 1 or exact[0] is not row:
|
|
1021
|
+
rejected.append(path)
|
|
1022
|
+
if not rejected:
|
|
1023
|
+
continue
|
|
1024
|
+
exclusions.append((row, rejected))
|
|
1025
|
+
for row, rejected in exclusions:
|
|
1026
|
+
block = row["usage"]
|
|
1027
|
+
kept = [Path(path) for path in block.get("cliSessionPaths") or [] if path not in rejected]
|
|
1028
|
+
replacement = collect_cli_usage(provider="codex", status_path=None, sessions=kept)
|
|
1029
|
+
replacement.update({key: block[key] for key in ("durationMs", "cliExecutionStatus") if key in block})
|
|
1030
|
+
replacement.update(attribution="ambiguous", unattributedCliSessionPaths=rejected,
|
|
1031
|
+
cliNote="Codex session matched more than one worker; duplicate attribution excluded.")
|
|
1032
|
+
row["usage"] = replacement
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _unattributed_usage(state: dict, claude_usage: dict | None) -> dict | None:
|
|
1036
|
+
workers = accounting_workers(state)
|
|
1037
|
+
assigned = {path for row in workers for path in (row.get("usage") or {}).get("cliSessionPaths") or []}
|
|
1038
|
+
assigned.update((state.get("leadUsage") or {}).get("cliSessionPaths") or [])
|
|
1039
|
+
ambiguous = {path for row in workers
|
|
1040
|
+
for path in (row.get("usage") or {}).get("unattributedCliSessionPaths") or []}
|
|
1041
|
+
paths = [Path(path) for path in sorted(ambiguous - assigned)]
|
|
1042
|
+
totals = _cli_session_totals("codex", paths)
|
|
1043
|
+
if not totals:
|
|
1044
|
+
return claude_usage
|
|
1045
|
+
cli_usage = _cli_usage_block("codex", _aggregate_totals(totals), paths)
|
|
1046
|
+
blocks = [cli_usage] + ([claude_usage] if claude_usage is not None else [])
|
|
1047
|
+
combined = _aggregate_totals(blocks)
|
|
1048
|
+
for key in ("billableEquivalentTokens", "estimatedCostUsd", "cliEstimatedCostUsd"):
|
|
1049
|
+
combined[key] = sum(block.get(key, 0) or 0 for block in blocks)
|
|
1050
|
+
combined.update(source="unattributed", cliSessionPaths=[str(path) for path in paths],
|
|
1051
|
+
note="Measured session usage counted once; worker identity is ambiguous.")
|
|
1052
|
+
if claude_usage is not None:
|
|
1053
|
+
combined["sessionIds"] = claude_usage.get("sessionIds") or []
|
|
1054
|
+
return combined
|
|
1055
|
+
|
|
1056
|
+
|
|
956
1057
|
def _wrapper_claude_worker(worker: dict) -> bool:
|
|
957
1058
|
"""호스트가 claude 가 아닐 때 래퍼로 띄운 claude 워커."""
|
|
958
1059
|
provider = str(worker.get("provider") or worker.get("agent") or "").strip()
|
|
@@ -979,10 +1080,11 @@ def _claude_wrapper_usage_block(
|
|
|
979
1080
|
호스트의 `collect_claude_runtime_usage` 가 같은 id 로 하는 일을 여기서 한다.
|
|
980
1081
|
"""
|
|
981
1082
|
since, until = window
|
|
982
|
-
session_ids =
|
|
1083
|
+
session_ids = usage_session_ids(state, worker_id)
|
|
983
1084
|
if not session_ids:
|
|
984
|
-
return
|
|
985
|
-
|
|
1085
|
+
return _claude_wrapper_status_usage(
|
|
1086
|
+
project_root, usage_dispatch_records(state, worker_id),
|
|
1087
|
+
"claude wrapper worker has no dispatch session id recorded in workerDispatches",
|
|
986
1088
|
)
|
|
987
1089
|
totals: list[dict] = []
|
|
988
1090
|
paths: list[Path] = []
|
|
@@ -1002,7 +1104,8 @@ def _claude_wrapper_usage_block(
|
|
|
1002
1104
|
paths.append(path)
|
|
1003
1105
|
attributed.append(session_id)
|
|
1004
1106
|
if not totals:
|
|
1005
|
-
return
|
|
1107
|
+
return _claude_wrapper_status_usage(
|
|
1108
|
+
project_root, usage_dispatch_records(state, worker_id),
|
|
1006
1109
|
"claude session jsonl not found under "
|
|
1007
1110
|
f"{claude_project_dir(project_root)} for dispatch session ids {session_ids}"
|
|
1008
1111
|
)
|
|
@@ -1012,30 +1115,23 @@ def _claude_wrapper_usage_block(
|
|
|
1012
1115
|
return block
|
|
1013
1116
|
|
|
1014
1117
|
|
|
1015
|
-
def
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
"""
|
|
1032
|
-
session_paths = _cli_sessions_for_windows(provider, project_root, windows)
|
|
1033
|
-
cli = collect_cli_usage(
|
|
1034
|
-
provider=provider,
|
|
1035
|
-
status_path=status_path,
|
|
1036
|
-
sessions=session_paths,
|
|
1037
|
-
fallback_window_used=fallback_window_used,
|
|
1038
|
-
)
|
|
1118
|
+
def _claude_wrapper_status_usage(project_root: Path, records: list, reason: str) -> dict:
|
|
1119
|
+
paths = []
|
|
1120
|
+
for raw in _worker_prompt_paths({}, records):
|
|
1121
|
+
prompt = _resolve_project_path(project_root, raw)
|
|
1122
|
+
if prompt is not None:
|
|
1123
|
+
paths.append(status_path_for_prompt(prompt))
|
|
1124
|
+
totals = _cli_session_totals("claude", paths)
|
|
1125
|
+
if not totals:
|
|
1126
|
+
return na_block(reason)
|
|
1127
|
+
block = usage_block(_aggregate_totals(totals), source="claude-jsonl")
|
|
1128
|
+
block["attribution"] = "wrapper-status"
|
|
1129
|
+
block["statusPaths"] = [str(path) for path in paths]
|
|
1130
|
+
return block
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def _attach_cli_usage(block: dict, cli: dict) -> None:
|
|
1134
|
+
"""Claude 호스트 사용량에 이미 귀속한 공급자 CLI 비용을 붙인다."""
|
|
1039
1135
|
for key in (
|
|
1040
1136
|
"cliTotalTokens",
|
|
1041
1137
|
"cliEstimatedCostUsd",
|
|
@@ -1055,16 +1151,17 @@ def _collect_cli_runtime_usage(
|
|
|
1055
1151
|
*,
|
|
1056
1152
|
incremental: bool = True,
|
|
1057
1153
|
) -> dict:
|
|
1154
|
+
state["additionalWorkerUsage"] = unrostered_usage_workers(state)
|
|
1058
1155
|
windows_by_worker = _codex_worker_windows(project_root, state)
|
|
1059
1156
|
run_window: tuple[str | None, str | None] = (None, None)
|
|
1060
1157
|
if team_state_path is not None:
|
|
1061
1158
|
run_window = resolve_run_window(team_state_path, state)
|
|
1062
|
-
for worker in state
|
|
1159
|
+
for worker in accounting_workers(state):
|
|
1063
1160
|
if not isinstance(worker, dict):
|
|
1064
1161
|
continue
|
|
1065
1162
|
worker_id = str(worker.get("workerId") or "").strip()
|
|
1066
1163
|
records = [
|
|
1067
|
-
dict(record) for record in
|
|
1164
|
+
dict(record) for record in usage_dispatch_records(state, worker_id)
|
|
1068
1165
|
] if worker_id else []
|
|
1069
1166
|
provider = _cli_assignment_provider(worker)
|
|
1070
1167
|
if provider:
|
|
@@ -1090,6 +1187,7 @@ def _collect_cli_runtime_usage(
|
|
|
1090
1187
|
f"{worker.get('provider') or worker.get('agent') or worker.get('workerId')}"
|
|
1091
1188
|
)
|
|
1092
1189
|
state["leadUsage"] = _cli_lead_usage(state, project_root, team_state_path)
|
|
1190
|
+
_remove_shared_codex_usage(state)
|
|
1093
1191
|
_populate_usage_summary(state, team_name=resolve_team_name(state),
|
|
1094
1192
|
sessions_found=0, needle_source="none")
|
|
1095
1193
|
return state
|
|
@@ -1130,7 +1228,7 @@ def _cli_lead_usage(
|
|
|
1130
1228
|
)
|
|
1131
1229
|
worker_paths = {
|
|
1132
1230
|
Path(path)
|
|
1133
|
-
for worker in state
|
|
1231
|
+
for worker in accounting_workers(state)
|
|
1134
1232
|
if isinstance(worker, dict)
|
|
1135
1233
|
for path in ((worker.get("usage") or {}).get("cliSessionPaths") or [])
|
|
1136
1234
|
}
|
|
@@ -1222,17 +1320,7 @@ def _pinned_cli_lead_session(
|
|
|
1222
1320
|
return None
|
|
1223
1321
|
|
|
1224
1322
|
|
|
1225
|
-
def
|
|
1226
|
-
state: dict,
|
|
1227
|
-
*,
|
|
1228
|
-
team_name: str,
|
|
1229
|
-
sessions_found: int,
|
|
1230
|
-
unattributed_sessions: list[str] | None = None,
|
|
1231
|
-
unattributed_usage: dict[str, Any] | None = None,
|
|
1232
|
-
needle_source: str = "observed-team-names",
|
|
1233
|
-
) -> None:
|
|
1234
|
-
workers = state.get("workers", [])
|
|
1235
|
-
lead = state.get("leadUsage") or {}
|
|
1323
|
+
def _usage_summary_totals(lead: dict, workers: list[dict], unattributed_usage: dict | None) -> dict:
|
|
1236
1324
|
lead_total = lead.get("totalTokens", 0) or 0
|
|
1237
1325
|
lead_cache_read = lead.get("cacheReadTokens", 0) or 0
|
|
1238
1326
|
lead_billable = lead.get("billableEquivalentTokens", 0) or 0
|
|
@@ -1247,7 +1335,28 @@ def _populate_usage_summary(
|
|
|
1247
1335
|
worker_cache_read += unattributed_usage.get("cacheReadTokens", 0) or 0
|
|
1248
1336
|
worker_billable += unattributed_usage.get("billableEquivalentTokens", 0) or 0
|
|
1249
1337
|
worker_cost += unattributed_usage.get("estimatedCostUsd", 0) or 0
|
|
1338
|
+
cli_cost += unattributed_usage.get("cliEstimatedCostUsd", 0) or 0
|
|
1339
|
+
|
|
1340
|
+
return {
|
|
1341
|
+
"leadTotalTokens": lead_total,
|
|
1342
|
+
"workerTotalTokens": worker_total,
|
|
1343
|
+
"grandTotalTokens": lead_total + worker_total,
|
|
1344
|
+
"leadCacheReadTokens": lead_cache_read,
|
|
1345
|
+
"workerCacheReadTokens": worker_cache_read,
|
|
1346
|
+
"grandCacheReadTokens": lead_cache_read + worker_cache_read,
|
|
1347
|
+
"leadBillableEquivalentTokens": lead_billable,
|
|
1348
|
+
"workerBillableEquivalentTokens": worker_billable,
|
|
1349
|
+
"grandBillableEquivalentTokens": lead_billable + worker_billable,
|
|
1350
|
+
"estimatedCostUsd": {
|
|
1351
|
+
"lead": round(lead_cost, 4),
|
|
1352
|
+
"claudeWorkers": round(worker_cost, 4),
|
|
1353
|
+
"cliWorkers": round(cli_cost, 4),
|
|
1354
|
+
"grandTotal": round(lead_cost + worker_cost + cli_cost, 4),
|
|
1355
|
+
},
|
|
1356
|
+
}
|
|
1250
1357
|
|
|
1358
|
+
|
|
1359
|
+
def _unmatched_usage_models(lead: dict, workers: list[dict]) -> list[str]:
|
|
1251
1360
|
unmatched_models: list[str] = []
|
|
1252
1361
|
if (
|
|
1253
1362
|
lead.get("model")
|
|
@@ -1270,27 +1379,28 @@ def _populate_usage_summary(
|
|
|
1270
1379
|
unmatched_models.append(u["model"])
|
|
1271
1380
|
if u.get("cliModel") and u.get("cliEstimatedCostUsd") is None and (u.get("cliTotalTokens") or 0) > 0:
|
|
1272
1381
|
unmatched_models.append(u["cliModel"])
|
|
1382
|
+
return sorted(set(unmatched_models))
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
def _populate_usage_summary(
|
|
1386
|
+
state: dict,
|
|
1387
|
+
*,
|
|
1388
|
+
team_name: str,
|
|
1389
|
+
sessions_found: int,
|
|
1390
|
+
unattributed_sessions: list[str] | None = None,
|
|
1391
|
+
unattributed_usage: dict[str, Any] | None = None,
|
|
1392
|
+
needle_source: str = "observed-team-names",
|
|
1393
|
+
) -> None:
|
|
1394
|
+
workers = accounting_workers(state)
|
|
1395
|
+
unattributed_usage = _unattributed_usage(state, unattributed_usage)
|
|
1396
|
+
lead = state.get("leadUsage") or {}
|
|
1273
1397
|
state["usageSummary"] = {
|
|
1274
|
-
|
|
1275
|
-
"workerTotalTokens": worker_total,
|
|
1276
|
-
"grandTotalTokens": lead_total + worker_total,
|
|
1277
|
-
"leadCacheReadTokens": lead_cache_read,
|
|
1278
|
-
"workerCacheReadTokens": worker_cache_read,
|
|
1279
|
-
"grandCacheReadTokens": lead_cache_read + worker_cache_read,
|
|
1280
|
-
"leadBillableEquivalentTokens": lead_billable,
|
|
1281
|
-
"workerBillableEquivalentTokens": worker_billable,
|
|
1282
|
-
"grandBillableEquivalentTokens": lead_billable + worker_billable,
|
|
1283
|
-
"estimatedCostUsd": {
|
|
1284
|
-
"lead": round(lead_cost, 4),
|
|
1285
|
-
"claudeWorkers": round(worker_cost, 4),
|
|
1286
|
-
"cliWorkers": round(cli_cost, 4),
|
|
1287
|
-
"grandTotal": round(lead_cost + worker_cost + cli_cost, 4),
|
|
1288
|
-
},
|
|
1398
|
+
**_usage_summary_totals(lead, workers, unattributed_usage),
|
|
1289
1399
|
"collectedAt": utc_now(),
|
|
1290
1400
|
"teamName": team_name,
|
|
1291
1401
|
"needleSource": needle_source,
|
|
1292
1402
|
"sessionsFound": sessions_found,
|
|
1293
|
-
"unmatchedModels":
|
|
1403
|
+
"unmatchedModels": _unmatched_usage_models(lead, workers),
|
|
1294
1404
|
"unattributedTeamSessions": unattributed_sessions or [],
|
|
1295
1405
|
"unattributedWorkerUsage": unattributed_usage,
|
|
1296
1406
|
"definitions": {
|
|
@@ -1302,18 +1412,10 @@ def _populate_usage_summary(
|
|
|
1302
1412
|
}
|
|
1303
1413
|
|
|
1304
1414
|
|
|
1305
|
-
def
|
|
1306
|
-
team_state_path: Path,
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
incremental: bool = True,
|
|
1310
|
-
) -> dict:
|
|
1311
|
-
# incremental: 세션 jsonl 스캔에 byte cursor 캐시 사용 (P6). 캐시는 윈도우
|
|
1312
|
-
# 적용 전 이벤트를 저장하므로 결과는 전체 스캔과 동일 — False 는 캐시 경로를
|
|
1313
|
-
# 완전히 우회하는 정확성 폴백(CLI --no-cache).
|
|
1314
|
-
state = json.loads(team_state_path.read_text())
|
|
1315
|
-
cwd = project_root or _infer_project_root(team_state_path, state)
|
|
1316
|
-
run_since, run_until = resolve_run_window(team_state_path, state)
|
|
1415
|
+
def _claude_sessions_for_run(
|
|
1416
|
+
state: dict, team_state_path: Path, cwd: Path,
|
|
1417
|
+
run_since: str | None, run_until: str | None, incremental: bool,
|
|
1418
|
+
) -> tuple:
|
|
1317
1419
|
team_name = resolve_team_name(state)
|
|
1318
1420
|
lead_sid = (state.get("lead") or {}).get("sessionId")
|
|
1319
1421
|
|
|
@@ -1341,6 +1443,13 @@ def collect_claude_runtime_usage(
|
|
|
1341
1443
|
if ts and (found_earliest is None or ts < found_earliest):
|
|
1342
1444
|
found_earliest = ts
|
|
1343
1445
|
run_since = relax_window_start(run_since, found_earliest, floor)
|
|
1446
|
+
return team_name, lead_sid, claude_sessions, run_since, needle_source
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def _claude_session_evidence(
|
|
1450
|
+
state: dict, cwd: Path, claude_sessions: dict, lead_sid: str | None,
|
|
1451
|
+
run_since: str | None, run_until: str | None, incremental: bool,
|
|
1452
|
+
) -> tuple:
|
|
1344
1453
|
by_agent: dict[str, list[tuple[str, Path, dict]]] = {}
|
|
1345
1454
|
lead_path: Path | None = None
|
|
1346
1455
|
# Team-tagged non-lead sessions that carry no agentName. These are almost
|
|
@@ -1375,7 +1484,7 @@ def collect_claude_runtime_usage(
|
|
|
1375
1484
|
if team_create_status in ("implicit", "skipped", "error"):
|
|
1376
1485
|
worker_prefix_pool = [
|
|
1377
1486
|
prefix
|
|
1378
|
-
for w in state
|
|
1487
|
+
for w in accounting_workers(state)
|
|
1379
1488
|
for prefix in match_prefixes(w.get("workerId") or "")
|
|
1380
1489
|
]
|
|
1381
1490
|
subagent_parent_sids = [lead_sid] if lead_sid else []
|
|
@@ -1394,6 +1503,13 @@ def collect_claude_runtime_usage(
|
|
|
1394
1503
|
if agent:
|
|
1395
1504
|
by_agent.setdefault(agent, []).append((sid, path, totals))
|
|
1396
1505
|
|
|
1506
|
+
return by_agent, lead_path, unattributed_sessions, unattributed_totals
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
def _attach_claude_lead_usage(
|
|
1510
|
+
state: dict, cwd: Path, lead_path: Path | None, lead_sid: str | None,
|
|
1511
|
+
run_since: str | None, run_until: str | None, incremental: bool,
|
|
1512
|
+
) -> None:
|
|
1397
1513
|
# Lead.
|
|
1398
1514
|
if lead_path is not None:
|
|
1399
1515
|
totals = claude_session_totals(lead_path, since=run_since, until=run_until,
|
|
@@ -1406,84 +1522,93 @@ def collect_claude_runtime_usage(
|
|
|
1406
1522
|
f"lead session jsonl not found under {claude_project_dir(cwd)} (sessionId={lead_sid})"
|
|
1407
1523
|
)
|
|
1408
1524
|
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def _matching_claude_worker_sessions(
|
|
1528
|
+
worker: dict, state: dict, cwd: Path, by_agent: dict,
|
|
1529
|
+
run_since: str | None, run_until: str | None, incremental: bool,
|
|
1530
|
+
) -> tuple:
|
|
1531
|
+
sessions_dir = claude_project_dir(cwd)
|
|
1532
|
+
worker_id = worker.get("workerId")
|
|
1533
|
+
prefixes = match_prefixes(worker_id) if worker_id else []
|
|
1534
|
+
|
|
1535
|
+
# pane 워커는 별도 `claude -p` 프로세스라 jsonl 에 agentName 도 teamName
|
|
1536
|
+
# 도 안 남긴다 — 아래 prefix 경로로는 영원히 매칭되지 않는다. dispatch 가
|
|
1537
|
+
# 발급해 적어 둔 이 id 가 그 세션을 짚는 유일한 결정적 단서다. 재시도는
|
|
1538
|
+
# attempt 마다 새 세션이므로 기록된 id 를 전부 합산한다.
|
|
1539
|
+
# 헬퍼에서 worker_id=None 은 "run 전체"라, 이름 없는 워커에 그대로 넘기면
|
|
1540
|
+
# 그 워커가 run 의 모든 세션을 흡수한다.
|
|
1541
|
+
dispatched_sids = usage_session_ids(state, worker_id) if worker_id else []
|
|
1542
|
+
matched: list[tuple[str, Path, dict]] = []
|
|
1543
|
+
matched_sids: set[str] = set()
|
|
1544
|
+
for sid in dispatched_sids:
|
|
1545
|
+
path = sessions_dir / f"{sid}.jsonl"
|
|
1546
|
+
if not path.is_file():
|
|
1547
|
+
continue
|
|
1548
|
+
totals = claude_session_totals(path, since=run_since, until=run_until,
|
|
1549
|
+
incremental=incremental)
|
|
1550
|
+
# 창 밖 세션은 여기서 버린다 — agentName 발견 경로가 위에서 같은
|
|
1551
|
+
# `startedAt` 검사로 거르는 것과 같은 이유다. 넣으면 0 토큰 totals 가
|
|
1552
|
+
# usage_block 을 타고 "이 워커는 0 을 썼다"로 보고되어, unavailable 로
|
|
1553
|
+
# 남아야 할 상태를 허위 0 이 덮는다.
|
|
1554
|
+
if not totals.get("startedAt"):
|
|
1555
|
+
continue
|
|
1556
|
+
matched.append((sid, path, totals))
|
|
1557
|
+
matched_sids.add(sid)
|
|
1558
|
+
|
|
1559
|
+
# 조건부 폴백이 아니라 합집합이다. 한 워커의 attempt 1 이 pane(sid 로만
|
|
1560
|
+
# 찾힌다), attempt 2 가 in-process 서브에이전트(agentName 으로만 찾힌다)일
|
|
1561
|
+
# 수 있고, `if not matched:` 로 두면 sid 가 하나라도 맞는 순간 attempt 2 의
|
|
1562
|
+
# 토큰이 통째로 누락된다. 양쪽에서 온 같은 세션은 sid 로 한 번만 센다.
|
|
1563
|
+
for agent_name, entries in by_agent.items():
|
|
1564
|
+
if not agent_matches(agent_name, prefixes):
|
|
1565
|
+
continue
|
|
1566
|
+
for entry in entries:
|
|
1567
|
+
if entry[0] in matched_sids:
|
|
1568
|
+
continue
|
|
1569
|
+
# team-needle 경로는 창 검사 없이 by_agent 를 채운다. 같은 리드
|
|
1570
|
+
# 세션의 다음 run 이 띄운 워커도 같은 needle 에 걸리는데, 창 밖이라
|
|
1571
|
+
# startedAt 이 없어 아래 정렬에서 맨 앞에 서고 `sessionId` 를
|
|
1572
|
+
# 차지한다 — 토큰은 0 이라 합계는 안 틀리고 리포트가 가리키는
|
|
1573
|
+
# 세션만 남의 것이 된다.
|
|
1574
|
+
if not entry[2].get("startedAt"):
|
|
1575
|
+
continue
|
|
1576
|
+
matched.append(entry)
|
|
1577
|
+
matched_sids.add(entry[0])
|
|
1578
|
+
|
|
1579
|
+
return matched, dispatched_sids, prefixes
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _collect_claude_workers(
|
|
1583
|
+
state: dict, cwd: Path, by_agent: dict,
|
|
1584
|
+
run_since: str | None, run_until: str | None, incremental: bool,
|
|
1585
|
+
) -> set[str]:
|
|
1409
1586
|
# Workers — dispatch 가 기록한 세션 id 로 짚은 세션과 agentName prefix 로
|
|
1410
1587
|
# 찾은 세션의 합집합을 합산한다(재배치 `-002`, convergence `-reverify-r1`,
|
|
1411
1588
|
# implementation `-executor`, report-writer `-impl` / `-2` 등).
|
|
1412
|
-
sessions_dir = claude_project_dir(cwd)
|
|
1413
1589
|
# sid 로 귀속한 세션 id 전체 — 아래 unattributed 폴드에서 빼는 데 쓴다.
|
|
1414
1590
|
attributed_sids: set[str] = set()
|
|
1415
1591
|
cli_windows_by_worker = _codex_worker_windows(cwd, state)
|
|
1416
|
-
for worker in state
|
|
1592
|
+
for worker in accounting_workers(state):
|
|
1417
1593
|
worker_id = worker.get("workerId")
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
# 도 안 남긴다 — 아래 prefix 경로로는 영원히 매칭되지 않는다. dispatch 가
|
|
1423
|
-
# 발급해 적어 둔 이 id 가 그 세션을 짚는 유일한 결정적 단서다. 재시도는
|
|
1424
|
-
# attempt 마다 새 세션이므로 기록된 id 를 전부 합산한다.
|
|
1425
|
-
# 헬퍼에서 worker_id=None 은 "run 전체"라, 이름 없는 워커에 그대로 넘기면
|
|
1426
|
-
# 그 워커가 run 의 모든 세션을 흡수한다.
|
|
1427
|
-
dispatched_sids = worker_session_ids(state, worker_id) if worker_id else []
|
|
1428
|
-
matched: list[tuple[str, Path, dict]] = []
|
|
1429
|
-
matched_sids: set[str] = set()
|
|
1430
|
-
for sid in dispatched_sids:
|
|
1431
|
-
path = sessions_dir / f"{sid}.jsonl"
|
|
1432
|
-
if not path.is_file():
|
|
1433
|
-
continue
|
|
1434
|
-
totals = claude_session_totals(path, since=run_since, until=run_until,
|
|
1435
|
-
incremental=incremental)
|
|
1436
|
-
# 창 밖 세션은 여기서 버린다 — agentName 발견 경로가 위에서 같은
|
|
1437
|
-
# `startedAt` 검사로 거르는 것과 같은 이유다. 넣으면 0 토큰 totals 가
|
|
1438
|
-
# usage_block 을 타고 "이 워커는 0 을 썼다"로 보고되어, unavailable 로
|
|
1439
|
-
# 남아야 할 상태를 허위 0 이 덮는다.
|
|
1440
|
-
if not totals.get("startedAt"):
|
|
1441
|
-
continue
|
|
1442
|
-
matched.append((sid, path, totals))
|
|
1443
|
-
matched_sids.add(sid)
|
|
1444
|
-
attributed_sids.add(sid)
|
|
1445
|
-
|
|
1446
|
-
# 조건부 폴백이 아니라 합집합이다. 한 워커의 attempt 1 이 pane(sid 로만
|
|
1447
|
-
# 찾힌다), attempt 2 가 in-process 서브에이전트(agentName 으로만 찾힌다)일
|
|
1448
|
-
# 수 있고, `if not matched:` 로 두면 sid 가 하나라도 맞는 순간 attempt 2 의
|
|
1449
|
-
# 토큰이 통째로 누락된다. 양쪽에서 온 같은 세션은 sid 로 한 번만 센다.
|
|
1450
|
-
for agent_name, entries in by_agent.items():
|
|
1451
|
-
if not agent_matches(agent_name, prefixes):
|
|
1452
|
-
continue
|
|
1453
|
-
for entry in entries:
|
|
1454
|
-
if entry[0] in matched_sids:
|
|
1455
|
-
continue
|
|
1456
|
-
# team-needle 경로는 창 검사 없이 by_agent 를 채운다. 같은 리드
|
|
1457
|
-
# 세션의 다음 run 이 띄운 워커도 같은 needle 에 걸리는데, 창 밖이라
|
|
1458
|
-
# startedAt 이 없어 아래 정렬에서 맨 앞에 서고 `sessionId` 를
|
|
1459
|
-
# 차지한다 — 토큰은 0 이라 합계는 안 틀리고 리포트가 가리키는
|
|
1460
|
-
# 세션만 남의 것이 된다.
|
|
1461
|
-
if not entry[2].get("startedAt"):
|
|
1462
|
-
continue
|
|
1463
|
-
matched.append(entry)
|
|
1464
|
-
matched_sids.add(entry[0])
|
|
1465
|
-
|
|
1594
|
+
matched, dispatched_sids, prefixes = _matching_claude_worker_sessions(
|
|
1595
|
+
worker, state, cwd, by_agent, run_since, run_until, incremental,
|
|
1596
|
+
)
|
|
1597
|
+
attributed_sids.update(sid for sid, _path, _totals in matched)
|
|
1466
1598
|
if not matched:
|
|
1467
1599
|
provider = _cli_assignment_provider(worker)
|
|
1468
1600
|
if provider and str(worker.get("runner") or "") == "cli-wrapper":
|
|
1469
1601
|
worker["usage"] = _worker_cli_usage_block(
|
|
1470
|
-
provider=provider,
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
dict(record)
|
|
1475
|
-
for record in worker_dispatch_records(
|
|
1476
|
-
state, str(worker_id or "").strip()
|
|
1477
|
-
)
|
|
1478
|
-
] if worker_id else [],
|
|
1479
|
-
fallback_windows=cli_windows_by_worker.get(
|
|
1480
|
-
str(worker_id or "").strip(), []
|
|
1481
|
-
),
|
|
1602
|
+
provider=provider, project_root=cwd, worker=worker,
|
|
1603
|
+
records=[dict(record) for record in usage_dispatch_records(state, worker_id)]
|
|
1604
|
+
if worker_id else [],
|
|
1605
|
+
fallback_windows=cli_windows_by_worker.get(worker_id, []),
|
|
1482
1606
|
)
|
|
1483
1607
|
else:
|
|
1484
|
-
worker["usage"] =
|
|
1608
|
+
worker["usage"] = _claude_wrapper_status_usage(
|
|
1609
|
+
cwd, usage_dispatch_records(state, worker_id) if worker_id else [],
|
|
1485
1610
|
"no Claude session jsonl in the run window for dispatched "
|
|
1486
|
-
f"sessionIds {dispatched_sids} or agentName prefixes {prefixes}"
|
|
1611
|
+
f"sessionIds {dispatched_sids} or agentName prefixes {prefixes}",
|
|
1487
1612
|
)
|
|
1488
1613
|
continue
|
|
1489
1614
|
|
|
@@ -1501,21 +1626,20 @@ def collect_claude_runtime_usage(
|
|
|
1501
1626
|
# attributable provider transcript usage without treating absence as zero.
|
|
1502
1627
|
provider = _cli_assignment_provider(worker)
|
|
1503
1628
|
if provider:
|
|
1504
|
-
|
|
1505
|
-
cwd,
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
)
|
|
1509
|
-
_attach_cli_usage(
|
|
1510
|
-
block,
|
|
1511
|
-
provider,
|
|
1512
|
-
cwd,
|
|
1513
|
-
windows,
|
|
1514
|
-
status_path,
|
|
1515
|
-
used_fallback,
|
|
1629
|
+
cli = _worker_cli_usage_block(
|
|
1630
|
+
provider=provider, project_root=cwd, worker=worker,
|
|
1631
|
+
records=[dict(record) for record in usage_dispatch_records(state, worker_id)],
|
|
1632
|
+
fallback_windows=cli_windows_by_worker.get(worker_id, []),
|
|
1516
1633
|
)
|
|
1634
|
+
_attach_cli_usage(block, cli)
|
|
1517
1635
|
worker["usage"] = block
|
|
1518
1636
|
|
|
1637
|
+
return attributed_sids
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
def _unattributed_claude_usage(
|
|
1641
|
+
unattributed_sessions: list[str], unattributed_totals: list[dict], attributed_sids: set[str],
|
|
1642
|
+
) -> tuple[list[str], dict | None]:
|
|
1519
1643
|
# Fold team-tagged worker sessions that carry no agentName into the worker
|
|
1520
1644
|
# pool. They cannot be mapped to a specific workerId (so each named worker
|
|
1521
1645
|
# row above stays `unavailable`), but the tokens are real team-worker spend —
|
|
@@ -1551,12 +1675,31 @@ def collect_claude_runtime_usage(
|
|
|
1551
1675
|
"cannot be mapped to a specific workerId."
|
|
1552
1676
|
)
|
|
1553
1677
|
|
|
1678
|
+
return unattributed_sessions, unattributed_usage
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
def collect_claude_runtime_usage(
|
|
1682
|
+
team_state_path: Path, project_root: Path | None = None, *, incremental: bool = True,
|
|
1683
|
+
) -> dict:
|
|
1684
|
+
state = json.loads(team_state_path.read_text())
|
|
1685
|
+
state["additionalWorkerUsage"] = unrostered_usage_workers(state)
|
|
1686
|
+
cwd = project_root or _infer_project_root(team_state_path, state)
|
|
1687
|
+
run_since, run_until = resolve_run_window(team_state_path, state)
|
|
1688
|
+
team_name, lead_sid, sessions, run_since, needle_source = _claude_sessions_for_run(
|
|
1689
|
+
state, team_state_path, cwd, run_since, run_until, incremental,
|
|
1690
|
+
)
|
|
1691
|
+
by_agent, lead_path, unattributed_sessions, unattributed_totals = _claude_session_evidence(
|
|
1692
|
+
state, cwd, sessions, lead_sid, run_since, run_until, incremental,
|
|
1693
|
+
)
|
|
1694
|
+
_attach_claude_lead_usage(state, cwd, lead_path, lead_sid, run_since, run_until, incremental)
|
|
1695
|
+
attributed = _collect_claude_workers(state, cwd, by_agent, run_since, run_until, incremental)
|
|
1696
|
+
unattributed_sessions, unattributed_usage = _unattributed_claude_usage(
|
|
1697
|
+
unattributed_sessions, unattributed_totals, attributed,
|
|
1698
|
+
)
|
|
1699
|
+
_remove_shared_codex_usage(state)
|
|
1554
1700
|
_populate_usage_summary(
|
|
1555
|
-
state,
|
|
1556
|
-
|
|
1557
|
-
sessions_found=len(claude_sessions),
|
|
1558
|
-
unattributed_sessions=unattributed_sessions,
|
|
1559
|
-
unattributed_usage=unattributed_usage,
|
|
1701
|
+
state, team_name=team_name, sessions_found=len(sessions),
|
|
1702
|
+
unattributed_sessions=unattributed_sessions, unattributed_usage=unattributed_usage,
|
|
1560
1703
|
needle_source=needle_source,
|
|
1561
1704
|
)
|
|
1562
1705
|
return state
|