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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.188.0",
3
+ "version": "0.188.1",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.188.0",
3
- "builtAt": "2026-09-03T19:30:32.235Z",
2
+ "package": "0.188.1",
3
+ "builtAt": "2026-09-03T21:11:05.123Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -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 DispatchError, link_agent_dispatch_result
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
- None if lead_cost is None or worker_cost is None else lead_cost + worker_cost,
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 = state.get("runEndedAt") or _run_end_estimate(run_dir, suffix) or utc_now()
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)