okstra 0.200.0 → 0.201.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/docs/cli.md +14 -3
- 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 +11 -5
- 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 +36 -5
- 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 +181 -47
- 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 +299 -162
- package/runtime/python/okstra_token_usage/cursor.py +2 -3
- 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-synthesis-packet-v1.0.schema.json +1 -1
- 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
|
|
@@ -919,11 +924,9 @@ def _worker_cli_usage_block(
|
|
|
919
924
|
used_fallback = not windows
|
|
920
925
|
if used_fallback:
|
|
921
926
|
windows = fallback_windows
|
|
922
|
-
session_paths
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
if path not in session_paths:
|
|
926
|
-
session_paths.append(path)
|
|
927
|
+
session_paths, attribution = _worker_cli_sessions(
|
|
928
|
+
provider, project_root, worker, records, windows, status_paths,
|
|
929
|
+
)
|
|
927
930
|
status_path = status_paths[-1] if status_paths else None
|
|
928
931
|
if provider == "zai":
|
|
929
932
|
session_paths = list(dict.fromkeys(
|
|
@@ -942,6 +945,7 @@ def _worker_cli_usage_block(
|
|
|
942
945
|
sessions=session_paths,
|
|
943
946
|
fallback_window_used=used_fallback,
|
|
944
947
|
)
|
|
948
|
+
block.update(attribution)
|
|
945
949
|
durations = [
|
|
946
950
|
wrapper_execution(path).get("durationMs")
|
|
947
951
|
for path in status_paths
|
|
@@ -953,6 +957,97 @@ def _worker_cli_usage_block(
|
|
|
953
957
|
return block
|
|
954
958
|
|
|
955
959
|
|
|
960
|
+
def _worker_cli_sessions(
|
|
961
|
+
provider: str, project_root: Path, worker: dict, records: list[dict],
|
|
962
|
+
windows: list[tuple[str, str]], status_paths: list[Path | None],
|
|
963
|
+
) -> tuple[list[Path], dict]:
|
|
964
|
+
candidates = list(dict.fromkeys(
|
|
965
|
+
path for cwd in _worker_session_cwds(project_root, records)
|
|
966
|
+
for path in _cli_sessions_for_windows(provider, cwd, windows)
|
|
967
|
+
))
|
|
968
|
+
if provider != "codex":
|
|
969
|
+
return candidates, {}
|
|
970
|
+
wanted = {str(record["sessionId"]) for record in records if record.get("sessionId")}
|
|
971
|
+
for raw in _worker_prompt_paths(worker, records):
|
|
972
|
+
prompt = _resolve_project_path(project_root, raw)
|
|
973
|
+
if prompt is not None:
|
|
974
|
+
wanted.update(codex_wrapper_session_ids(log_path_for_prompt(prompt)))
|
|
975
|
+
for path in status_paths:
|
|
976
|
+
status = read_wrapper_status(path) if path is not None else None
|
|
977
|
+
if status is not None and status.raw.get("sessionId"):
|
|
978
|
+
wanted.add(str(status.raw["sessionId"]))
|
|
979
|
+
if wanted:
|
|
980
|
+
selected = [path for path in candidates if codex_session_ids(path) & wanted]
|
|
981
|
+
found = {session_id for path in selected for session_id in codex_session_ids(path)}
|
|
982
|
+
return selected, {"attribution": "session-id", "requestedSessionIds": sorted(wanted),
|
|
983
|
+
"missingSessionIds": sorted(wanted - found)}
|
|
984
|
+
unique_windows = all(
|
|
985
|
+
len({path for cwd in _worker_session_cwds(project_root, records)
|
|
986
|
+
for path in _cli_sessions_for_windows(provider, cwd, [window])}) <= 1
|
|
987
|
+
for window in windows
|
|
988
|
+
)
|
|
989
|
+
if unique_windows:
|
|
990
|
+
return candidates, {"attribution": "legacy-window"}
|
|
991
|
+
return [], {
|
|
992
|
+
"attribution": "ambiguous", "unattributedCliSessionPaths": [str(path) for path in candidates],
|
|
993
|
+
"cliNote": "Several Codex sessions match the wrapper window; no session identity was recorded.",
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def _remove_shared_codex_usage(state: dict) -> None:
|
|
998
|
+
"""같은 세션의 시간대 추정을 확정된 소유자보다 우선하지 않는다."""
|
|
999
|
+
rows = [row for row in accounting_workers(state)
|
|
1000
|
+
if (row.get("usage") or {}).get("source") == "codex-cli"]
|
|
1001
|
+
owners: dict[str, list[dict]] = {}
|
|
1002
|
+
for row in rows:
|
|
1003
|
+
for path in row["usage"].get("cliSessionPaths") or []:
|
|
1004
|
+
owners.setdefault(path, []).append(row)
|
|
1005
|
+
exclusions = []
|
|
1006
|
+
for row in rows:
|
|
1007
|
+
block = row["usage"]
|
|
1008
|
+
rejected = []
|
|
1009
|
+
for path in block.get("cliSessionPaths") or []:
|
|
1010
|
+
shared = owners[path]
|
|
1011
|
+
if len(shared) < 2:
|
|
1012
|
+
continue
|
|
1013
|
+
exact = [item for item in shared if item["usage"].get("attribution") == "session-id"]
|
|
1014
|
+
if len(exact) != 1 or exact[0] is not row:
|
|
1015
|
+
rejected.append(path)
|
|
1016
|
+
if not rejected:
|
|
1017
|
+
continue
|
|
1018
|
+
exclusions.append((row, rejected))
|
|
1019
|
+
for row, rejected in exclusions:
|
|
1020
|
+
block = row["usage"]
|
|
1021
|
+
kept = [Path(path) for path in block.get("cliSessionPaths") or [] if path not in rejected]
|
|
1022
|
+
replacement = collect_cli_usage(provider="codex", status_path=None, sessions=kept)
|
|
1023
|
+
replacement.update({key: block[key] for key in ("durationMs", "cliExecutionStatus") if key in block})
|
|
1024
|
+
replacement.update(attribution="ambiguous", unattributedCliSessionPaths=rejected,
|
|
1025
|
+
cliNote="Codex session matched more than one worker; duplicate attribution excluded.")
|
|
1026
|
+
row["usage"] = replacement
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def _unattributed_usage(state: dict, claude_usage: dict | None) -> dict | None:
|
|
1030
|
+
workers = accounting_workers(state)
|
|
1031
|
+
assigned = {path for row in workers for path in (row.get("usage") or {}).get("cliSessionPaths") or []}
|
|
1032
|
+
assigned.update((state.get("leadUsage") or {}).get("cliSessionPaths") or [])
|
|
1033
|
+
ambiguous = {path for row in workers
|
|
1034
|
+
for path in (row.get("usage") or {}).get("unattributedCliSessionPaths") or []}
|
|
1035
|
+
paths = [Path(path) for path in sorted(ambiguous - assigned)]
|
|
1036
|
+
totals = _cli_session_totals("codex", paths)
|
|
1037
|
+
if not totals:
|
|
1038
|
+
return claude_usage
|
|
1039
|
+
cli_usage = _cli_usage_block("codex", _aggregate_totals(totals), paths)
|
|
1040
|
+
blocks = [cli_usage] + ([claude_usage] if claude_usage is not None else [])
|
|
1041
|
+
combined = _aggregate_totals(blocks)
|
|
1042
|
+
for key in ("billableEquivalentTokens", "estimatedCostUsd", "cliEstimatedCostUsd"):
|
|
1043
|
+
combined[key] = sum(block.get(key, 0) or 0 for block in blocks)
|
|
1044
|
+
combined.update(source="unattributed", cliSessionPaths=[str(path) for path in paths],
|
|
1045
|
+
note="Measured session usage counted once; worker identity is ambiguous.")
|
|
1046
|
+
if claude_usage is not None:
|
|
1047
|
+
combined["sessionIds"] = claude_usage.get("sessionIds") or []
|
|
1048
|
+
return combined
|
|
1049
|
+
|
|
1050
|
+
|
|
956
1051
|
def _wrapper_claude_worker(worker: dict) -> bool:
|
|
957
1052
|
"""호스트가 claude 가 아닐 때 래퍼로 띄운 claude 워커."""
|
|
958
1053
|
provider = str(worker.get("provider") or worker.get("agent") or "").strip()
|
|
@@ -979,10 +1074,11 @@ def _claude_wrapper_usage_block(
|
|
|
979
1074
|
호스트의 `collect_claude_runtime_usage` 가 같은 id 로 하는 일을 여기서 한다.
|
|
980
1075
|
"""
|
|
981
1076
|
since, until = window
|
|
982
|
-
session_ids =
|
|
1077
|
+
session_ids = usage_session_ids(state, worker_id)
|
|
983
1078
|
if not session_ids:
|
|
984
|
-
return
|
|
985
|
-
|
|
1079
|
+
return _claude_wrapper_status_usage(
|
|
1080
|
+
project_root, usage_dispatch_records(state, worker_id),
|
|
1081
|
+
"claude wrapper worker has no dispatch session id recorded in workerDispatches",
|
|
986
1082
|
)
|
|
987
1083
|
totals: list[dict] = []
|
|
988
1084
|
paths: list[Path] = []
|
|
@@ -1002,7 +1098,8 @@ def _claude_wrapper_usage_block(
|
|
|
1002
1098
|
paths.append(path)
|
|
1003
1099
|
attributed.append(session_id)
|
|
1004
1100
|
if not totals:
|
|
1005
|
-
return
|
|
1101
|
+
return _claude_wrapper_status_usage(
|
|
1102
|
+
project_root, usage_dispatch_records(state, worker_id),
|
|
1006
1103
|
"claude session jsonl not found under "
|
|
1007
1104
|
f"{claude_project_dir(project_root)} for dispatch session ids {session_ids}"
|
|
1008
1105
|
)
|
|
@@ -1012,30 +1109,23 @@ def _claude_wrapper_usage_block(
|
|
|
1012
1109
|
return block
|
|
1013
1110
|
|
|
1014
1111
|
|
|
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
|
-
)
|
|
1112
|
+
def _claude_wrapper_status_usage(project_root: Path, records: list, reason: str) -> dict:
|
|
1113
|
+
paths = []
|
|
1114
|
+
for raw in _worker_prompt_paths({}, records):
|
|
1115
|
+
prompt = _resolve_project_path(project_root, raw)
|
|
1116
|
+
if prompt is not None:
|
|
1117
|
+
paths.append(status_path_for_prompt(prompt))
|
|
1118
|
+
totals = _cli_session_totals("claude", paths)
|
|
1119
|
+
if not totals:
|
|
1120
|
+
return na_block(reason)
|
|
1121
|
+
block = usage_block(_aggregate_totals(totals), source="claude-jsonl")
|
|
1122
|
+
block["attribution"] = "wrapper-status"
|
|
1123
|
+
block["statusPaths"] = [str(path) for path in paths]
|
|
1124
|
+
return block
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def _attach_cli_usage(block: dict, cli: dict) -> None:
|
|
1128
|
+
"""Claude 호스트 사용량에 이미 귀속한 공급자 CLI 비용을 붙인다."""
|
|
1039
1129
|
for key in (
|
|
1040
1130
|
"cliTotalTokens",
|
|
1041
1131
|
"cliEstimatedCostUsd",
|
|
@@ -1055,16 +1145,17 @@ def _collect_cli_runtime_usage(
|
|
|
1055
1145
|
*,
|
|
1056
1146
|
incremental: bool = True,
|
|
1057
1147
|
) -> dict:
|
|
1148
|
+
state["additionalWorkerUsage"] = unrostered_usage_workers(state)
|
|
1058
1149
|
windows_by_worker = _codex_worker_windows(project_root, state)
|
|
1059
1150
|
run_window: tuple[str | None, str | None] = (None, None)
|
|
1060
1151
|
if team_state_path is not None:
|
|
1061
1152
|
run_window = resolve_run_window(team_state_path, state)
|
|
1062
|
-
for worker in state
|
|
1153
|
+
for worker in accounting_workers(state):
|
|
1063
1154
|
if not isinstance(worker, dict):
|
|
1064
1155
|
continue
|
|
1065
1156
|
worker_id = str(worker.get("workerId") or "").strip()
|
|
1066
1157
|
records = [
|
|
1067
|
-
dict(record) for record in
|
|
1158
|
+
dict(record) for record in usage_dispatch_records(state, worker_id)
|
|
1068
1159
|
] if worker_id else []
|
|
1069
1160
|
provider = _cli_assignment_provider(worker)
|
|
1070
1161
|
if provider:
|
|
@@ -1090,6 +1181,7 @@ def _collect_cli_runtime_usage(
|
|
|
1090
1181
|
f"{worker.get('provider') or worker.get('agent') or worker.get('workerId')}"
|
|
1091
1182
|
)
|
|
1092
1183
|
state["leadUsage"] = _cli_lead_usage(state, project_root, team_state_path)
|
|
1184
|
+
_remove_shared_codex_usage(state)
|
|
1093
1185
|
_populate_usage_summary(state, team_name=resolve_team_name(state),
|
|
1094
1186
|
sessions_found=0, needle_source="none")
|
|
1095
1187
|
return state
|
|
@@ -1130,7 +1222,7 @@ def _cli_lead_usage(
|
|
|
1130
1222
|
)
|
|
1131
1223
|
worker_paths = {
|
|
1132
1224
|
Path(path)
|
|
1133
|
-
for worker in state
|
|
1225
|
+
for worker in accounting_workers(state)
|
|
1134
1226
|
if isinstance(worker, dict)
|
|
1135
1227
|
for path in ((worker.get("usage") or {}).get("cliSessionPaths") or [])
|
|
1136
1228
|
}
|
|
@@ -1222,17 +1314,7 @@ def _pinned_cli_lead_session(
|
|
|
1222
1314
|
return None
|
|
1223
1315
|
|
|
1224
1316
|
|
|
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 {}
|
|
1317
|
+
def _usage_summary_totals(lead: dict, workers: list[dict], unattributed_usage: dict | None) -> dict:
|
|
1236
1318
|
lead_total = lead.get("totalTokens", 0) or 0
|
|
1237
1319
|
lead_cache_read = lead.get("cacheReadTokens", 0) or 0
|
|
1238
1320
|
lead_billable = lead.get("billableEquivalentTokens", 0) or 0
|
|
@@ -1247,7 +1329,28 @@ def _populate_usage_summary(
|
|
|
1247
1329
|
worker_cache_read += unattributed_usage.get("cacheReadTokens", 0) or 0
|
|
1248
1330
|
worker_billable += unattributed_usage.get("billableEquivalentTokens", 0) or 0
|
|
1249
1331
|
worker_cost += unattributed_usage.get("estimatedCostUsd", 0) or 0
|
|
1332
|
+
cli_cost += unattributed_usage.get("cliEstimatedCostUsd", 0) or 0
|
|
1333
|
+
|
|
1334
|
+
return {
|
|
1335
|
+
"leadTotalTokens": lead_total,
|
|
1336
|
+
"workerTotalTokens": worker_total,
|
|
1337
|
+
"grandTotalTokens": lead_total + worker_total,
|
|
1338
|
+
"leadCacheReadTokens": lead_cache_read,
|
|
1339
|
+
"workerCacheReadTokens": worker_cache_read,
|
|
1340
|
+
"grandCacheReadTokens": lead_cache_read + worker_cache_read,
|
|
1341
|
+
"leadBillableEquivalentTokens": lead_billable,
|
|
1342
|
+
"workerBillableEquivalentTokens": worker_billable,
|
|
1343
|
+
"grandBillableEquivalentTokens": lead_billable + worker_billable,
|
|
1344
|
+
"estimatedCostUsd": {
|
|
1345
|
+
"lead": round(lead_cost, 4),
|
|
1346
|
+
"claudeWorkers": round(worker_cost, 4),
|
|
1347
|
+
"cliWorkers": round(cli_cost, 4),
|
|
1348
|
+
"grandTotal": round(lead_cost + worker_cost + cli_cost, 4),
|
|
1349
|
+
},
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1250
1352
|
|
|
1353
|
+
def _unmatched_usage_models(lead: dict, workers: list[dict]) -> list[str]:
|
|
1251
1354
|
unmatched_models: list[str] = []
|
|
1252
1355
|
if (
|
|
1253
1356
|
lead.get("model")
|
|
@@ -1270,27 +1373,28 @@ def _populate_usage_summary(
|
|
|
1270
1373
|
unmatched_models.append(u["model"])
|
|
1271
1374
|
if u.get("cliModel") and u.get("cliEstimatedCostUsd") is None and (u.get("cliTotalTokens") or 0) > 0:
|
|
1272
1375
|
unmatched_models.append(u["cliModel"])
|
|
1376
|
+
return sorted(set(unmatched_models))
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def _populate_usage_summary(
|
|
1380
|
+
state: dict,
|
|
1381
|
+
*,
|
|
1382
|
+
team_name: str,
|
|
1383
|
+
sessions_found: int,
|
|
1384
|
+
unattributed_sessions: list[str] | None = None,
|
|
1385
|
+
unattributed_usage: dict[str, Any] | None = None,
|
|
1386
|
+
needle_source: str = "observed-team-names",
|
|
1387
|
+
) -> None:
|
|
1388
|
+
workers = accounting_workers(state)
|
|
1389
|
+
unattributed_usage = _unattributed_usage(state, unattributed_usage)
|
|
1390
|
+
lead = state.get("leadUsage") or {}
|
|
1273
1391
|
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
|
-
},
|
|
1392
|
+
**_usage_summary_totals(lead, workers, unattributed_usage),
|
|
1289
1393
|
"collectedAt": utc_now(),
|
|
1290
1394
|
"teamName": team_name,
|
|
1291
1395
|
"needleSource": needle_source,
|
|
1292
1396
|
"sessionsFound": sessions_found,
|
|
1293
|
-
"unmatchedModels":
|
|
1397
|
+
"unmatchedModels": _unmatched_usage_models(lead, workers),
|
|
1294
1398
|
"unattributedTeamSessions": unattributed_sessions or [],
|
|
1295
1399
|
"unattributedWorkerUsage": unattributed_usage,
|
|
1296
1400
|
"definitions": {
|
|
@@ -1302,18 +1406,10 @@ def _populate_usage_summary(
|
|
|
1302
1406
|
}
|
|
1303
1407
|
|
|
1304
1408
|
|
|
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)
|
|
1409
|
+
def _claude_sessions_for_run(
|
|
1410
|
+
state: dict, team_state_path: Path, cwd: Path,
|
|
1411
|
+
run_since: str | None, run_until: str | None, incremental: bool,
|
|
1412
|
+
) -> tuple:
|
|
1317
1413
|
team_name = resolve_team_name(state)
|
|
1318
1414
|
lead_sid = (state.get("lead") or {}).get("sessionId")
|
|
1319
1415
|
|
|
@@ -1341,6 +1437,13 @@ def collect_claude_runtime_usage(
|
|
|
1341
1437
|
if ts and (found_earliest is None or ts < found_earliest):
|
|
1342
1438
|
found_earliest = ts
|
|
1343
1439
|
run_since = relax_window_start(run_since, found_earliest, floor)
|
|
1440
|
+
return team_name, lead_sid, claude_sessions, run_since, needle_source
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
def _claude_session_evidence(
|
|
1444
|
+
state: dict, cwd: Path, claude_sessions: dict, lead_sid: str | None,
|
|
1445
|
+
run_since: str | None, run_until: str | None, incremental: bool,
|
|
1446
|
+
) -> tuple:
|
|
1344
1447
|
by_agent: dict[str, list[tuple[str, Path, dict]]] = {}
|
|
1345
1448
|
lead_path: Path | None = None
|
|
1346
1449
|
# Team-tagged non-lead sessions that carry no agentName. These are almost
|
|
@@ -1375,7 +1478,7 @@ def collect_claude_runtime_usage(
|
|
|
1375
1478
|
if team_create_status in ("implicit", "skipped", "error"):
|
|
1376
1479
|
worker_prefix_pool = [
|
|
1377
1480
|
prefix
|
|
1378
|
-
for w in state
|
|
1481
|
+
for w in accounting_workers(state)
|
|
1379
1482
|
for prefix in match_prefixes(w.get("workerId") or "")
|
|
1380
1483
|
]
|
|
1381
1484
|
subagent_parent_sids = [lead_sid] if lead_sid else []
|
|
@@ -1394,6 +1497,13 @@ def collect_claude_runtime_usage(
|
|
|
1394
1497
|
if agent:
|
|
1395
1498
|
by_agent.setdefault(agent, []).append((sid, path, totals))
|
|
1396
1499
|
|
|
1500
|
+
return by_agent, lead_path, unattributed_sessions, unattributed_totals
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
def _attach_claude_lead_usage(
|
|
1504
|
+
state: dict, cwd: Path, lead_path: Path | None, lead_sid: str | None,
|
|
1505
|
+
run_since: str | None, run_until: str | None, incremental: bool,
|
|
1506
|
+
) -> None:
|
|
1397
1507
|
# Lead.
|
|
1398
1508
|
if lead_path is not None:
|
|
1399
1509
|
totals = claude_session_totals(lead_path, since=run_since, until=run_until,
|
|
@@ -1406,84 +1516,93 @@ def collect_claude_runtime_usage(
|
|
|
1406
1516
|
f"lead session jsonl not found under {claude_project_dir(cwd)} (sessionId={lead_sid})"
|
|
1407
1517
|
)
|
|
1408
1518
|
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
def _matching_claude_worker_sessions(
|
|
1522
|
+
worker: dict, state: dict, cwd: Path, by_agent: dict,
|
|
1523
|
+
run_since: str | None, run_until: str | None, incremental: bool,
|
|
1524
|
+
) -> tuple:
|
|
1525
|
+
sessions_dir = claude_project_dir(cwd)
|
|
1526
|
+
worker_id = worker.get("workerId")
|
|
1527
|
+
prefixes = match_prefixes(worker_id) if worker_id else []
|
|
1528
|
+
|
|
1529
|
+
# pane 워커는 별도 `claude -p` 프로세스라 jsonl 에 agentName 도 teamName
|
|
1530
|
+
# 도 안 남긴다 — 아래 prefix 경로로는 영원히 매칭되지 않는다. dispatch 가
|
|
1531
|
+
# 발급해 적어 둔 이 id 가 그 세션을 짚는 유일한 결정적 단서다. 재시도는
|
|
1532
|
+
# attempt 마다 새 세션이므로 기록된 id 를 전부 합산한다.
|
|
1533
|
+
# 헬퍼에서 worker_id=None 은 "run 전체"라, 이름 없는 워커에 그대로 넘기면
|
|
1534
|
+
# 그 워커가 run 의 모든 세션을 흡수한다.
|
|
1535
|
+
dispatched_sids = usage_session_ids(state, worker_id) if worker_id else []
|
|
1536
|
+
matched: list[tuple[str, Path, dict]] = []
|
|
1537
|
+
matched_sids: set[str] = set()
|
|
1538
|
+
for sid in dispatched_sids:
|
|
1539
|
+
path = sessions_dir / f"{sid}.jsonl"
|
|
1540
|
+
if not path.is_file():
|
|
1541
|
+
continue
|
|
1542
|
+
totals = claude_session_totals(path, since=run_since, until=run_until,
|
|
1543
|
+
incremental=incremental)
|
|
1544
|
+
# 창 밖 세션은 여기서 버린다 — agentName 발견 경로가 위에서 같은
|
|
1545
|
+
# `startedAt` 검사로 거르는 것과 같은 이유다. 넣으면 0 토큰 totals 가
|
|
1546
|
+
# usage_block 을 타고 "이 워커는 0 을 썼다"로 보고되어, unavailable 로
|
|
1547
|
+
# 남아야 할 상태를 허위 0 이 덮는다.
|
|
1548
|
+
if not totals.get("startedAt"):
|
|
1549
|
+
continue
|
|
1550
|
+
matched.append((sid, path, totals))
|
|
1551
|
+
matched_sids.add(sid)
|
|
1552
|
+
|
|
1553
|
+
# 조건부 폴백이 아니라 합집합이다. 한 워커의 attempt 1 이 pane(sid 로만
|
|
1554
|
+
# 찾힌다), attempt 2 가 in-process 서브에이전트(agentName 으로만 찾힌다)일
|
|
1555
|
+
# 수 있고, `if not matched:` 로 두면 sid 가 하나라도 맞는 순간 attempt 2 의
|
|
1556
|
+
# 토큰이 통째로 누락된다. 양쪽에서 온 같은 세션은 sid 로 한 번만 센다.
|
|
1557
|
+
for agent_name, entries in by_agent.items():
|
|
1558
|
+
if not agent_matches(agent_name, prefixes):
|
|
1559
|
+
continue
|
|
1560
|
+
for entry in entries:
|
|
1561
|
+
if entry[0] in matched_sids:
|
|
1562
|
+
continue
|
|
1563
|
+
# team-needle 경로는 창 검사 없이 by_agent 를 채운다. 같은 리드
|
|
1564
|
+
# 세션의 다음 run 이 띄운 워커도 같은 needle 에 걸리는데, 창 밖이라
|
|
1565
|
+
# startedAt 이 없어 아래 정렬에서 맨 앞에 서고 `sessionId` 를
|
|
1566
|
+
# 차지한다 — 토큰은 0 이라 합계는 안 틀리고 리포트가 가리키는
|
|
1567
|
+
# 세션만 남의 것이 된다.
|
|
1568
|
+
if not entry[2].get("startedAt"):
|
|
1569
|
+
continue
|
|
1570
|
+
matched.append(entry)
|
|
1571
|
+
matched_sids.add(entry[0])
|
|
1572
|
+
|
|
1573
|
+
return matched, dispatched_sids, prefixes
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def _collect_claude_workers(
|
|
1577
|
+
state: dict, cwd: Path, by_agent: dict,
|
|
1578
|
+
run_since: str | None, run_until: str | None, incremental: bool,
|
|
1579
|
+
) -> set[str]:
|
|
1409
1580
|
# Workers — dispatch 가 기록한 세션 id 로 짚은 세션과 agentName prefix 로
|
|
1410
1581
|
# 찾은 세션의 합집합을 합산한다(재배치 `-002`, convergence `-reverify-r1`,
|
|
1411
1582
|
# implementation `-executor`, report-writer `-impl` / `-2` 등).
|
|
1412
|
-
sessions_dir = claude_project_dir(cwd)
|
|
1413
1583
|
# sid 로 귀속한 세션 id 전체 — 아래 unattributed 폴드에서 빼는 데 쓴다.
|
|
1414
1584
|
attributed_sids: set[str] = set()
|
|
1415
1585
|
cli_windows_by_worker = _codex_worker_windows(cwd, state)
|
|
1416
|
-
for worker in state
|
|
1586
|
+
for worker in accounting_workers(state):
|
|
1417
1587
|
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
|
-
|
|
1588
|
+
matched, dispatched_sids, prefixes = _matching_claude_worker_sessions(
|
|
1589
|
+
worker, state, cwd, by_agent, run_since, run_until, incremental,
|
|
1590
|
+
)
|
|
1591
|
+
attributed_sids.update(sid for sid, _path, _totals in matched)
|
|
1466
1592
|
if not matched:
|
|
1467
1593
|
provider = _cli_assignment_provider(worker)
|
|
1468
1594
|
if provider and str(worker.get("runner") or "") == "cli-wrapper":
|
|
1469
1595
|
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
|
-
),
|
|
1596
|
+
provider=provider, project_root=cwd, worker=worker,
|
|
1597
|
+
records=[dict(record) for record in usage_dispatch_records(state, worker_id)]
|
|
1598
|
+
if worker_id else [],
|
|
1599
|
+
fallback_windows=cli_windows_by_worker.get(worker_id, []),
|
|
1482
1600
|
)
|
|
1483
1601
|
else:
|
|
1484
|
-
worker["usage"] =
|
|
1602
|
+
worker["usage"] = _claude_wrapper_status_usage(
|
|
1603
|
+
cwd, usage_dispatch_records(state, worker_id) if worker_id else [],
|
|
1485
1604
|
"no Claude session jsonl in the run window for dispatched "
|
|
1486
|
-
f"sessionIds {dispatched_sids} or agentName prefixes {prefixes}"
|
|
1605
|
+
f"sessionIds {dispatched_sids} or agentName prefixes {prefixes}",
|
|
1487
1606
|
)
|
|
1488
1607
|
continue
|
|
1489
1608
|
|
|
@@ -1501,21 +1620,20 @@ def collect_claude_runtime_usage(
|
|
|
1501
1620
|
# attributable provider transcript usage without treating absence as zero.
|
|
1502
1621
|
provider = _cli_assignment_provider(worker)
|
|
1503
1622
|
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,
|
|
1623
|
+
cli = _worker_cli_usage_block(
|
|
1624
|
+
provider=provider, project_root=cwd, worker=worker,
|
|
1625
|
+
records=[dict(record) for record in usage_dispatch_records(state, worker_id)],
|
|
1626
|
+
fallback_windows=cli_windows_by_worker.get(worker_id, []),
|
|
1516
1627
|
)
|
|
1628
|
+
_attach_cli_usage(block, cli)
|
|
1517
1629
|
worker["usage"] = block
|
|
1518
1630
|
|
|
1631
|
+
return attributed_sids
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
def _unattributed_claude_usage(
|
|
1635
|
+
unattributed_sessions: list[str], unattributed_totals: list[dict], attributed_sids: set[str],
|
|
1636
|
+
) -> tuple[list[str], dict | None]:
|
|
1519
1637
|
# Fold team-tagged worker sessions that carry no agentName into the worker
|
|
1520
1638
|
# pool. They cannot be mapped to a specific workerId (so each named worker
|
|
1521
1639
|
# row above stays `unavailable`), but the tokens are real team-worker spend —
|
|
@@ -1551,12 +1669,31 @@ def collect_claude_runtime_usage(
|
|
|
1551
1669
|
"cannot be mapped to a specific workerId."
|
|
1552
1670
|
)
|
|
1553
1671
|
|
|
1672
|
+
return unattributed_sessions, unattributed_usage
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def collect_claude_runtime_usage(
|
|
1676
|
+
team_state_path: Path, project_root: Path | None = None, *, incremental: bool = True,
|
|
1677
|
+
) -> dict:
|
|
1678
|
+
state = json.loads(team_state_path.read_text())
|
|
1679
|
+
state["additionalWorkerUsage"] = unrostered_usage_workers(state)
|
|
1680
|
+
cwd = project_root or _infer_project_root(team_state_path, state)
|
|
1681
|
+
run_since, run_until = resolve_run_window(team_state_path, state)
|
|
1682
|
+
team_name, lead_sid, sessions, run_since, needle_source = _claude_sessions_for_run(
|
|
1683
|
+
state, team_state_path, cwd, run_since, run_until, incremental,
|
|
1684
|
+
)
|
|
1685
|
+
by_agent, lead_path, unattributed_sessions, unattributed_totals = _claude_session_evidence(
|
|
1686
|
+
state, cwd, sessions, lead_sid, run_since, run_until, incremental,
|
|
1687
|
+
)
|
|
1688
|
+
_attach_claude_lead_usage(state, cwd, lead_path, lead_sid, run_since, run_until, incremental)
|
|
1689
|
+
attributed = _collect_claude_workers(state, cwd, by_agent, run_since, run_until, incremental)
|
|
1690
|
+
unattributed_sessions, unattributed_usage = _unattributed_claude_usage(
|
|
1691
|
+
unattributed_sessions, unattributed_totals, attributed,
|
|
1692
|
+
)
|
|
1693
|
+
_remove_shared_codex_usage(state)
|
|
1554
1694
|
_populate_usage_summary(
|
|
1555
|
-
state,
|
|
1556
|
-
|
|
1557
|
-
sessions_found=len(claude_sessions),
|
|
1558
|
-
unattributed_sessions=unattributed_sessions,
|
|
1559
|
-
unattributed_usage=unattributed_usage,
|
|
1695
|
+
state, team_name=team_name, sessions_found=len(sessions),
|
|
1696
|
+
unattributed_sessions=unattributed_sessions, unattributed_usage=unattributed_usage,
|
|
1560
1697
|
needle_source=needle_source,
|
|
1561
1698
|
)
|
|
1562
1699
|
return state
|
|
@@ -18,9 +18,8 @@ from pathlib import Path
|
|
|
18
18
|
|
|
19
19
|
from okstra_project.dirs import okstra_home
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
CACHE_SCHEMA_VERSION = 2
|
|
21
|
+
# 응답 ID가 없는 이전 캐시는 중복 여부를 복구할 수 없어 원본부터 재스캔한다.
|
|
22
|
+
CACHE_SCHEMA_VERSION = 3
|
|
24
23
|
IDENTITY_PREFIX_BYTES = 256
|
|
25
24
|
MAX_NEEDLES = 16
|
|
26
25
|
|