@hydra-acp/budgeter 0.1.15 → 0.1.16
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/dist/cost/repair-cmd.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{writeFileSync as
|
|
1
|
+
import{writeFileSync as j,readFileSync as v,existsSync as C}from"node:fs";import{basename as y,resolve as w}from"node:path";import{homedir as E}from"node:os";import{buildOpencodeIndex as D,defaultOpencodeDbs as W,listSessionDirs as J,knownUpstreams as U,readGate as P,scanSession as x}from"./repair-io.js";import{planSession as _,splitLedger as H}from"./repair-core.js";import{applyPlan as L,revertSession as z,reshapeSession as M,ALGO_VERSION as Y}from"./repair-write.js";const G=`Usage: hydra-acp-budgeter repair [OPTIONS]
|
|
2
2
|
|
|
3
3
|
Reconcile recorded session cost against the agent's own ledger.
|
|
4
4
|
|
|
@@ -12,6 +12,11 @@ Dry-run by default \u2014 prints what would change and writes nothing.
|
|
|
12
12
|
OPTIONS:
|
|
13
13
|
--apply Write the repair (requires the daemon to be stopped)
|
|
14
14
|
--revert Undo a previous repair, restoring the original values
|
|
15
|
+
--reshape Re-split an already-repaired session's cost ledger into
|
|
16
|
+
cumulativeCost (retired upstreams) + costAmount (current)
|
|
17
|
+
without touching its transcript. Fixes repairs written
|
|
18
|
+
before the split existed, whose collapsed shape makes the
|
|
19
|
+
next resurrect double-count the current upstream.
|
|
15
20
|
--session <id> Limit to one session (repeatable)
|
|
16
21
|
--limit <n> Process at most n sessions, largest inflation first
|
|
17
22
|
--report <path> Write a JSON report of the plan
|
|
@@ -26,36 +31,53 @@ SAFETY
|
|
|
26
31
|
* A repaired session is gated in meta.json and will not be repaired twice;
|
|
27
32
|
to redo it, --revert first.
|
|
28
33
|
* Sessions that cannot be proven are refused, never guessed at.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
* --revert refuses a session that recorded usage after its repair: those rows
|
|
35
|
+
have no pristine value to restore, so the undo cannot be lossless. --force
|
|
36
|
+
accepts the loss.
|
|
37
|
+
* --reshape is idempotent and shape-only: for a session whose total is
|
|
38
|
+
already right it moves money between the two fields and changes no total.
|
|
39
|
+
The pre-reshape currentUsage is stamped write-once into the gate.
|
|
40
|
+
`;function V(i){const t={apply:!1,revert:!1,sessions:[],limit:void 0,report:void 0,json:!1,force:!1,skipWarm:!1,reshape:!1};for(let s=0;s<i.length;s+=1){const n=i[s];if(n==="--apply")t.apply=!0;else if(n==="--revert")t.revert=!0;else if(n==="--reshape")t.reshape=!0;else if(n==="--json")t.json=!0;else if(n==="--force")t.force=!0;else if(n==="--skip-warm")t.skipWarm=!0;else if(n==="--session")t.sessions.push(i[++s]??"");else if(n==="--limit")t.limit=Number(i[++s]);else if(n==="--report")t.report=i[++s];else{if(n==="-h"||n==="--help")return;process.stderr.write(`repair: unknown option ${n}
|
|
41
|
+
`),process.exit(2)}}return t.apply&&t.revert&&(process.stderr.write(`repair: --apply and --revert are mutually exclusive
|
|
42
|
+
`),process.exit(2)),t.reshape&&t.revert&&(process.stderr.write(`repair: --reshape and --revert are mutually exclusive
|
|
43
|
+
`),process.exit(2)),t}function q(i){const t=i??w(process.env.HYDRA_ACP_HOME??w(E(),".hydra-acp"),"daemon.pid");if(!C(t))return"down";let s;try{s=JSON.parse(v(t,"utf8")).pid}catch{return"unknown"}if(typeof s!="number")return"unknown";try{return process.kill(s,0),"up"}catch(n){return n.code==="ESRCH"?"down":"unknown"}}async function B(){const i=process.env.HYDRA_ACP_HOME??w(E(),".hydra-acp");let t,s;try{t=JSON.parse(v(w(i,"daemon.pid"),"utf8")).loopbackPort}catch{return}try{s=v(w(i,"auth-token"),"utf8").trim()}catch{return}if(!(typeof t!="number"||!s))try{const n=new AbortController,c=setTimeout(()=>n.abort(),3e3),g=await fetch(`http://127.0.0.1:${t}/v1/sessions?status=warm`,{headers:{Authorization:`Bearer ${s}`},signal:n.signal});if(clearTimeout(c),!g.ok)return;const f=await g.json();return Array.isArray(f.sessions)?new Set(f.sessions.map(p=>p.sessionId).filter(p=>!!p)):void 0}catch{return}}function m(i){return i.toLocaleString("en-US",{minimumFractionDigits:2,maximumFractionDigits:2})}async function se(i){const t=V(i);if(t===void 0){process.stdout.write(G);return}let s=new Set;if(t.skipWarm){const e=await B();e===void 0&&(process.stderr.write(`repair: --skip-warm needs the daemon's warm-session list and could not get it.
|
|
32
44
|
Refusing rather than guessing which sessions are live.
|
|
33
45
|
`),process.exit(2)),s=e,process.stderr.write(`skipping ${s.size} warm session(s) held by the running daemon
|
|
34
|
-
`)}if(
|
|
46
|
+
`)}if(t.apply||t.revert||t.reshape){const e=q();e==="up"&&t.skipWarm&&!t.force||e!=="down"&&!t.force&&(process.stderr.write(e==="up"?`repair: the hydra daemon is running.
|
|
35
47
|
Stop it first (\`hydra daemon stop\`) \u2014 a live daemon overwrites meta.json
|
|
36
48
|
from stale in-memory state and undoes half of the repair.
|
|
37
49
|
Use --force only if you are certain no session is live.
|
|
38
50
|
`:`repair: cannot determine whether the hydra daemon is running.
|
|
39
51
|
Refusing rather than risking a write against a live daemon.
|
|
40
52
|
Stop the daemon and retry, or pass --force.
|
|
41
|
-
`),process.exit(2))}let n=
|
|
42
|
-
`)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`),process.
|
|
46
|
-
|
|
47
|
-
`)
|
|
53
|
+
`),process.exit(2))}let n=J();if(t.sessions.length>0){const e=new Set(t.sessions);n=n.filter(r=>e.has(y(r))||[...e].some(o=>y(r).endsWith(o)))}if(t.revert){let e=0,r=0,o=0;for(const a of n){if(s.size>0&&s.has(y(a)))continue;const l=z(a,{force:t.force});if(l){if(l.refused!==void 0){o+=1,process.stdout.write(`refused ${y(a)} ${l.refused}
|
|
54
|
+
`);continue}e+=1,r+=l.rowsReverted,process.stdout.write(`reverted ${y(a)} (${l.rowsReverted} rows)
|
|
55
|
+
`)}}process.stdout.write(`
|
|
56
|
+
${e} session(s) reverted, ${r} rows restored.
|
|
57
|
+
`),o>0&&process.stdout.write(`${o} session(s) refused: they kept working after their repair, so a
|
|
58
|
+
revert would lose spend recorded since. Use --force to accept that.
|
|
59
|
+
`);return}const c=W();if(c.length===0&&(process.stderr.write(`repair: no OpenCode database found; nothing to reconcile against.
|
|
60
|
+
`),process.exit(1)),t.reshape){process.stderr.write(`indexing ${c.length} OpenCode database(s)\u2026
|
|
61
|
+
`);const e=D(c);let r=0,o=0,a=0,l=0;for(const T of n){const h=x(T);if(h===void 0||s.size>0&&s.has(h.input.sessionId)||P(h.meta)===void 0)continue;const A=U(h.meta);if(A.length===0)continue;const N=H(e.ledger,A,h.meta.upstreamSessionId),d=M(T,N,F(),{dryRun:!t.apply});if(d.kind==="refuse"){a+=1,process.stdout.write(`refused ${h.input.sessionId} ${d.why}
|
|
62
|
+
`);continue}if(d.kind==="skip"){o+=1;continue}r+=1,l+=d.totalDelta,process.stdout.write(`${t.apply?"reshaped":"would reshape"} ${h.input.sessionId} ${d.beforeCum.toFixed(2)}+${d.beforeCur.toFixed(2)} -> ${d.cum.toFixed(2)}+${d.cur.toFixed(2)}`+(Math.abs(d.totalDelta)>.005?` total ${d.totalDelta>0?"+":""}${d.totalDelta.toFixed(2)}`:"")+`
|
|
63
|
+
`)}process.stdout.write(`
|
|
64
|
+
${r} session(s) ${t.apply?"reshaped":"to reshape"}, ${o} already correct, ${a} refused.
|
|
65
|
+
`+(t.apply?"":`dry run \u2014 nothing written. Re-run with --apply.
|
|
66
|
+
`)+`net total change $${l.toFixed(2)} (shape-only for sessions whose total was already right)
|
|
67
|
+
`);return}process.stderr.write(`indexing ${c.length} OpenCode database(s)\u2026
|
|
68
|
+
`);const g=D(c),f=[],p=new Map;let $=0;for(const e of n){const r=x(e);if(r===void 0||s.has(r.input.sessionId))continue;if(P(r.meta)){$+=1;continue}const o=_(r.input,g.callIndex,g.ledger);if(!o.ok){const a=p.get(o.refusal.reason)??{n:0,reported:0};a.n+=1,a.reported+=r.input.reportedTotal,p.set(o.refusal.reason,a);continue}f.push({sessionId:r.input.sessionId,dir:e,reported:o.plan.reportedTotal,trueTotal:o.plan.trueTotal,rows:o.plan.rows.length,upstreams:o.plan.attribution.upstreams.length,plan:o.plan})}f.sort((e,r)=>r.reported-r.trueTotal-(e.reported-e.trueTotal));const u=t.limit===void 0?f:f.slice(0,t.limit),b=u.reduce((e,r)=>e+r.reported,0),S=u.reduce((e,r)=>e+r.trueTotal,0),O={tool:"hydra-acp-budgeter",algo:Y,at:new Date().toISOString(),mode:t.apply?"apply":"dry-run",sessions:u.map(e=>({sessionId:e.sessionId,reported:e.reported,trueTotal:e.trueTotal,rows:e.rows,upstreams:e.plan.attribution.upstreams,forwardedSkipped:e.plan.attribution.forwardedSkipped})),totals:{reported:b,trueTotal:S,phantom:b-S},refused:Object.fromEntries([...p].map(([e,r])=>[e,r])),alreadyRepaired:$,warmSkipped:s.size};if(t.report!==void 0&&j(t.report,`${JSON.stringify(O,null,2)}
|
|
69
|
+
`),t.json)process.stdout.write(`${JSON.stringify(O,null,2)}
|
|
48
70
|
`);else{process.stdout.write(`
|
|
49
71
|
${"session".padEnd(34)}${"rows".padStart(6)}${"ups".padStart(5)}${"reported".padStart(13)}${"true".padStart(12)}${"phantom".padStart(13)}
|
|
50
|
-
`);for(const e of
|
|
51
|
-
`);
|
|
72
|
+
`);for(const e of u.slice(0,25))process.stdout.write(`${e.sessionId.slice(-32).padEnd(34)}${String(e.rows).padStart(6)}${String(e.upstreams).padStart(5)}${m(e.reported).padStart(13)}${m(e.trueTotal).padStart(12)}${m(e.reported-e.trueTotal).padStart(13)}
|
|
73
|
+
`);u.length>25&&process.stdout.write(`\u2026 and ${u.length-25} more
|
|
52
74
|
`),process.stdout.write(`
|
|
53
|
-
${
|
|
54
|
-
`)
|
|
55
|
-
`);for(const[e,
|
|
56
|
-
`)}if(!
|
|
75
|
+
${u.length} session(s) repairable: $${m(b)} recorded -> $${m(S)} actual ($${m(b-S)} phantom)
|
|
76
|
+
`),$>0&&process.stdout.write(`${$} session(s) already repaired (use --revert to redo)
|
|
77
|
+
`);for(const[e,r]of[...p].sort((o,a)=>a[1].reported-o[1].reported))process.stdout.write(`refused: ${e.padEnd(22)} ${String(r.n).padStart(4)} sessions $${m(r.reported)}
|
|
78
|
+
`)}if(!t.apply){const e=`
|
|
57
79
|
Dry run \u2014 nothing written. Re-run with --apply to commit.
|
|
58
|
-
`;
|
|
59
|
-
applied to ${
|
|
60
|
-
`);for(const e of
|
|
61
|
-
`);
|
|
80
|
+
`;t.json?process.stderr.write(e):process.stdout.write(e);return}let R=0,I=0;const k=[];for(const e of u){const r=x(e.dir);if(r!==void 0)try{const o=L(r,e.plan,F());R+=1,I+=o.rowsRewritten}catch(o){k.push(`${e.sessionId}: ${o.message}`)}}process.stdout.write(`
|
|
81
|
+
applied to ${R} session(s), ${I} rows rewritten.
|
|
82
|
+
`);for(const e of k)process.stderr.write(`FAILED ${e}
|
|
83
|
+
`);k.length>0&&(process.exitCode=1)}function F(){try{const i=w(import.meta.dirname,"../../package.json");return JSON.parse(v(i,"utf8")).version??"unknown"}catch{return"unknown"}}export{G as REPAIR_HELP,q as daemonState,se as runRepair};
|
package/dist/cost/repair-core.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
function y(e,l,a){const n=l.map(t=>{const d=e.get(t)??[];let u=0;for(const[,i]of d)u+=i;return{upstreamSessionId:t,cost:u,firstAt:d[0]?.[0]??Number.MAX_SAFE_INTEGER,lastAt:d[d.length-1]?.[0]??0,isCurrent:t===a}});n.sort((t,d)=>t.firstAt-d.firstAt);const o=a!==void 0&&n.some(t=>t.isCurrent);let r=0,s=0;for(const t of n)!o||t.isCurrent?s+=t.cost:r+=t.cost;return{retired:r,current:s,perUpstream:n}}function c(e,l){const a=new Set;let n=0,o=0,r=0;for(const s of e){if(s.forwarded){r+=1;continue}const t=l.get(s.toolCallId);if(t===void 0){o+=1;continue}a.add(t),n+=1}return{upstreams:[...a].sort(),ownResolved:n,ownUnresolved:o,forwardedSkipped:r}}function m(e,l,a){let n=0;for(const o of l)for(const[r,s]of e.get(o)??[]){if(r>a)break;n+=s}return n}function g(e,l,a){const n=[];for(const t of l)for(const d of e.get(t)??[])n.push(d);n.sort((t,d)=>t[0]-d[0]);const o=[];let r=0,s=0;for(const t of a){for(;r<n.length&&n[r][0]<=t;)s+=n[r][1],r+=1;o.push(s)}return o}function b(e,l){const a=[];for(const r of e)if(r.repaired<-1e-6){a.push(`negative repaired value ${r.repaired} at ${r.file}:${r.line}`);break}for(let r=1;r<e.length;r+=1)if(e[r].repaired<e[r-1].repaired-1e-6){a.push(`repaired series decreases at ${e[r].file}:${e[r].line} (${e[r-1].repaired} -> ${e[r].repaired})`);break}for(let r=0;r<e.length;r+=1){const s=Math.max(e[r].amount,e[r+1]?.amount??e[r].amount);if(e[r].repaired>s+1e-6){a.push(`repaired ${e[r].repaired} exceeds recorded ${e[r].amount} at ${e[r].file}:${e[r].line}`);break}}const o=e[e.length-1];return o!==void 0&&o.repaired>l+1e-6&&a.push(`repaired final ${o.repaired} exceeds meta.json total ${l}`),{ok:a.length===0,violations:a}}function R(e,l,a){const n=(i,p,f)=>({ok:!1,refusal:{sessionId:e.sessionId,reason:i,detail:p,...f?{attribution:f}:{}}});if(e.reportedTotal<=0)return n("zero-cost","nothing to repair");if(e.forkedFromSessionId)return n("fork",`forked from ${e.forkedFromSessionId}`);const o=c(e.toolCalls,l);if(o.upstreams.length===0)return n("no-own-upstream",`no tool call resolves to an agent session (${o.ownUnresolved} unresolved, ${o.forwardedSkipped} forwarded)`,o);if(o.ownUnresolved>o.ownResolved)return n("majority-unresolved",`${o.ownUnresolved} of ${o.ownUnresolved+o.ownResolved} own tool ids unresolved \u2014 ledger partially purged, total would under-report`,o);const r=g(a,o.upstreams,e.usageRows.map(i=>i.recordedAt)),s=e.usageRows.map((i,p)=>({...i,repaired:r[p]??0})),t=b(s,e.reportedTotal);if(!t.ok)return n("guard-violation",t.violations.join("; "),o);const d=m(a,o.upstreams,Number.MAX_SAFE_INTEGER),u=y(a,o.upstreams,e.currentUpstream);return{ok:!0,plan:{sessionId:e.sessionId,attribution:o,rows:s,reportedTotal:e.reportedTotal,trueTotal:d,split:u}}}export{c as attribute,b as checkGuards,R as planSession,y as splitLedger,m as truthAt,g as truthSeries};
|
package/dist/cost/repair-io.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{existsSync as
|
|
2
|
-
`)})}catch(
|
|
1
|
+
import{existsSync as p,readdirSync as S,readFileSync as I}from"node:fs";import{gunzipSync as w}from"node:zlib";import{homedir as x}from"node:os";import{resolve as d}from"node:path";import{DatabaseSync as A}from"node:sqlite";import{logger as R}from"../util/log.js";const l=R("cost/repair-io"),_="hydra-acp-budgeter",b="costRepair",f=/^history\.jsonl(\.(\d+)(\.gz)?)?$/;function U(){const n=process.env.OPENCODE_DATA_DIR??d(x(),".local/share/opencode");return["opencode.db","opencode-local.db"].map(e=>d(n,e)).filter(e=>p(e))}function j(n){const e=new Map,s=new Map;for(const t of n){let r;try{r=new A(t,{readOnly:!0})}catch(o){l.warn(`cannot open ${t}: ${o.message}`);continue}try{for(const o of r.prepare("select session_id, data from part where data like '%callID%'").all()){const a=o.data.indexOf('"callID":"');if(a<0)continue;const i=o.data.indexOf('"',a+10);i<0||e.set(o.data.slice(a+10,i),o.session_id)}for(const o of r.prepare("select session_id, data from message").all()){let a;try{a=JSON.parse(o.data)}catch{continue}if(a.role!=="assistant")continue;const i=a.time?.created;if(typeof i!="number")continue;const c=s.get(o.session_id)??[];c.push([i,a.cost??0]),s.set(o.session_id,c)}}catch(o){l.warn(`reading ${t} failed: ${o.message}`)}finally{r.close()}}for(const t of s.values())t.sort((r,o)=>r[0]-o[0]);return l.debug(`indexed ${e.size} tool calls, ${s.size} agent sessions`),{callIndex:e,ledger:s}}function E(n){const e=S(n).filter(t=>f.test(t));e.sort((t,r)=>{const o=t==="history.jsonl"?Number.MAX_SAFE_INTEGER:Number(f.exec(t)?.[2]??0),a=r==="history.jsonl"?Number.MAX_SAFE_INTEGER:Number(f.exec(r)?.[2]??0);return o-a});const s=[];for(const t of e)try{const r=I(d(n,t)),o=t.endsWith(".gz")?w(r).toString("utf8"):r.toString("utf8");s.push({name:t,gz:t.endsWith(".gz"),lines:o.split(`
|
|
2
|
+
`)})}catch(r){l.warn(`reading ${t} failed: ${r.message}`)}return s}function k(n){try{return JSON.parse(I(d(n,"meta.json"),"utf8"))}catch{return}}function C(n){const e=n.currentUsage??{},s=typeof e.costAmount=="number"?e.costAmount:0,t=typeof e.cumulativeCost=="number"?e.cumulativeCost:0;return s+t}function O(n){const s=n.extensionState?.[_]?.[b];return s&&typeof s=="object"?s:void 0}function v(n){const e=O(n),s=[],t=r=>{typeof r=="string"&&r.length>0&&!s.includes(r)&&s.push(r)};if(e!==void 0&&Array.isArray(e.upstreams))for(const r of e.upstreams)t(r);for(const r of n.upstreamGenerations??[])t(r.upstreamSessionId);return t(n.upstreamSessionId),s}function G(n){const e=k(n);if(e===void 0)return;const s=E(n),t=[],r=[];for(const o of s)for(let a=0;a<o.lines.length;a+=1){const i=o.lines[a];if(i.length===0||!i.includes('"usage_update"')&&!i.includes('"toolCallId"'))continue;let c;try{c=JSON.parse(i)}catch{continue}const u=c.params?.update;if(!u)continue;const g=u._meta?.["hydra-acp"];if(u.sessionUpdate==="usage_update"){const h=u.cost,m=g?.[b]?.original,y=typeof m=="number"?m:h?.amount;typeof y=="number"&&typeof c.recordedAt=="number"&&t.push({file:o.name,line:a,recordedAt:c.recordedAt,amount:y});continue}typeof u.toolCallId=="string"&&r.push({toolCallId:u.toolCallId,forwarded:g?.planner!==void 0})}return t.sort((o,a)=>o.recordedAt-a.recordedAt),{dir:n,meta:e,files:s,input:{sessionId:e.sessionId,agentId:e.agentId,forkedFromSessionId:e.forkedFromSessionId,reportedTotal:C(e),usageRows:t,toolCalls:r,currentUpstream:e.upstreamSessionId}}}function M(){const n=process.env.HYDRA_ACP_HOME??d(x(),".hydra-acp");return d(n,"sessions")}function H(){const n=M();return p(n)?S(n).map(e=>d(n,e)).filter(e=>p(d(e,"meta.json"))):[]}export{f as ARCHIVE_RE,_ as BUDGETER_NS,b as REPAIR_KEY,j as buildOpencodeIndex,U as defaultOpencodeDbs,M as hydraSessionsDir,v as knownUpstreams,H as listSessionDirs,O as readGate,k as readMeta,E as readTranscriptFiles,C as reportedTotalOf,G as scanSession};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import{renameSync as
|
|
2
|
-
`);return
|
|
3
|
-
`)}function
|
|
4
|
-
`)
|
|
1
|
+
import{renameSync as U,writeFileSync as C,readFileSync as h,statSync as $,utimesSync as A}from"node:fs";import{gzipSync as j}from"node:zlib";import{resolve as R}from"node:path";import{logger as E}from"../util/log.js";import{BUDGETER_NS as f,REPAIR_KEY as g,readMeta as J,readTranscriptFiles as k}from"./repair-io.js";const S=E("cost/repair-write"),x=1,v=1;function O(n){return`${f}@${n}`}function M(n,r){const s=JSON.parse(n),i=s.params.update,o=i._meta??{},e=o["hydra-acp"]??{},t=i.cost??{},u=e[g],a=u&&typeof u.original=="number"?u.original:t.amount;return e[g]={v,algo:x,original:a},o["hydra-acp"]=e,i._meta=o,i.cost={...t,amount:r},JSON.stringify(s)}function T(n){const r=JSON.parse(n),s=r.params.update,i=s._meta,o=i?.["hydra-acp"],e=o?.[g];if(e)return typeof e.original=="number"&&(s.cost={...s.cost??{},amount:e.original}),delete o[g],Object.keys(o).length===0&&delete i["hydra-acp"],Object.keys(i).length===0&&delete s._meta,JSON.stringify(r)}function b(n,r){const s=`${n}.repair-tmp`;let i;try{const o=$(n);i={atime:o.atime,mtime:o.mtime}}catch{i=void 0}C(s,r,{mode:384}),U(s,n),i&&A(n,i.atime,i.mtime)}function N(n){const r=n.lines.join(`
|
|
2
|
+
`);return n.gz?j(Buffer.from(r,"utf8")):r}function B(n,r,s){const i=new Map;for(const t of r.rows){if(Math.abs(t.repaired-t.amount)<1e-9)continue;const u=i.get(t.file)??new Map;u.set(t.line,t.repaired),i.set(t.file,u)}const o=[];let e=0;for(const t of n.files){const u=i.get(t.name);if(!u||u.size===0)continue;const a=[...t.lines];for(const[d,c]of u){const l=a[d];if(l===void 0||l.length===0)throw new Error(`${t.name}:${d} missing while applying repair`);a[d]=M(l,c),e+=1}b(R(n.dir,t.name),N({...t,lines:a})),o.push(t.name)}return _(n,r,e,s),{filesRewritten:o,rowsRewritten:e}}function _(n,r,s,i){const o=R(n.dir,"meta.json"),e=JSON.parse(h(o,"utf8")),t=e.currentUsage??{},u={v,algo:x,tool:O(i),source:"opencode-ledger",method:"tool-call-join",upstreams:[...r.attribution.upstreams],rowsRewritten:s,originalCurrentUsage:e.currentUsage,hadExtensionState:e.extensionState!==void 0,reportedTotal:r.reportedTotal,trueTotal:r.trueTotal,at:new Date().toISOString()};e.currentUsage={...t,costAmount:r.split.current,cumulativeCost:r.split.retired>0?r.split.retired:void 0};const a=e.extensionState??{};a[f]={...a[f]??{},[g]:u},e.extensionState=a,b(o,`${JSON.stringify(e,null,2)}
|
|
3
|
+
`)}function G(n,r){const s=Date.parse(r);if(Number.isNaN(s))return 0;let i=0;for(const o of k(n))for(const e of o.lines){if(e.length===0||!e.includes('"usage_update"')||e.includes(g))continue;let t;try{t=JSON.parse(e)}catch{continue}typeof t.params?.update?.cost!="object"||t.params.update.cost===null||typeof t.recordedAt=="number"&&t.recordedAt>s&&(i+=1)}return i}function D(n,r={}){const s=J(n);if(s===void 0)return;const o=s.extensionState?.[f]?.[g];if(!o)return;if(r.force!==!0&&typeof o.at=="string"){const c=G(n,o.at);if(c>0)return{filesRewritten:[],rowsReverted:0,postRepairRows:c,refused:`${c} usage row(s) recorded after the repair have no pristine value to restore; reverting would strand the transcript on two baselines and roll meta.json back to its repair-time snapshot, discarding spend since then`}}const e=[];let t=0;for(const c of k(n)){const l=[...c.lines];let m=!1;for(let w=0;w<l.length;w+=1){const p=l[w];if(p.length===0||!p.includes(g))continue;let y;try{y=T(p)}catch{continue}y!==void 0&&(l[w]=y,m=!0,t+=1)}m&&(b(R(n,c.name),N({...c,lines:l})),e.push(c.name))}const u=R(n,"meta.json"),a=JSON.parse(h(u,"utf8"));o.originalCurrentUsage===void 0?delete a.currentUsage:a.currentUsage=o.originalCurrentUsage;const d=a.extensionState??{};return d[f]&&(delete d[f][g],Object.keys(d[f]).length===0&&delete d[f]),Object.keys(d).length===0&&!o.hadExtensionState?delete a.extensionState:a.extensionState=d,b(u,`${JSON.stringify(a,null,2)}
|
|
4
|
+
`),S.debug(`reverted ${n}: ${t} rows`),{filesRewritten:e,rowsReverted:t}}function K(n,r,s,i={}){const o=R(n,"meta.json"),e=JSON.parse(h(o,"utf8")),u=e.extensionState?.[f]?.[g];if(u===void 0)return{kind:"skip",why:"no-gate"};if(r.perUpstream.length<2)return{kind:"skip",why:"single-upstream"};if(!r.perUpstream.some(y=>y.isCurrent))return{kind:"refuse",why:"current upstream absent from the ledger"};const a=e.currentUsage??{},d=typeof a.cumulativeCost=="number"?a.cumulativeCost:0,c=typeof a.costAmount=="number"?a.costAmount:0,l=r.retired,m=r.current;if(Math.abs(d-l)<1e-6&&Math.abs(c-m)<1e-6)return{kind:"skip",why:"already-correct"};const w=e.extensionState,p={...u};return p.preReshapeCurrentUsage===void 0&&(p.preReshapeCurrentUsage=e.currentUsage),p.reshapedAt=new Date().toISOString(),p.reshapeTool=O(s),e.currentUsage={...a,costAmount:m,cumulativeCost:l>0?l:void 0},w[f]={...w[f]??{},[g]:p},e.extensionState=w,i.dryRun!==!0&&(b(o,`${JSON.stringify(e,null,2)}
|
|
5
|
+
`),S.info(`reshaped ${String(e.sessionId)}: ${d}+${c} -> ${l}+${m}`)),{kind:"ok",beforeCum:d,beforeCur:c,cum:l,cur:m,totalDelta:l+m-(d+c)}}export{x as ALGO_VERSION,v as MARKER_VERSION,B as applyPlan,G as postRepairUsageRows,K as reshapeSession,T as revertLine,D as revertSession,M as rewriteLine};
|
package/package.json
CHANGED