okstra 0.188.0 → 0.188.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/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -36,6 +36,7 @@ from __future__ import annotations
|
|
|
36
36
|
|
|
37
37
|
import argparse
|
|
38
38
|
import json
|
|
39
|
+
from datetime import datetime, timezone
|
|
39
40
|
import subprocess
|
|
40
41
|
import sys
|
|
41
42
|
from dataclasses import dataclass
|
|
@@ -45,7 +46,11 @@ from typing import Any, Callable, Mapping, Sequence
|
|
|
45
46
|
from .agent.activity import ActivityProjectionError, project_agent_activity
|
|
46
47
|
from .report_contract import apply_execution_roles
|
|
47
48
|
from .report_assembly import ReportAssemblyError, assemble_report
|
|
48
|
-
from .dispatch_state import
|
|
49
|
+
from .dispatch_state import (
|
|
50
|
+
DispatchError,
|
|
51
|
+
link_agent_dispatch_result,
|
|
52
|
+
mutate_team_state,
|
|
53
|
+
)
|
|
49
54
|
from .final_report_paths import (
|
|
50
55
|
final_report_data_path,
|
|
51
56
|
final_report_markdown_path,
|
|
@@ -495,6 +500,7 @@ def run_finalize(
|
|
|
495
500
|
commands = [(name, cmd) for name, cmd in commands if name in selected]
|
|
496
501
|
|
|
497
502
|
first_failure = ""
|
|
503
|
+
validated = False
|
|
498
504
|
for name, command in commands:
|
|
499
505
|
# 실패한 시퀀스가 worktree 를 거두면 재작업 대상이 사라진다.
|
|
500
506
|
if name == STEP_TEARDOWN_STAGES and first_failure:
|
|
@@ -505,6 +511,10 @@ def run_finalize(
|
|
|
505
511
|
steps.append(step_payload(name, command, result))
|
|
506
512
|
if result.returncode != 0 and not first_failure:
|
|
507
513
|
first_failure = f"{name} failed with exit code {result.returncode}"
|
|
514
|
+
if name == STEP_VALIDATE_RUN and result.returncode == 0:
|
|
515
|
+
validated = True
|
|
516
|
+
if validated:
|
|
517
|
+
_record_run_end(ctx.team_state_path)
|
|
508
518
|
pointer, pointer_error = _recorded_next_phase(ctx)
|
|
509
519
|
payload: dict[str, Any] = {
|
|
510
520
|
"ok": not first_failure,
|
|
@@ -519,6 +529,35 @@ def run_finalize(
|
|
|
519
529
|
return payload
|
|
520
530
|
|
|
521
531
|
|
|
532
|
+
def _record_run_end(team_state_path: Path) -> None:
|
|
533
|
+
"""검증이 처음 통과한 시각을 `team-state.runEndedAt` 에 적는다. 이미 있으면 그대로.
|
|
534
|
+
|
|
535
|
+
토큰 수집기(`okstra_token_usage.collect.resolve_run_window`)는 이 값을 창의
|
|
536
|
+
끝으로 읽는데 어떤 코드도 적지 않았다. 그래서 완료된 run 을 다시 finalize
|
|
537
|
+
하면 창의 끝이 지금이 되어, 같은 세션이 그 뒤에 돌린 run 이 이 run 의 리드
|
|
538
|
+
사용량에 들어왔다(관측 2026-09-03, dev-10626 error-analysis r04: 하루 뒤
|
|
539
|
+
재수집에 오늘 run 이 포함, 리드 3h → 11h). run 이 끝났다고 판정하는 자리는
|
|
540
|
+
검증 통과이므로 여기서 한 번만 적는다. 실패해도 finalize 는 이미 끝난
|
|
541
|
+
시퀀스라 결과를 바꾸지 않고 경고만 낸다.
|
|
542
|
+
"""
|
|
543
|
+
ended = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
544
|
+
|
|
545
|
+
def stamp(state: dict[str, Any]) -> bool:
|
|
546
|
+
if state.get("runEndedAt"):
|
|
547
|
+
return False
|
|
548
|
+
state["runEndedAt"] = ended
|
|
549
|
+
return True
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
mutate_team_state(team_state_path, stamp)
|
|
553
|
+
except (DispatchError, OSError) as exc:
|
|
554
|
+
print(
|
|
555
|
+
f"report-finalize: could not record runEndedAt on {team_state_path} "
|
|
556
|
+
f"({exc}); the next usage collection will end at the current time",
|
|
557
|
+
file=sys.stderr,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
522
561
|
def _recorded_next_phase(ctx: FinalizeContext) -> tuple[dict[str, str], str]:
|
|
523
562
|
"""`validate-run` 이 방금 태스크 매니페스트에 쓴 다음 phase 포인터.
|
|
524
563
|
|
|
@@ -191,7 +191,9 @@ def project_token_usage(team_state: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
191
191
|
"grand": _usage_row(
|
|
192
192
|
summary,
|
|
193
193
|
"grand",
|
|
194
|
-
|
|
194
|
+
# 두 소수의 합은 부동소수점 잔재(85.05619999999999)를 남긴다 — 정본
|
|
195
|
+
# JSON 에는 수집기와 같은 소수 넷째 자리로 적는다.
|
|
196
|
+
None if lead_cost is None or worker_cost is None else round(lead_cost + worker_cost, 4),
|
|
195
197
|
),
|
|
196
198
|
"workerDetails": _worker_details(team_state),
|
|
197
199
|
"cli": {"costUsd": costs.get("cliWorkers")},
|
|
@@ -230,6 +230,32 @@ def run_window_relaxation_floor(team_state_path: Path) -> str | None:
|
|
|
230
230
|
return relaxation_floor(run_dir, suffix, manifest)
|
|
231
231
|
|
|
232
232
|
|
|
233
|
+
def _recorded_run_end(state: dict, manifest: dict | None) -> str | None:
|
|
234
|
+
"""완료된 run 이 지난 수집에서 기록한 창의 끝 — 리드·워커 usage 블록의 endedAt 최댓값.
|
|
235
|
+
|
|
236
|
+
run 의 끝을 적는 코드가 없다(`runEndedAt` 은 읽기만, status 파일은 안 쓰인다).
|
|
237
|
+
그래서 완료된 run 을 다시 수집하면 창의 끝이 지금이 되어, 같은 세션이 그
|
|
238
|
+
뒤에 돌린 다른 run 까지 이 run 의 리드 창에 들어온다(관측 2026-09-03:
|
|
239
|
+
dev-10626 error-analysis r04 를 하루 뒤 재finalize 하면 오늘 run 이 들어옴).
|
|
240
|
+
매니페스트가 `completed` 인 run 은 처음 완료됐을 때 수집한 창이 곧 run 의
|
|
241
|
+
끝이므로 그 값으로 고정한다. 아직 완료 전이면 None — 리드가 서술문을 고쳐
|
|
242
|
+
Phase 7 을 다시 도는 동안은 창이 지금까지 늘어나는 것이 맞다.
|
|
243
|
+
"""
|
|
244
|
+
if not isinstance(manifest, dict) or manifest.get("status") != "completed":
|
|
245
|
+
return None
|
|
246
|
+
ends: list[str] = []
|
|
247
|
+
blocks = [state.get("leadUsage")] + [
|
|
248
|
+
worker.get("usage") for worker in (state.get("workers") or []) if isinstance(worker, dict)
|
|
249
|
+
]
|
|
250
|
+
for block in blocks:
|
|
251
|
+
if not isinstance(block, dict) or block.get("source") == "unavailable":
|
|
252
|
+
continue
|
|
253
|
+
ended = block.get("endedAt")
|
|
254
|
+
if isinstance(ended, str) and ended:
|
|
255
|
+
ends.append(ended)
|
|
256
|
+
return max(ends) if ends else None
|
|
257
|
+
|
|
258
|
+
|
|
233
259
|
def _run_end_estimate(run_dir: Path, suffix: str) -> str | None:
|
|
234
260
|
"""run 종료 근사 — 같은 run 의 status 산출물 mtime(reconcile 후 고정, Phase 7
|
|
235
261
|
재렌더로도 바뀌지 않음). 완료 전(status 부재)이면 None."""
|
|
@@ -353,7 +379,8 @@ def resolve_run_window(
|
|
|
353
379
|
섞여 폭증한다(관측: requirements-discovery 한 run 에 lead 1.7억 토큰 /
|
|
354
380
|
$416 / 3h). 토큰 집계를 이 윈도우로 스코핑해 그 run 분만 센다. 시작 =
|
|
355
381
|
이 run 의 run-manifest createdAt, 종료 = team-state.runEndedAt → 이 run 의
|
|
356
|
-
status mtime →
|
|
382
|
+
status mtime → 완료된 run 이 지난 수집에서 기록한 창의 끝(`_recorded_run_end`)
|
|
383
|
+
→ 현재 시각(아직 진행 중) 순으로 해소한다. 접미사를 못 뽑으면
|
|
357
384
|
(None, None) — 윈도우 없이 전체를 세는 기존 동작으로 안전 폴백.
|
|
358
385
|
|
|
359
386
|
시작 완화(`relax_start`, 기본 True — 토큰 수집용): run-manifest createdAt 이
|
|
@@ -386,7 +413,12 @@ def resolve_run_window(
|
|
|
386
413
|
if not window_start_is_pinned(since, floor):
|
|
387
414
|
earliest = _earliest_lead_session_ts(state, team_state_path)
|
|
388
415
|
since = relax_window_start(since, earliest, floor)
|
|
389
|
-
until =
|
|
416
|
+
until = (
|
|
417
|
+
state.get("runEndedAt")
|
|
418
|
+
or _run_end_estimate(run_dir, suffix)
|
|
419
|
+
or _recorded_run_end(state, manifest)
|
|
420
|
+
or utc_now()
|
|
421
|
+
)
|
|
390
422
|
return since, until
|
|
391
423
|
|
|
392
424
|
|
|
@@ -238,7 +238,7 @@ def populate_data_token_cells(data_path: Path, team_state: dict) -> int:
|
|
|
238
238
|
"billableTokens": summary.get("grandBillableEquivalentTokens"),
|
|
239
239
|
# CLI tracked on its own row per the template — grand here means
|
|
240
240
|
# lead + claudeWorkers, not lead + claudeWorkers + cli.
|
|
241
|
-
"costUsd": lead_cost + worker_cost,
|
|
241
|
+
"costUsd": round(lead_cost + worker_cost, 4),
|
|
242
242
|
})
|
|
243
243
|
token_usage.setdefault("cli", {})["costUsd"] = cli_cost
|
|
244
244
|
token_usage["workerDetails"] = _worker_detail_rows(team_state)
|