elanous 0.2.23 → 0.2.24

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.
Files changed (347) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{272-b23d2e92161d11fd.js → 272-0eed0fb0aab716b6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{3961-fbef064c213b2b18.js → 3961-aaae04ed1d32622f.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d5341ac96203a983.js → page-3b9fc39362473ae7.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f2b62c19ca68fef8.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/board/{page-55c631c032dcc47e.js → page-d7602d489edf5f37.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-ad3bd8084048bee4.js → page-24282bf9f90031a5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/page-8b7c5ab6b5bc8d64.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5c5d0e370f495ef6.js → page-4eb84d454f8c3415.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-c325e21543cd48a8.js → page-b2e494bdad57fcb5.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/{page-6168a7c43e6ef8f5.js → page-c0ab229673a8b0eb.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-3e2b45f3c36dbac4.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/page-c22aa511b28743ad.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/page-26345242e4049f55.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/editor/{page-25bb425afdb27541.js → page-bf88c0f558d8ef54.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-32d72fdef1afe11a.js → page-52fec523e5b5d3ad.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/page-67723dfdcde92b31.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/inside/{page-2bd9ce9c020d02d4.js → page-22725e2570f1126b.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a34096245e63b9f2.js → page-1891335950d68b70.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/layout-3267af1ebdc73cf5.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-4cc66d22ca4ec502.js → page-c314b75c11234998.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-83b14c729fcea837.js → page-9f9c85415f61782b.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-c1d4276059085587.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-833aab587f26ad4c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-ccdb83ed40c8d724.js → page-8526c17ae38e6a50.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-7ad3de8fa06fdea9.js → page-f998637a8402115c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-b7bb53828b17b335.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1b1850de0257e46d.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-786111f30b6f0969.js → page-6f23159fedb41f67.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-48fb31810693c357.js → page-1d809a87139dd024.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4e17cce5e2ed0fee.js → page-da9e570234d481fd.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-cb154acadb025414.js → page-b92caddb5c715f30.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-8ce3a38e210b810a.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-c2dd2b19c93f90ab.js → page-57ffeeb6019440fd.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-abbb417bf514630b.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-d3c007c8a0ddd7ee.js → page-2c2b3f29cec1efa4.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7837ce2aacb97b04.js → page-9369279f72fc7b51.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/page-64861f14039499c3.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-ddc4c93efd8ab94b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-901a1391b81053bc.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/page-68d9bbaac9841a58.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-119be9a0444d2482.js → page-b5c0c996f329371e.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-fa806c018833e332.js → page-89881ccbcf728978.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-0e87a803b3d7165e.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-d431fbdea6ad93dd.js → page-58d4e5cb30ef4871.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-02ce4c3b5f39e4d2.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/page-e620d1bb8eadcec2.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-847c3de6ca8a4416.js → page-acf868c64d77f950.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-54fa605fa6dba457.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-74e69ba2c8e970d4.js → page-bc79f0833533fa84.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-70038f41ae7cf931.js → page-d5c4398eea03d8f3.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/72becfd42807b50d.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -1
  65. package/apps/pwa/out/ceo/index.txt +14 -14
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/decisions/index.html +1 -1
  73. package/apps/pwa/out/decisions/index.txt +14 -14
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +14 -14
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +14 -14
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +14 -14
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +14 -14
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +14 -14
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +14 -14
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +14 -14
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +14 -14
  91. package/apps/pwa/out/live-run/index.html +1 -1
  92. package/apps/pwa/out/live-run/index.txt +14 -14
  93. package/apps/pwa/out/loops/index.html +1 -1
  94. package/apps/pwa/out/loops/index.txt +14 -14
  95. package/apps/pwa/out/market/index.html +1 -1
  96. package/apps/pwa/out/market/index.txt +14 -14
  97. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  98. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  99. package/apps/pwa/out/morning/index.html +1 -1
  100. package/apps/pwa/out/morning/index.txt +14 -14
  101. package/apps/pwa/out/observatory/index.html +1 -1
  102. package/apps/pwa/out/observatory/index.txt +14 -14
  103. package/apps/pwa/out/ops/checklist/index.html +1 -1
  104. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  105. package/apps/pwa/out/ops/release/index.html +1 -1
  106. package/apps/pwa/out/ops/release/index.txt +14 -14
  107. package/apps/pwa/out/ops/seats/index.html +1 -1
  108. package/apps/pwa/out/ops/seats/index.txt +14 -14
  109. package/apps/pwa/out/outputs/index.html +1 -1
  110. package/apps/pwa/out/outputs/index.txt +14 -14
  111. package/apps/pwa/out/reflection/index.html +1 -1
  112. package/apps/pwa/out/reflection/index.txt +15 -15
  113. package/apps/pwa/out/scheduler/index.html +1 -1
  114. package/apps/pwa/out/scheduler/index.txt +14 -14
  115. package/apps/pwa/out/sessions/index.html +1 -1
  116. package/apps/pwa/out/sessions/index.txt +14 -14
  117. package/apps/pwa/out/settings/devices/index.html +1 -1
  118. package/apps/pwa/out/settings/devices/index.txt +14 -14
  119. package/apps/pwa/out/settings/index.html +1 -1
  120. package/apps/pwa/out/settings/index.txt +14 -14
  121. package/apps/pwa/out/setup/done/index.html +1 -1
  122. package/apps/pwa/out/setup/done/index.txt +14 -14
  123. package/apps/pwa/out/setup/index.html +1 -1
  124. package/apps/pwa/out/setup/index.txt +14 -14
  125. package/apps/pwa/out/share/index.html +1 -1
  126. package/apps/pwa/out/share/index.txt +14 -14
  127. package/apps/pwa/out/showroom/index.html +1 -1
  128. package/apps/pwa/out/showroom/index.txt +14 -14
  129. package/apps/pwa/out/tasks/index.html +1 -1
  130. package/apps/pwa/out/tasks/index.txt +14 -14
  131. package/apps/pwa/out/term/index.html +1 -1
  132. package/apps/pwa/out/term/index.txt +14 -14
  133. package/apps/pwa/out/today/index.html +1 -1
  134. package/apps/pwa/out/today/index.txt +14 -14
  135. package/apps/pwa/out/trace/index.html +1 -1
  136. package/apps/pwa/out/trace/index.txt +14 -14
  137. package/apps/pwa/out/vault/index.html +1 -1
  138. package/apps/pwa/out/vault/index.txt +14 -14
  139. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  140. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  141. package/apps/pwa/out/workflows/index.html +1 -1
  142. package/apps/pwa/out/workflows/index.txt +14 -14
  143. package/apps/pwa/out/workspace/index.html +1 -1
  144. package/apps/pwa/out/workspace/index.txt +14 -14
  145. package/apps/pwa/out/worktrees/index.html +1 -1
  146. package/apps/pwa/out/worktrees/index.txt +14 -14
  147. package/catalog/models/openrouter/deepseek-v4-pro-0813.yaml +39 -0
  148. package/catalog/models/openrouter/deepseek-v4.1-flash.yaml +38 -0
  149. package/catalog/models/openrouter/glm-5.3-flash.yaml +35 -0
  150. package/catalog/providers/openrouter.yaml +2 -0
  151. package/graphs/implement-loop.yaml +31 -0
  152. package/graphs/ops/harness-gc.yaml +17 -0
  153. package/graphs/ops/recipes.yaml +3 -0
  154. package/graphs/overlays/README.md +1 -1
  155. package/graphs/overlays/stop-heal-ladder.yaml +105 -0
  156. package/package.json +1 -1
  157. package/scripts/add-api-key.sh +1 -1
  158. package/scripts/ci-typecheck-changed.ts +71 -14
  159. package/scripts/fleet/bedrock-pod-secret.ts +122 -0
  160. package/scripts/fleet/node-setup-linux.sh +97 -0
  161. package/scripts/fleet/node-setup.sh +11 -0
  162. package/scripts/fleet/zai-pod-secret.ts +86 -0
  163. package/scripts/land-global-checks.ts +3 -0
  164. package/scripts/measure-log-diet-2.ts +213 -0
  165. package/scripts/measure-tsc-affected-roots.ts +289 -0
  166. package/scripts/release-loop/gate-executor.ts +28 -0
  167. package/scripts/release-loop/gate-node.ts +262 -26
  168. package/scripts/release-loop/resume-release.ts +9 -3
  169. package/scripts/release-loop/resume.ts +41 -0
  170. package/scripts/release-loop/unattended-release.ts +22 -7
  171. package/scripts/tsc-baseline-cache.ts +147 -0
  172. package/scripts/usage-rollup.ts +58 -6
  173. package/src/agent/run-context.ts +10 -0
  174. package/src/agent-mission/driver.ts +18 -3
  175. package/src/agent-mission/resource-ladder.ts +8 -3
  176. package/src/agent-mission/review-loop.ts +8 -2
  177. package/src/agent-substrate/acp-implementer.ts +139 -0
  178. package/src/agent-substrate/pr-reviewer.ts +31 -4
  179. package/src/agent-substrate/self-review-cli.ts +48 -9
  180. package/src/autopilot/mission-critique.ts +18 -5
  181. package/src/autopilot/mission-feasibility-negotiate.ts +11 -10
  182. package/src/autopilot/mission-trace.ts +12 -1
  183. package/src/autopilot/system-lookback-run.ts +16 -10
  184. package/src/bench/bench.ts +183 -0
  185. package/src/budget/fetchers/grok.ts +15 -5
  186. package/src/budget/llm-cost.ts +37 -5
  187. package/src/budget/types.ts +8 -2
  188. package/src/budget/unified-usage.ts +88 -6
  189. package/src/cli/bench-cli.ts +34 -0
  190. package/src/cli/guardian-cli.ts +26 -1
  191. package/src/cli/image-cli.ts +101 -0
  192. package/src/cli/log-event-names.ts +4 -0
  193. package/src/cli/pr-cli.ts +62 -5
  194. package/src/cli/pr-land-overlap-evidence.ts +160 -12
  195. package/src/cli/release-cli.ts +18 -9
  196. package/src/cli/rfc-status.ts +52 -2
  197. package/src/cli/self-update.ts +104 -0
  198. package/src/cli/tasks-cli.ts +109 -4
  199. package/src/cli/usage-cli.ts +91 -8
  200. package/src/core-turn/run-goal-loop.ts +5 -4
  201. package/src/dashboard/ad-run-setup-bindings.ts +279 -0
  202. package/src/dashboard/index.ts +18 -276
  203. package/src/dashboard/stale-install-check.ts +11 -0
  204. package/src/debug/log-diet.ts +38 -7
  205. package/src/debug/log.ts +19 -1
  206. package/src/domains/release-tool.ts +39 -0
  207. package/src/error-report/report.ts +21 -4
  208. package/src/github/pr-labels.ts +1 -0
  209. package/src/graph-runner/graph-cli.ts +42 -1
  210. package/src/graph-runner/runner.ts +167 -6
  211. package/src/grok/usage.ts +23 -2
  212. package/src/grounding/ref-index.ts +253 -0
  213. package/src/harness/dispatch-backpressure.ts +10 -2
  214. package/src/harness/harness-clean.ts +199 -78
  215. package/src/harness/harness-cli-command.ts +122 -7
  216. package/src/harness/harness-gc.ts +331 -0
  217. package/src/harness/harness-stop.ts +4 -2
  218. package/src/harness/harness-substrate-default.ts +142 -7
  219. package/src/harness/launch-registry.ts +6 -0
  220. package/src/harness/worktree-branch-prefix.ts +9 -0
  221. package/src/hq/fence.ts +187 -0
  222. package/src/hq/seats.ts +38 -22
  223. package/src/images/image-ledger.ts +179 -0
  224. package/src/index.ts +93 -11
  225. package/src/instance/child-scope.ts +32 -1
  226. package/src/instance/dev-project.ts +133 -0
  227. package/src/instance/resolve.ts +24 -0
  228. package/src/instance/spawn-elanous.ts +45 -0
  229. package/src/intake-plane/ref-prefetch.ts +110 -0
  230. package/src/intelligence-map/model-catalog.ts +72 -0
  231. package/src/llm/bedrock-eventstream.ts +215 -0
  232. package/src/llm/bedrock-provider.ts +114 -41
  233. package/src/llm/bedrock.ts +174 -28
  234. package/src/llm/openrouter-byok-cost.ts +96 -0
  235. package/src/llm/openrouter-deepseek.ts +151 -0
  236. package/src/llm/openrouter-upstream-pins.config.ts +25 -0
  237. package/src/llm/zai-coding-plan.ts +700 -0
  238. package/src/llm.ts +87 -16
  239. package/src/loops/checker.ts +148 -9
  240. package/src/loops/orchestrator/resync-shadow.ts +34 -7
  241. package/src/maturity/feature-maturity.ts +2 -2
  242. package/src/model-tier/llm-tier-map.ts +9 -5
  243. package/src/models/prompts.ts +2 -1
  244. package/src/mss/logging/log-store.ts +58 -11
  245. package/src/nexus/api/missions.ts +73 -2
  246. package/src/nexus/api/ops-api.ts +18 -0
  247. package/src/nexus/index.ts +7 -75
  248. package/src/nexus/tools-kind.ts +78 -0
  249. package/src/oauth/codex-account-rotation.ts +39 -2
  250. package/src/oauth/codex-account-store.ts +323 -24
  251. package/src/oauth/fallback-chain.ts +53 -15
  252. package/src/onboarding/first-run.ts +37 -18
  253. package/src/onboarding/web-first.ts +20 -2
  254. package/src/ops/machine-heartbeat.ts +185 -0
  255. package/src/ops/overview-cache.ts +175 -0
  256. package/src/ops/overview.ts +8 -0
  257. package/src/prompt-cache/openai.ts +3 -0
  258. package/src/prompt-cache/types.ts +5 -0
  259. package/src/prompt-enhance/enhance.ts +23 -0
  260. package/src/release-loop/placement.ts +16 -1
  261. package/src/requirements/requirement-ledger.ts +536 -0
  262. package/src/retro/daily.ts +5 -5
  263. package/src/seat-loop/seat-loop.ts +32 -10
  264. package/src/self-dev/dev-cli.ts +44 -10
  265. package/src/self-dev/dev-pipeline.ts +7 -2
  266. package/src/self-dev/draft-residue.ts +14 -2
  267. package/src/self-dev/draft-sweep.ts +103 -0
  268. package/src/self-dev/graph-journey-route.ts +6 -0
  269. package/src/self-dev/orchestrate-cli.ts +25 -3
  270. package/src/self-dev/orchestrate.ts +41 -6
  271. package/src/self-dev/run-supervisor.ts +188 -15
  272. package/src/self-dev/run-types.ts +13 -0
  273. package/src/self-dev/supervisor-verdict-edges.ts +1 -0
  274. package/src/self-implement/budget-gate.ts +53 -13
  275. package/src/self-implement/child-context-composition.ts +139 -0
  276. package/src/self-implement/child-llm-preference.ts +13 -5
  277. package/src/self-implement/claude-acp-implement.ts +67 -0
  278. package/src/self-implement/flex-signals.ts +59 -0
  279. package/src/self-implement/frozen-merges.ts +8 -3
  280. package/src/self-implement/goal-author-decompose.ts +10 -2
  281. package/src/self-implement/goal-author.ts +24 -9
  282. package/src/self-implement/graph-overlay-condition.ts +28 -0
  283. package/src/self-implement/graph-overlay-yaml.ts +41 -7
  284. package/src/self-implement/graph-templates.ts +33 -1
  285. package/src/self-implement/graph-yaml.ts +56 -0
  286. package/src/self-implement/heal-intake.ts +5 -1
  287. package/src/self-implement/orchestrator.ts +144 -15
  288. package/src/self-implement/running-runs.ts +75 -5
  289. package/src/self-implement/seams.ts +47 -1
  290. package/src/self-implement/self-implement-cli.ts +9 -4
  291. package/src/self-implement/stop-autoheal.ts +69 -2
  292. package/src/session-runtime/retry-policy.ts +2 -1
  293. package/src/steward/launch.ts +5 -4
  294. package/src/steward/triage-budget.ts +62 -0
  295. package/src/steward/triage.ts +83 -22
  296. package/src/task-agent/actions.ts +62 -5
  297. package/src/task-agent/completion-evidence.ts +5 -0
  298. package/src/task-agent/cover.ts +101 -3
  299. package/src/task-agent/judge.ts +16 -0
  300. package/src/task-agent/land-order-ledger.ts +95 -0
  301. package/src/task-agent/live-move-history.ts +87 -1
  302. package/src/task-agent/live-moves.ts +350 -20
  303. package/src/task-agent/rejudge-on-head.ts +75 -20
  304. package/src/task-agent/review-prior-fix.ts +146 -0
  305. package/src/task-agent/run-adopt.ts +136 -0
  306. package/src/task-agent/shadow.ts +53 -9
  307. package/src/task-agent/task-hand.ts +54 -7
  308. package/src/task-orchestrator/surfaces/pod-account-broker.ts +38 -5
  309. package/src/task-orchestrator/surfaces/pod-bedrock-child.ts +74 -0
  310. package/src/task-orchestrator/surfaces/pod-child-default.ts +50 -0
  311. package/src/task-orchestrator/surfaces/pod-child-providers.ts +14 -1
  312. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +79 -6
  313. package/src/task-orchestrator/surfaces/pod-lease.ts +17 -5
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  315. package/src/task-orchestrator/surfaces/pod-pool.ts +20 -4
  316. package/src/task-orchestrator/surfaces/pod-zai-child.ts +101 -0
  317. package/src/task-orchestrator/surfaces/self-implement-pod.ts +213 -23
  318. package/src/task-orchestrator/surfaces/self-implement.ts +19 -1
  319. package/src/typecheck-ratchet.ts +1 -0
  320. package/src/user-config.ts +51 -11
  321. package/src/version/packed-revision.json +1 -1
  322. package/src/workflow-runtime/triggers/registry.ts +59 -0
  323. package/src/workflow-runtime/triggers/webhook-source.ts +35 -5
  324. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-0160ab83c8685a12.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/bots/page-e6784759a2531031.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/control/page-31417d3604154a6d.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/decisions/page-e9606dc0bdec9305.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a648bc464c9dc754.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/field/page-d27b794c8ba76bbb.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/layout-22170f4f00a15496.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/loops/page-6e2781c523dad761.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/market/page-26dd415c8f13f341.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/not-found-015c02c4fec4023e.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6b0ae5b5c42f6d2f.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/page-16258e956ea93fc2.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f35dd1dd96ae87.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/settings/page-7350ee93f4fe1033.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-00723458575f2b19.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/setup/page-745d9b6c015076f8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/share/page-736d33c7414a8a1a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/term/page-c782041b5f5199ba.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/trace/page-0c056abff82b9891.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/vault/page-242d668e4fbf72c4.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/workflows/page-81dd0d8ae923ed72.js +0 -1
  345. package/apps/pwa/out/_next/static/css/5619d59e87eddf66.css +0 -1
  346. /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_buildManifest.js +0 -0
  347. /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_ssgManifest.js +0 -0
@@ -0,0 +1,700 @@
1
+ // ── ZAI-CODING-PLAN-PROVIDER — GLM Coding Plan(Z.ai) 직결 ──
2
+ //
3
+ // 대표 2026-10-11 결정(A안): OpenRouter 를 거치지 않고 기존 `glm` 공급자 «안에서» 코딩 엔드포인트를 고른다.
4
+ // ⛔ 공급자 «이름»을 늘리지 않는다 — `glm` 은 이미 union·사다리에 있다(llm-tier-map 의 Record 를 안 건드린다).
5
+ // ⛔ 이용 규정상 «지원 도구 밖» 위험은 대표 가 알고 택했다 — 그래서 ***위장하지 않는다***(헤더 꾸미기 0).
6
+ //
7
+ // 이 파일 = 엔드포인트 선택 · 동시 상한 · 오류 판별 · 정책 결정 · 폴백 · 크레딧 적산 · 공식 사용량 조회.
8
+ // `llm.ts` 는 배선(한 분기 ⊕ export 한 줄)만 갖는다(OP Q3 · RFC llm-ts-split · Bedrock #25966 선례).
9
+ // ⛔ 순환 방지: `../llm.js` 는 «타입»만 정적으로 가져오고, 값(호환 공급자·OpenRouter 공급자)은 호출 때 동적 import.
10
+ //
11
+ // 근거 문서 = PR #26038 `docs/reference/zai-glm-coding-plan/` — 오류 코드 `api-reference__api-code.md` ·
12
+ // 크레딧·창 `devpack__overview.md`(Max = 5h 28,000 / 주 140,000 · 비첨두 50% · 첨두 = 평일 14–18시 UTC+8).
13
+ // ⛔ 키 값은 어떤 로그·오류·요약에도 싣지 않는다 — 관측은 «있음/없음»만.
14
+
15
+ import type { LLMProvider, LLMStreamEvent, LLMOpts, LLMMessage } from '../llm.js';
16
+ import type { LLMConfig as UCLLMConfig } from '../user-config.js';
17
+ import { appendFileSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
18
+ import { dirname, join } from 'node:path';
19
+ import { debug } from '../debug/log.js';
20
+ import { keyFromCacheOrEnv } from '../config.js';
21
+ import { quotaSignalDir } from '../budget/codex-reset-credit-state.js';
22
+
23
+ /** 코딩 엔드포인트(OpenAI 호환). FAQ: "API endpoint for other tools is `https://api.z.ai/api/coding/paas/v4`". */
24
+ export const ZAI_CODING_PLAN_URL = 'https://api.z.ai/api/coding/paas/v4/chat/completions';
25
+ /** 공식 사용량 조회(문서엔 없다 · 2026-10-11 실측 200 — ⓒ). 응답 `data.limits[]` = 5h·주 창. */
26
+ export const ZAI_QUOTA_URL = 'https://api.z.ai/api/monitor/usage/quota/limit';
27
+ export const ZAI_CODING_DEFAULT_MODEL = 'glm-5.3';
28
+ export const ZAI_CODING_MODELS = ['glm-5.3', 'glm-5.3-flash'] as const;
29
+ /** 폴백 대상(사다리 `openrouter` balanced 와 같은 id). */
30
+ export const ZAI_OPENROUTER_FALLBACK_MODEL = 'openrouter/z-ai/glm-5.3';
31
+ export const ZAI_DEFAULT_MAX_CONCURRENT = 3;
32
+ /** Max 한도(공식 조회가 되면 그 값이 이긴다). */
33
+ export const ZAI_MAX_PLAN_LIMITS = { fiveHour: 28_000, weekly: 140_000 } as const;
34
+ const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
35
+ const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
36
+
37
+ export type ZaiOveragePolicy = 'stop' | 'balance' | 'openrouter';
38
+ export const ZAI_OVERAGE_POLICIES: readonly ZaiOveragePolicy[] = ['stop', 'balance', 'openrouter'];
39
+ /** 기본 = `stop` — 잔액도 OpenRouter 도 «돈»이다. 명시할 때만 연다(codex `credits` 와 같은 fail-closed 결). */
40
+ export const ZAI_DEFAULT_OVERAGE_POLICY: ZaiOveragePolicy = 'stop';
41
+
42
+ export function isZaiOveragePolicy(v: unknown): v is ZaiOveragePolicy {
43
+ return typeof v === 'string' && (ZAI_OVERAGE_POLICIES as readonly string[]).includes(v);
44
+ }
45
+
46
+ export interface ZaiCodingPlanSettings {
47
+ overage: ZaiOveragePolicy;
48
+ maxConcurrent: number;
49
+ }
50
+
51
+ export function zaiSettingsFromConfig(cfg: Pick<UCLLMConfig, 'zaiCodingPlan'> | undefined): ZaiCodingPlanSettings {
52
+ const raw = cfg?.zaiCodingPlan;
53
+ const max = raw?.maxConcurrent;
54
+ return {
55
+ overage: isZaiOveragePolicy(raw?.overage) ? raw.overage : ZAI_DEFAULT_OVERAGE_POLICY,
56
+ maxConcurrent: typeof max === 'number' && Number.isSafeInteger(max) && max >= 1 ? max : ZAI_DEFAULT_MAX_CONCURRENT,
57
+ };
58
+ }
59
+
60
+ // ── 자격 ────────────────────────────────────────────────
61
+
62
+ export interface ZaiKeyDeps {
63
+ /** 시험 seam — 기본은 키 캐시(`~/.cache/zai_api_key`) → env `ZAI_API_KEY`. */
64
+ readonly key?: () => string | undefined;
65
+ }
66
+
67
+ export function getZaiApiKey(deps: ZaiKeyDeps = {}): string | undefined {
68
+ const v = (deps.key ?? (() => keyFromCacheOrEnv('ZAI_API_KEY')))();
69
+ return v && v.trim() ? v.trim() : undefined;
70
+ }
71
+
72
+ /** ⭐ 직결 경로가 «서나» — 키가 없으면 false 이고 `glm` 은 종전 동작(OpenRouter 안내 오류) 그대로다. */
73
+ export function zaiCodingPlanAvailable(deps: ZaiKeyDeps = {}): boolean {
74
+ return getZaiApiKey(deps) !== undefined;
75
+ }
76
+
77
+ // ── 크레딧 ──────────────────────────────────────────────
78
+
79
+ const CREDIT_RATES: Record<'glm-5.3' | 'glm-5.3-flash', { input: number; cached: number; output: number }> = {
80
+ 'glm-5.3': { input: 6.9, cached: 1.7, output: 24 },
81
+ 'glm-5.3-flash': { input: 2.3, cached: 0.56, output: 8 },
82
+ };
83
+
84
+ /** 첨두 = 월–금 14:00–18:00 싱가포르 시각(UTC+8). 그 밖(주말 포함)은 비첨두 50%. */
85
+ export function isZaiPeak(nowMs: number): boolean {
86
+ const sg = new Date(nowMs + 8 * 60 * 60 * 1000);
87
+ const day = sg.getUTCDay();
88
+ const hour = sg.getUTCHours();
89
+ return day >= 1 && day <= 5 && hour >= 14 && hour < 18;
90
+ }
91
+
92
+ export function zaiRateFamily(model: string): 'glm-5.3' | 'glm-5.3-flash' {
93
+ return /flash/i.test(model) ? 'glm-5.3-flash' : 'glm-5.3';
94
+ }
95
+
96
+ /** 크레딧 = (입력×in + 캐시입력×cached + 출력×out)/10,000 × (첨두 1 · 비첨두 0.5).
97
+ * ⚠️ `input` 은 «새 입력만»(elanous 규약 · `parseOpenAIUsage` 가 캐시분을 이미 뺐다). */
98
+ export function zaiCredits(
99
+ model: string,
100
+ usage: { input: number; cached: number; output: number },
101
+ nowMs: number,
102
+ ): number {
103
+ const r = CREDIT_RATES[zaiRateFamily(model)];
104
+ const base = (usage.input * r.input + usage.cached * r.cached + usage.output * r.output) / 10_000;
105
+ // ⛔ 반올림하지 않는다 — 작은 호출이 0 으로 접혀 적산이 실제보다 낮아진다(반올림은 표시 때만).
106
+ return base * (isZaiPeak(nowMs) ? 1 : 0.5);
107
+ }
108
+
109
+ // ── 오류 판별 ───────────────────────────────────────────
110
+
111
+ /** plan-limit = 플랜 창 소진(잔액 사용 꺼짐) · balance-limit = 잔액·월 상한 소진 · plan-unavailable = 플랜 자체가 막힘. */
112
+ export type ZaiLimitKind = 'plan-limit' | 'balance-limit' | 'plan-unavailable';
113
+ export type ZaiLimitWindow = '5h' | 'weekly';
114
+
115
+ export interface ZaiErrorClass {
116
+ code: number;
117
+ kind: ZaiLimitKind | 'rate' | 'other';
118
+ window: ZaiLimitWindow | null;
119
+ /** epoch ms — `next_flush_time` 에서. 못 읽으면 null. */
120
+ resetAt: number | null;
121
+ /** 원문 시각 문자열(관측용 · 키 아님). */
122
+ resetRaw: string | null;
123
+ }
124
+
125
+ const PLAN_LIMIT = new Set([1308, 1310]);
126
+ const BALANCE_LIMIT = new Set([1113, 1316, 1317, 1318, 1319, 1320, 1321]);
127
+ const PLAN_UNAVAILABLE = new Set([1309, 1311, 1313, 1314, 1315]);
128
+ const RATE = new Set([1302, 1305]);
129
+ const FIVE_H_CODES = new Set([1316, 1318, 1320]);
130
+ const WEEK_CODES = new Set([1310, 1317, 1319, 1321]);
131
+
132
+ /** `{"error":{"code":"1316","message":"… Resets at 2026-10-11 09:12:33."}}` → 판별. JSON 이 아니면 null. */
133
+ export function classifyZaiError(status: number, bodyText: string): ZaiErrorClass | null {
134
+ let parsed: unknown;
135
+ try { parsed = JSON.parse(bodyText); } catch { return null; }
136
+ const err = (parsed as { error?: { code?: unknown; message?: unknown; next_flush_time?: unknown } } | null)?.error;
137
+ const code = Number(err?.code);
138
+ if (!err || !Number.isFinite(code)) return null;
139
+ const message = typeof err.message === 'string' ? err.message : '';
140
+ const kind: ZaiErrorClass['kind'] = PLAN_LIMIT.has(code) ? 'plan-limit'
141
+ : BALANCE_LIMIT.has(code) ? 'balance-limit'
142
+ : PLAN_UNAVAILABLE.has(code) ? 'plan-unavailable'
143
+ : RATE.has(code) ? 'rate' : 'other';
144
+ const window: ZaiLimitWindow | null = FIVE_H_CODES.has(code) ? '5h'
145
+ : WEEK_CODES.has(code) ? 'weekly'
146
+ : code === 1308 ? (/hour/i.test(message) ? '5h' : /week|day/i.test(message) ? 'weekly' : null)
147
+ : null;
148
+ const flush = parseNextFlushTime(err.next_flush_time ?? (parsed as Record<string, unknown>).next_flush_time, message);
149
+ void status;
150
+ return { code, kind, window, resetAt: flush.at, resetRaw: flush.raw };
151
+ }
152
+
153
+ /** `next_flush_time` — 숫자(epoch s/ms) 또는 문자열. 시간대 없는 «YYYY-MM-DD HH:mm:ss» 는 UTC+8(Z.ai 기준 시각)로 읽는다.
154
+ * ⚠️ 그 가정은 관측(`resetRaw`)으로 남겨 실측과 대조한다. */
155
+ export function parseNextFlushTime(field: unknown, message: string): { at: number | null; raw: string | null } {
156
+ if (typeof field === 'number' && Number.isFinite(field)) {
157
+ return { at: field < 1e12 ? field * 1000 : field, raw: String(field) };
158
+ }
159
+ const candidates = [typeof field === 'string' ? field : '', message];
160
+ for (const text of candidates) {
161
+ if (!text) continue;
162
+ const epoch = text.match(/\b(\d{13}|\d{10})\b/);
163
+ const iso = text.match(/(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}(?::\d{2})?)(?:\.\d+)?\s*(Z|[+-]\d{2}:?\d{2})?/);
164
+ if (iso) {
165
+ const tz = iso[3] ? (iso[3] === 'Z' ? 'Z' : iso[3].includes(':') ? iso[3] : `${iso[3].slice(0, 3)}:${iso[3].slice(3)}`) : '+08:00';
166
+ const time = iso[2].length === 5 ? `${iso[2]}:00` : iso[2];
167
+ const at = Date.parse(`${iso[1]}T${time}${tz}`);
168
+ if (Number.isFinite(at)) return { at, raw: iso[0] };
169
+ }
170
+ if (epoch) {
171
+ const n = Number(epoch[1]);
172
+ return { at: epoch[1].length === 10 ? n * 1000 : n, raw: epoch[1] };
173
+ }
174
+ }
175
+ return { at: null, raw: null };
176
+ }
177
+
178
+ export type ZaiAction = 'call' | 'stop' | 'openrouter';
179
+
180
+ /** 정책 결정표 — `pre` = 호출 전(기록된 한도가 아직 살아 있다) · `post` = 방금 서버가 한도 오류를 냈다.
181
+ * - stop → 언제나 멈춘다(돈 0).
182
+ * - openrouter → 언제나 OpenRouter 로.
183
+ * - balance → 플랜 창만 찼으면(pre) 그대로 호출(서버가 잔액으로 «extra usage» 를 쓴다) ·
184
+ * 잔액·월 상한(1113/1316~1321)이나 서버가 막았으면 다음 칸(OpenRouter). */
185
+ export function decideZaiAction(policy: ZaiOveragePolicy, kind: ZaiLimitKind, phase: 'pre' | 'post'): ZaiAction {
186
+ if (policy === 'stop') return 'stop';
187
+ if (policy === 'openrouter') return 'openrouter';
188
+ return kind === 'plan-limit' && phase === 'pre' ? 'call' : 'openrouter';
189
+ }
190
+
191
+ // ── 원장(적산) ──────────────────────────────────────────
192
+
193
+ export interface ZaiCallRecord {
194
+ ts: number; kind: 'call'; model: string;
195
+ input: number; cached: number; output: number;
196
+ credits: number; peak: boolean;
197
+ /** plan = 구독 창 · balance = 창이 찬 뒤 잔액(extra usage)로 갔다고 «추정»한 호출. */
198
+ bucket: 'plan' | 'balance';
199
+ }
200
+ export interface ZaiLimitRecord {
201
+ ts: number; kind: 'limit'; code: number; limitKind: ZaiLimitKind;
202
+ window: ZaiLimitWindow | null; resetAt: number | null; model: string;
203
+ }
204
+ export type ZaiLedgerRecord = ZaiCallRecord | ZaiLimitRecord;
205
+
206
+ export interface ZaiLedgerDeps {
207
+ /** 원장 파일 경로 — 기본 `<자격 뿌리>/budget/zai-coding-plan-ledger.jsonl`(계정 자원이라 우주를 안 가른다 · codex 신호와 같은 뿌리). */
208
+ readonly ledgerPath?: string;
209
+ readonly now?: () => number;
210
+ }
211
+
212
+ export function zaiLedgerPath(deps: ZaiLedgerDeps = {}): string {
213
+ return deps.ledgerPath ?? join(quotaSignalDir(), 'zai-coding-plan-ledger.jsonl');
214
+ }
215
+
216
+ export function appendZaiLedger(record: ZaiLedgerRecord, deps: ZaiLedgerDeps = {}): void {
217
+ const path = zaiLedgerPath(deps);
218
+ try {
219
+ mkdirSync(dirname(path), { recursive: true });
220
+ appendFileSync(path, `${JSON.stringify(record)}\n`, { mode: 0o600 });
221
+ } catch (error) {
222
+ debug.log('zai.coding-plan', 'ledger-write-failed', { error: error instanceof Error ? error.message : String(error) }, { level: 'warn' });
223
+ }
224
+ }
225
+
226
+ /** 최근 8일만 읽는다(주 창 + 여유). 깨진 줄은 버린다.
227
+ * ⭐ 버린 줄이 많으면(≥ 1,000) 남길 줄만으로 파일을 다시 쓴다 — 원장이 끝없이 자라지 않는다. */
228
+ export function readZaiLedger(deps: ZaiLedgerDeps = {}): ZaiLedgerRecord[] {
229
+ const now = (deps.now ?? Date.now)();
230
+ const path = zaiLedgerPath(deps);
231
+ let text = '';
232
+ try { text = readFileSync(path, 'utf8'); } catch { return []; }
233
+ const out: ZaiLedgerRecord[] = [];
234
+ let dropped = 0;
235
+ for (const line of text.split('\n')) {
236
+ if (!line.trim()) continue;
237
+ try {
238
+ const r = JSON.parse(line) as ZaiLedgerRecord;
239
+ if (typeof r?.ts === 'number' && r.ts >= now - WEEK_MS - 24 * 60 * 60 * 1000) out.push(r);
240
+ else dropped += 1;
241
+ } catch { dropped += 1; }
242
+ }
243
+ if (dropped >= ZAI_LEDGER_COMPACT_THRESHOLD) {
244
+ try {
245
+ // 임시 파일 → rename(원자 교체). ⚠️ 읽기~교체 사이 다른 프로세스의 append 는 잃을 수 있다 — 드문 압축 순간에 한하고 적산은 추정이다.
246
+ const tmpPath = `${path}.${process.pid}.tmp`;
247
+ writeFileSync(tmpPath, out.map((r) => JSON.stringify(r)).join('\n') + (out.length > 0 ? '\n' : ''), { mode: 0o600 });
248
+ renameSync(tmpPath, path);
249
+ debug.log('zai.coding-plan', 'ledger-compacted', { dropped, kept: out.length });
250
+ } catch { /* 다음 읽기에 다시 시도한다 */ }
251
+ }
252
+ return out;
253
+ }
254
+ export const ZAI_LEDGER_COMPACT_THRESHOLD = 1_000;
255
+
256
+ /** 아직 살아 있는 한도 기록(가장 최근 · 리셋 시각이 미래). 리셋 시각을 모르면 15분만 유효(그 뒤엔 다시 찔러 본다). */
257
+ export function activeZaiLimit(records: readonly ZaiLedgerRecord[], nowMs: number): ZaiLimitRecord | null {
258
+ let best: ZaiLimitRecord | null = null;
259
+ for (const r of records) {
260
+ if (r.kind !== 'limit') continue;
261
+ const until = r.resetAt ?? r.ts + 15 * 60 * 1000;
262
+ if (until <= nowMs) continue;
263
+ // 같은 창의 더 뒤 «성공» 호출이 있으면 그 한도는 풀린 것이다(리셋 카드 등).
264
+ const cleared = records.some((c) => c.kind === 'call' && c.ts > r.ts && c.bucket === 'plan');
265
+ if (cleared && r.limitKind === 'plan-limit') continue;
266
+ if (!best || r.ts > best.ts) best = r;
267
+ }
268
+ return best;
269
+ }
270
+
271
+ export interface ZaiLocalWindows {
272
+ fiveHour: { used: number; oldestTs: number | null };
273
+ weekly: { used: number; sinceTs: number | null };
274
+ balanceCredits: number;
275
+ calls: number;
276
+ }
277
+
278
+ /** ⓐ 우리 쪽 적산 — 5h 는 «소비 후 5시간에 풀린다»(롤링). 주 창은 기준 시각을 모르면 롤링 7일로 «추정». */
279
+ export function sumZaiLocalWindows(records: readonly ZaiLedgerRecord[], nowMs: number, weeklyStart?: number | null): ZaiLocalWindows {
280
+ const fiveFrom = nowMs - FIVE_HOURS_MS;
281
+ const weekFrom = weeklyStart && weeklyStart <= nowMs ? weeklyStart : nowMs - WEEK_MS;
282
+ let five = 0; let week = 0; let balance = 0; let calls = 0; let oldest: number | null = null;
283
+ for (const r of records) {
284
+ if (r.kind !== 'call') continue;
285
+ if (r.ts >= weekFrom) {
286
+ calls += 1;
287
+ if (r.bucket === 'balance') { balance += r.credits; continue; }
288
+ week += r.credits;
289
+ }
290
+ if (r.ts >= fiveFrom && r.bucket === 'plan') {
291
+ five += r.credits;
292
+ if (oldest === null || r.ts < oldest) oldest = r.ts;
293
+ }
294
+ }
295
+ const round = (n: number) => Math.round(n * 1000) / 1000;
296
+ return { fiveHour: { used: round(five), oldestTs: oldest }, weekly: { used: round(week), sinceTs: weekFrom }, balanceCredits: round(balance), calls };
297
+ }
298
+
299
+ // ── 공식 사용량 조회(ⓒ) ─────────────────────────────────
300
+
301
+ export interface ZaiQuotaWindow { window: ZaiLimitWindow; limit: number; used: number; remaining: number; resetAt: number | null }
302
+ export type ZaiQuotaResult =
303
+ | { status: 'ok'; level: string | null; windows: ZaiQuotaWindow[]; fetchedAt: number }
304
+ | { status: 'unauthorized' | 'error'; detail: string };
305
+
306
+ /** 응답 `{data:{limits:[{type:'CREDIT_LIMIT',unit:3,number:5,usage,currentValue,remaining,nextResetTime}], level}}`.
307
+ * unit 3 = 시간 · unit 6 = 주(실측 2026-10-11: 3/5 → 28,000 · 6/1 → 140,000). 모르는 단위는 버린다. */
308
+ export function parseZaiQuota(body: unknown, fetchedAt: number): ZaiQuotaResult {
309
+ const data = (body as { data?: { limits?: unknown; level?: unknown } } | null)?.data;
310
+ if (!data || !Array.isArray(data.limits)) return { status: 'error', detail: 'upstream-shape' };
311
+ const windows: ZaiQuotaWindow[] = [];
312
+ for (const l of data.limits as Array<Record<string, unknown>>) {
313
+ const num = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
314
+ const unit = num(l.unit); const n = num(l.number); const limit = num(l.usage);
315
+ const used = num(l.currentValue); const remaining = num(l.remaining);
316
+ if (unit === null || n === null || limit === null) continue;
317
+ // ⛔ 쓴 양을 모르는 창은 «0» 으로 만들지 않는다 — 버리고 아래에서 불완전으로 판정한다.
318
+ if (used === null && remaining === null) continue;
319
+ const window: ZaiLimitWindow | null = unit === 3 && n === 5 ? '5h' : unit === 6 && n === 1 ? 'weekly' : null;
320
+ if (!window || windows.some((w) => w.window === window)) continue;
321
+ windows.push({
322
+ window, limit,
323
+ used: used ?? Math.max(0, limit - (remaining as number)),
324
+ remaining: remaining ?? Math.max(0, limit - (used as number)),
325
+ resetAt: num(l.nextResetTime),
326
+ });
327
+ }
328
+ // ⛔ 5h·주 «둘 다» 있어야 공식으로 쓴다 — 하나라도 빠지면 조회 실패로 보고 추정 표로 간다(행이 사라지지 않는다).
329
+ if (!windows.some((w) => w.window === '5h') || !windows.some((w) => w.window === 'weekly')) {
330
+ return { status: 'error', detail: windows.length === 0 ? 'no-known-window' : 'incomplete-windows' };
331
+ }
332
+ windows.sort((a, b) => (a.window === '5h' ? -1 : 1) - (b.window === '5h' ? -1 : 1));
333
+ return { status: 'ok', level: typeof data.level === 'string' ? data.level : null, windows, fetchedAt };
334
+ }
335
+
336
+ export interface ZaiQuotaDeps extends ZaiKeyDeps {
337
+ readonly fetchImpl?: typeof fetch;
338
+ readonly now?: () => number;
339
+ /** 마지막 공식 조회 캐시 — 호출 경로의 «잔액 귀속» 판정이 읽는다. */
340
+ readonly snapshotPath?: string;
341
+ }
342
+
343
+ function zaiQuotaSnapshotPath(deps: ZaiQuotaDeps = {}): string {
344
+ return deps.snapshotPath ?? join(quotaSignalDir(), 'zai-coding-plan-quota.json');
345
+ }
346
+
347
+ export async function fetchZaiQuota(deps: ZaiQuotaDeps = {}): Promise<ZaiQuotaResult> {
348
+ const key = getZaiApiKey(deps);
349
+ if (!key) return { status: 'error', detail: 'no-key' };
350
+ const now = (deps.now ?? Date.now)();
351
+ try {
352
+ const res = await (deps.fetchImpl ?? fetch)(ZAI_QUOTA_URL, {
353
+ headers: { authorization: `Bearer ${key}` },
354
+ signal: AbortSignal.timeout(10_000),
355
+ });
356
+ if (res.status === 401 || res.status === 403) return { status: 'unauthorized', detail: `http-${res.status}` };
357
+ if (!res.ok) return { status: 'error', detail: `http-${res.status}` };
358
+ const parsed = parseZaiQuota(await res.json(), now);
359
+ if (parsed.status === 'ok') {
360
+ try {
361
+ const path = zaiQuotaSnapshotPath(deps);
362
+ mkdirSync(dirname(path), { recursive: true });
363
+ writeFileSync(path, JSON.stringify(parsed), { mode: 0o600 });
364
+ } catch { /* 캐시는 선택 */ }
365
+ }
366
+ debug.log('zai.coding-plan', 'quota-query', {
367
+ status: parsed.status,
368
+ ...(parsed.status === 'ok' ? { windows: parsed.windows.map((w) => ({ window: w.window, used: w.used, limit: w.limit, resetAt: w.resetAt })) } : {}),
369
+ });
370
+ return parsed;
371
+ } catch (error) {
372
+ return { status: 'error', detail: error instanceof Error ? error.message : String(error) };
373
+ }
374
+ }
375
+
376
+ function readZaiQuotaSnapshot(deps: ZaiQuotaDeps = {}): Extract<ZaiQuotaResult, { status: 'ok' }> | null {
377
+ try {
378
+ const v = JSON.parse(readFileSync(zaiQuotaSnapshotPath(deps), 'utf8')) as ZaiQuotaResult;
379
+ return v.status === 'ok' ? v : null;
380
+ } catch { return null; }
381
+ }
382
+
383
+ // ── 동시 상한 ───────────────────────────────────────────
384
+
385
+ /** 프로세스 안 세마포어(⚠️ 프로세스 «사이»는 안 묶는다 — 각 프로세스가 자기 상한을 갖는다). */
386
+ export class ZaiConcurrencyGate {
387
+ private active = 0;
388
+ private readonly waiters: Array<() => void> = [];
389
+ constructor(private readonly limit: () => number) {}
390
+ get inFlight(): number { return this.active; }
391
+ get queued(): number { return this.waiters.length; }
392
+ async acquire(signal?: AbortSignal): Promise<() => void> {
393
+ // ⛔ 이미 취소된 요청은 줄을 서지 않는다 — 서면 슬롯이 풀릴 때 그 슬롯을 «주인 없이» 쥔다.
394
+ if (signal?.aborted) throw signal.reason ?? new Error('aborted');
395
+ if (this.active >= this.limit() || this.waiters.length > 0) {
396
+ await new Promise<void>((resolve, reject) => {
397
+ const onAbort = () => {
398
+ const i = this.waiters.indexOf(wake);
399
+ if (i >= 0) this.waiters.splice(i, 1);
400
+ reject(signal?.reason ?? new Error('aborted'));
401
+ };
402
+ const wake = () => { signal?.removeEventListener('abort', onAbort); resolve(); };
403
+ this.waiters.push(wake);
404
+ signal?.addEventListener('abort', onAbort, { once: true });
405
+ });
406
+ // ⭐ 슬롯은 release 가 «양도»했다(active 를 줄이지 않고 넘김) — 여기서 다시 세지 않는다.
407
+ } else {
408
+ this.active += 1;
409
+ }
410
+ let released = false;
411
+ return () => {
412
+ if (released) return;
413
+ released = true;
414
+ // ⛔ 대기자가 있으면 active 를 줄이지 않고 슬롯을 그대로 넘긴다 — 줄였다 늘리면 그 사이 새 호출이 끼어 상한을 넘는다.
415
+ const next = this.waiters.shift();
416
+ if (next) next();
417
+ else this.active -= 1;
418
+ };
419
+ }
420
+ }
421
+
422
+ /** 상한 값마다 게이트 하나 — 한 공급자를 만든다고 다른 설정의 상한이 바뀌지 않는다. */
423
+ const sharedGates = new Map<number, ZaiConcurrencyGate>();
424
+ function gateFor(limit: number): ZaiConcurrencyGate {
425
+ let gate = sharedGates.get(limit);
426
+ if (!gate) { gate = new ZaiConcurrencyGate(() => limit); sharedGates.set(limit, gate); }
427
+ return gate;
428
+ }
429
+
430
+ // ── 공급자 ──────────────────────────────────────────────
431
+
432
+ export class ZaiCodingPlanLimitError extends Error {
433
+ readonly code: number;
434
+ readonly limitKind: ZaiLimitKind;
435
+ readonly resetAt: number | null;
436
+ constructor(opts: { code: number; limitKind: ZaiLimitKind; resetAt: number | null; policy: ZaiOveragePolicy; reason: string }) {
437
+ const when = opts.resetAt ? ` · 리셋 ${new Date(opts.resetAt).toISOString()}` : '';
438
+ super(`GLM Coding Plan 한도(code ${opts.code} · ${opts.limitKind}${when}) — 정책 llm.zaiCodingPlan.overage=${opts.policy}: ${opts.reason}`);
439
+ this.name = 'ZaiCodingPlanLimitError';
440
+ this.code = opts.code;
441
+ this.limitKind = opts.limitKind;
442
+ this.resetAt = opts.resetAt;
443
+ }
444
+ }
445
+
446
+ export interface ZaiProviderDeps extends ZaiKeyDeps, ZaiLedgerDeps {
447
+ /** 시험 seam — 호환 공급자 생성기(기본 = llm.ts `makeOpenAICompatProvider`). */
448
+ readonly makeCompat?: (name: string, defaultModel: string, url: string, cfg: UCLLMConfig, wire: Record<string, unknown>) => LLMProvider;
449
+ /** 시험 seam — OpenRouter 폴백 공급자(기본 = llm.ts `makeOpenRouterProvider`). */
450
+ readonly makeOpenRouter?: (cfg: UCLLMConfig) => LLMProvider;
451
+ readonly openRouterKey?: () => string | undefined;
452
+ readonly gate?: ZaiConcurrencyGate;
453
+ readonly quotaSnapshot?: () => Extract<ZaiQuotaResult, { status: 'ok' }> | null;
454
+ }
455
+
456
+ /** 모델 id 정리 — `glm-5.3`·`glm-5.3-flash` 외에는 기본 모델로(코딩 엔드포인트가 모르는 id 를 보내 1211 을 맞지 않는다). */
457
+ export function zaiWireModel(model: string | undefined): string {
458
+ const m = (model ?? '').trim().toLowerCase().replace(/^z-ai\//, '').replace(/^openrouter\/z-ai\//, '');
459
+ return (ZAI_CODING_MODELS as readonly string[]).includes(m) ? m : ZAI_CODING_DEFAULT_MODEL;
460
+ }
461
+
462
+ /** 폴백 대상은 사양대로 «하나»(`openrouter/z-ai/glm-5.3`) — flash 호출도 같다. */
463
+ function openRouterModelFor(_wireModel: string): string {
464
+ return ZAI_OPENROUTER_FALLBACK_MODEL;
465
+ }
466
+
467
+ /** reasoningLevel → `reasoning_effort`(GLM-5.3 는 low·high·max 만). 설정이 없으면 싣지 않는다(서버 기본). */
468
+ function zaiReasoningEffort(requested: LLMOpts['reasoningEffort'], level: UCLLMConfig['reasoningLevel']): 'low' | 'high' | 'max' | undefined {
469
+ const v = requested ?? level;
470
+ if (!v) return undefined;
471
+ if (v === 'off' || v === 'minimal' || v === 'low') return 'low';
472
+ if (v === 'medium' || v === 'high') return 'high';
473
+ return 'max';
474
+ }
475
+
476
+ /**
477
+ * ⭐ GLM Coding Plan 직결 공급자. 이름은 `glm`(새 공급자 이름 0).
478
+ * 흐름: 기록된 한도 확인(pre) → 동시 상한 → 호출 → usage 적산 / 429 판별 → 정책대로 멈춤·OpenRouter.
479
+ * ⛔ 폴백은 «첫 이벤트 전»에만 한다 — 이미 흘린 출력 위에 다른 모델을 잇지 않는다.
480
+ */
481
+ export function makeZaiCodingPlanProvider(cfg: UCLLMConfig, deps: ZaiProviderDeps = {}): LLMProvider {
482
+ const settings = zaiSettingsFromConfig(cfg);
483
+ const defaultModel = zaiWireModel(cfg.model);
484
+ const now = deps.now ?? Date.now;
485
+ const gate = deps.gate ?? gateFor(settings.maxConcurrent);
486
+ const quotaSnapshot = deps.quotaSnapshot ?? (() => readZaiQuotaSnapshot());
487
+
488
+ async function compat(model: string, effort: 'low' | 'high' | 'max' | undefined): Promise<LLMProvider> {
489
+ const key = getZaiApiKey(deps);
490
+ const make = deps.makeCompat ?? ((await import('../llm.js')) as unknown as {
491
+ makeOpenAICompatProvider: NonNullable<ZaiProviderDeps['makeCompat']>;
492
+ }).makeOpenAICompatProvider;
493
+ // ⛔ cfg.baseUrl 은 넘기지 않는다 — 코딩 엔드포인트가 이 경로의 정체성이다. 키는 Z.ai 키만(남의 cfg.apiKey 금지).
494
+ return make('glm', model, ZAI_CODING_PLAN_URL, { provider: 'glm', model, apiKey: key, reasoningLevel: cfg.reasoningLevel } as UCLLMConfig, {
495
+ // GLM-5.3 은 thinking 이 «항상» 켜진다 — 기본 max_tokens 2048 이면 본문 전에 잘린다(OpenRouter 와 같은 처리).
496
+ omitMaxTokens: true,
497
+ omitDefaultTemperature: true,
498
+ ...(effort ? { extraBody: { reasoning_effort: effort } } : {}),
499
+ });
500
+ }
501
+
502
+ async function openRouter(): Promise<LLMProvider | null> {
503
+ const key = deps.openRouterKey ? deps.openRouterKey() : (await import('../config.js')).getOpenRouterApiKey();
504
+ if (!key) return null;
505
+ const make = deps.makeOpenRouter ?? ((await import('../llm.js')) as unknown as {
506
+ makeOpenRouterProvider: NonNullable<ZaiProviderDeps['makeOpenRouter']>;
507
+ }).makeOpenRouterProvider;
508
+ return make({ provider: 'openrouter', model: ZAI_OPENROUTER_FALLBACK_MODEL, reasoningLevel: cfg.reasoningLevel } as UCLLMConfig);
509
+ }
510
+
511
+ async function* viaOpenRouter(
512
+ messages: LLMMessage[], opts: LLMOpts, wireModel: string,
513
+ info: { code: number; limitKind: ZaiLimitKind; resetAt: number | null; phase: 'pre' | 'post' },
514
+ ): AsyncGenerator<LLMStreamEvent, void, unknown> {
515
+ const or = await openRouter();
516
+ const orModel = openRouterModelFor(wireModel);
517
+ debug.log('zai.coding-plan', 'fallback', {
518
+ code: info.code, limitKind: info.limitKind, resetAt: info.resetAt, phase: info.phase,
519
+ model: wireModel, to: orModel, policy: settings.overage, openRouterKey: or !== null,
520
+ }, { level: 'warn' });
521
+ if (!or) {
522
+ throw new ZaiCodingPlanLimitError({ ...info, policy: settings.overage, reason: 'OpenRouter 폴백 키(OPENROUTER_API_KEY) 없음' });
523
+ }
524
+ yield* or.streamChat!(messages, { ...opts, model: orModel });
525
+ }
526
+
527
+ return {
528
+ name: 'glm',
529
+ defaultModel,
530
+ available: () => zaiCodingPlanAvailable(deps),
531
+ async *streamChat(messages, opts = {}) {
532
+ const wireModel = zaiWireModel(opts.model || defaultModel);
533
+ // ⭐ 슬롯을 «먼저» 잡고 한도를 본다 — 기다리는 동안 앞선 호출이 429 를 적었으면 그 판단을 따른다.
534
+ const release = await gate.acquire(opts.signal);
535
+ const startedAt = now();
536
+ const ledger = readZaiLedger(deps);
537
+ const active = activeZaiLimit(ledger, startedAt) ?? snapshotLimit(quotaSnapshot(), startedAt);
538
+ if (active) {
539
+ const action = decideZaiAction(settings.overage, active.limitKind, 'pre');
540
+ debug.log('zai.coding-plan', 'pre-limit', {
541
+ code: active.code, limitKind: active.limitKind, window: active.window, resetAt: active.resetAt, model: wireModel, action, policy: settings.overage,
542
+ });
543
+ if (action === 'stop') {
544
+ release();
545
+ throw new ZaiCodingPlanLimitError({ code: active.code, limitKind: active.limitKind, resetAt: active.resetAt, policy: settings.overage, reason: '리셋까지 멈춘다' });
546
+ }
547
+ if (action === 'openrouter') {
548
+ release();
549
+ yield* viaOpenRouter(messages, opts, wireModel, { code: active.code, limitKind: active.limitKind, resetAt: active.resetAt, phase: 'pre' });
550
+ return;
551
+ }
552
+ }
553
+ let emitted = false;
554
+ let usage: { input: number; cached: number; output: number } | null = null;
555
+ try {
556
+ const effort = zaiReasoningEffort(opts.reasoningEffort, cfg.reasoningLevel);
557
+ const provider = await compat(wireModel, effort);
558
+ debug.log('zai.coding-plan', 'request', {
559
+ model: wireModel, inFlight: gate.inFlight, maxConcurrent: settings.maxConcurrent, reasoningEffort: effort ?? null,
560
+ });
561
+ try {
562
+ for await (const ev of provider.streamChat!(messages, { ...opts, model: wireModel })) {
563
+ if (ev.type === 'usage') {
564
+ usage = {
565
+ input: ev.usage.inputTokens ?? 0,
566
+ cached: ev.usage.cacheReadInputTokens ?? 0,
567
+ output: ev.usage.outputTokens ?? 0,
568
+ };
569
+ } else {
570
+ emitted = true;
571
+ }
572
+ yield ev;
573
+ }
574
+ } catch (error) {
575
+ const status = (error as { status?: unknown })?.status;
576
+ const bodyText = (error as { bodyText?: unknown })?.bodyText;
577
+ const cls = typeof status === 'number' && typeof bodyText === 'string' ? classifyZaiError(status, bodyText) : null;
578
+ if (!cls || cls.kind === 'rate' || cls.kind === 'other') {
579
+ if (cls) debug.log('zai.coding-plan', 'error', { code: cls.code, kind: cls.kind, model: wireModel }, { level: 'warn' });
580
+ throw error;
581
+ }
582
+ appendZaiLedger({ ts: now(), kind: 'limit', code: cls.code, limitKind: cls.kind, window: cls.window, resetAt: cls.resetAt, model: wireModel }, deps);
583
+ const action = decideZaiAction(settings.overage, cls.kind, 'post');
584
+ debug.log('zai.coding-plan', 'limit', {
585
+ code: cls.code, limitKind: cls.kind, window: cls.window, resetAt: cls.resetAt, resetRaw: cls.resetRaw,
586
+ model: wireModel, action, policy: settings.overage, emitted,
587
+ }, { level: 'warn' });
588
+ if (action === 'stop' || emitted) {
589
+ throw new ZaiCodingPlanLimitError({
590
+ code: cls.code, limitKind: cls.kind, resetAt: cls.resetAt, policy: settings.overage,
591
+ reason: emitted ? '출력이 이미 나간 뒤라 폴백하지 않는다' : '리셋까지 멈춘다',
592
+ });
593
+ }
594
+ release();
595
+ yield* viaOpenRouter(messages, opts, wireModel, { code: cls.code, limitKind: cls.kind, resetAt: cls.resetAt, phase: 'post' });
596
+ return;
597
+ }
598
+ } finally {
599
+ release();
600
+ if (usage) {
601
+ const ts = now();
602
+ const credits = zaiCredits(wireModel, usage, ts);
603
+ const bucket = estimateBucket(settings, readZaiLedger(deps), quotaSnapshot(), ts, active);
604
+ appendZaiLedger({ ts, kind: 'call', model: wireModel, ...usage, credits, peak: isZaiPeak(ts), bucket }, deps);
605
+ debug.log('zai.coding-plan', 'usage', { model: wireModel, ...usage, credits, bucket, peak: isZaiPeak(ts) });
606
+ }
607
+ }
608
+ },
609
+ async *chat(messages, opts = {}) {
610
+ for await (const ev of this.streamChat!(messages, opts)) {
611
+ if (ev.type === 'text' && ev.delta) yield ev.delta;
612
+ }
613
+ },
614
+ };
615
+ }
616
+
617
+ /** 10분 안의 공식 조회가 어느 창이든 남은 양 0 이라 했으면 «플랜 한도»로 본다(서버가 잔액으로 조용히 결제하기 «전»에
618
+ * stop·openrouter 정책이 막을 수 있게). 코드는 창에 맞춘 1308(5h)·1310(주)로 적는다(합성 · 관측에 window 로 남는다). */
619
+ export function snapshotLimit(snapshot: Extract<ZaiQuotaResult, { status: 'ok' }> | null, nowMs: number): ZaiLimitRecord | null {
620
+ if (!snapshot || nowMs - snapshot.fetchedAt >= 10 * 60 * 1000) return null;
621
+ const full = snapshot.windows.find((w) => w.remaining <= 0 && (w.resetAt === null || w.resetAt > nowMs));
622
+ if (!full) return null;
623
+ return { ts: snapshot.fetchedAt, kind: 'limit', code: full.window === '5h' ? 1308 : 1310, limitKind: 'plan-limit', window: full.window, resetAt: full.resetAt, model: '' };
624
+ }
625
+
626
+ /** 잔액(extra usage)으로 갔나를 «추정»한다 — 서버는 호출마다 어느 쪽에서 깎았는지 말해 주지 않는다.
627
+ * 근거 셋 중 하나: ⑴ 살아 있는 플랜 한도 기록 위에서 balance 정책으로 호출했다
628
+ * ⑵ 10분 안의 공식 조회가 어느 창이든 남은 양 0 이라 했다 ⑶ 우리 적산 5h 가 한도를 넘었다. */
629
+ function estimateBucket(
630
+ settings: ZaiCodingPlanSettings,
631
+ ledger: readonly ZaiLedgerRecord[],
632
+ snapshot: Extract<ZaiQuotaResult, { status: 'ok' }> | null,
633
+ nowMs: number,
634
+ active: ZaiLimitRecord | null,
635
+ ): 'plan' | 'balance' {
636
+ if (settings.overage !== 'balance') return 'plan';
637
+ if (active?.limitKind === 'plan-limit') return 'balance';
638
+ if (snapshot && nowMs - snapshot.fetchedAt < 10 * 60 * 1000 && snapshot.windows.some((w) => w.remaining <= 0)) return 'balance';
639
+ const local = sumZaiLocalWindows(ledger, nowMs);
640
+ return local.fiveHour.used >= ZAI_MAX_PLAN_LIMITS.fiveHour ? 'balance' : 'plan';
641
+ }
642
+
643
+ // ── usage 표면 ──────────────────────────────────────────
644
+
645
+ export interface ZaiUsageWindowView {
646
+ window: ZaiLimitWindow;
647
+ used: number;
648
+ limit: number;
649
+ resetAt: number | null;
650
+ /** official = 콘솔 조회 · next_flush_time = 429 본문 · estimated = 우리 적산(추정). */
651
+ source: 'official' | 'next_flush_time' | 'estimated';
652
+ }
653
+
654
+ export interface ZaiUsageView {
655
+ windows: ZaiUsageWindowView[];
656
+ local: ZaiLocalWindows;
657
+ balanceCredits: number;
658
+ quotaStatus: ZaiQuotaResult['status'];
659
+ quotaDetail: string | null;
660
+ lastLimit: { code: number; limitKind: ZaiLimitKind; resetAt: number | null; ts: number } | null;
661
+ }
662
+
663
+ /** 공식 조회가 되면 그것을, 안 되면 우리 적산 ⊕ 429 리셋 시각으로 «추정» 표를 만든다. */
664
+ export function buildZaiUsageView(quota: ZaiQuotaResult, ledger: readonly ZaiLedgerRecord[], nowMs: number): ZaiUsageView {
665
+ const weeklyOfficial = quota.status === 'ok' ? quota.windows.find((w) => w.window === 'weekly') : undefined;
666
+ const weeklyStart = weeklyOfficial?.resetAt ? weeklyOfficial.resetAt - WEEK_MS : null;
667
+ const local = sumZaiLocalWindows(ledger, nowMs, weeklyStart);
668
+ const limits = ledger.filter((r): r is ZaiLimitRecord => r.kind === 'limit');
669
+ const last = limits.length > 0 ? limits.reduce((a, b) => (b.ts > a.ts ? b : a)) : null;
670
+ const flushFor = (window: ZaiLimitWindow) => {
671
+ const hits = limits.filter((r) => r.window === window && r.resetAt !== null && r.resetAt > nowMs);
672
+ return hits.length > 0 ? hits.reduce((a, b) => (b.ts > a.ts ? b : a)).resetAt : null;
673
+ };
674
+ let windows: ZaiUsageWindowView[];
675
+ if (quota.status === 'ok') {
676
+ windows = quota.windows.map((w) => ({ window: w.window, used: w.used, limit: w.limit, resetAt: w.resetAt, source: 'official' as const }));
677
+ } else {
678
+ const fiveFlush = flushFor('5h');
679
+ const weekFlush = flushFor('weekly');
680
+ windows = [
681
+ {
682
+ window: '5h', used: local.fiveHour.used, limit: ZAI_MAX_PLAN_LIMITS.fiveHour,
683
+ resetAt: fiveFlush ?? (local.fiveHour.oldestTs !== null ? local.fiveHour.oldestTs + FIVE_HOURS_MS : null),
684
+ source: fiveFlush ? 'next_flush_time' : 'estimated',
685
+ },
686
+ {
687
+ window: 'weekly', used: local.weekly.used, limit: ZAI_MAX_PLAN_LIMITS.weekly,
688
+ resetAt: weekFlush, source: weekFlush ? 'next_flush_time' : 'estimated',
689
+ },
690
+ ];
691
+ }
692
+ return {
693
+ windows,
694
+ local,
695
+ balanceCredits: local.balanceCredits,
696
+ quotaStatus: quota.status,
697
+ quotaDetail: quota.status === 'ok' ? null : quota.detail,
698
+ lastLimit: last ? { code: last.code, limitKind: last.limitKind, resetAt: last.resetAt, ts: last.ts } : null,
699
+ };
700
+ }