tickmarkr 1.57.0 → 1.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,33 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { readFileSync } from "node:fs";
2
+ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { probeVersion } from "./claude-code.js";
6
6
  import { parseWorkerResult } from "./prompt.js";
7
- import { channelsFromConfig, MODEL_ID_RE, shq } from "./types.js";
8
- // KIMI-03: collectUsage is DELIBERATELY ABSENT documented won't-implement, consistent with the
9
- // metering-honesty invariant (adapters/types.ts). Sessions live under ~/.kimi-code/sessions/ with
10
- // kimi export (ZIP) + session_index.jsonl; no harness-readable per-attempt token counter was found
11
- // in the session store (research F-6, 2026-07-17). Consequence: kimi channels report honestly
12
- // `unmetered` same class as grok (grok.ts:11-39). Revisit if kimi ships usage in the
13
- // `-p --output-format stream-json` envelope or a counter in the session store.
7
+ import { channelsFromConfig, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
8
+ // KIMI-03 → v1.58 T5: the "no harness-readable counter" block (research F-6, 2026-07-17) is
9
+ // LIFTED for collectUsage — kimi 0.27.0 writes a wire journal per agent at
10
+ // ~/.kimi-code/sessions/<wd>/session_<uuid>/agents/<agent>/wire.jsonl, and ~/.kimi-code/
11
+ // workspaces.json maps each wd_* dir to its realpath'd workspace root. Field mapping pinned ONLY
12
+ // after live verification of the real store (2026-07-18, 53 wire files / 178 usage rows, zero
13
+ // tokens spent):
14
+ // - the canonical row is top-level `{"type":"usage.record","usage":{...},"usageScope":"turn",
15
+ // "time":<epoch ms>}` — 178/178 rows carried scope "turn" and the identical key set
16
+ // {inputOther, output, inputCacheRead, inputCacheCreation};
17
+ // - per-turn DELTA semantics verified arithmetically: each turn's inputCacheRead ≈ prior
18
+ // inputCacheRead + inputOther + output (the grown context re-read from cache — claude's
19
+ // per-message convention), and inputOther/output are non-monotonic per-turn counts, so
20
+ // SUMMING rows is the correct fold (never a cumulative 3A+2B+C trap);
21
+ // - the SAME usage object is echoed inside the step.end loop event
22
+ // (`type:"context.append_loop_event"`, event.usage) — folding anything but usage.record
23
+ // rows double-counts every turn, hence the exact type+scope pin below;
24
+ // - mapping: inputOther→input, output→output, inputCacheRead→cacheRead,
25
+ // inputCacheCreation→cacheWrite (creation observed live only as 0; mapped by name).
26
+ // Absent or ambiguous usage resolves to UNMETERED, never an invented count: unknown usageScope,
27
+ // non-numeric core fields, torn lines, or no post-cursor row all skip/return undefined — the
28
+ // "?? 0" poisoning class stays banned. contextUsage remains absent (resumeUnknownContext below).
29
+ const MAX_SESSION_FILES = 20;
30
+ const MAX_SESSION_BYTES = 8_000_000;
14
31
  const CREDENTIALS_PATH = join(homedir(), ".kimi-code", "credentials", "kimi-code.json");
15
32
  // KIMI-01. Auth verdict from ~/.kimi-code/credentials/kimi-code.json ONLY — flat JSON, never a
16
33
  // network call. expires_at is epoch SECONDS (not ISO like grok). Non-empty refresh_token dominates
@@ -101,6 +118,102 @@ export const kimi = {
101
118
  return { command: this.headlessCommand(ctx.promptFile, a.model) };
102
119
  },
103
120
  parse: parseKimiResult,
121
+ collectUsage(cwd, sinceMs) {
122
+ try {
123
+ const real = realpathSync(cwd);
124
+ const home = join(homedir(), ".kimi-code");
125
+ const ws = JSON.parse(readFileSync(join(home, "workspaces.json"), "utf8"));
126
+ // a recreated workspace gets a new wd_* hash for the same root — fold every match
127
+ const wdDirs = Object.entries(ws.workspaces ?? {})
128
+ .filter(([, v]) => v && typeof v === "object" && v.root === real)
129
+ .map(([k]) => k);
130
+ // newest-first by mtime, bounded — mtime picks WHICH files to scan, never a record's cursor
131
+ const files = [];
132
+ for (const wd of wdDirs) {
133
+ const wdPath = join(home, "sessions", wd);
134
+ let sessions;
135
+ try {
136
+ sessions = readdirSync(wdPath);
137
+ }
138
+ catch {
139
+ continue;
140
+ }
141
+ for (const s of sessions) {
142
+ if (!s.startsWith("session_"))
143
+ continue;
144
+ const agentsPath = join(wdPath, s, "agents");
145
+ let agents;
146
+ try {
147
+ agents = readdirSync(agentsPath);
148
+ }
149
+ catch {
150
+ continue;
151
+ }
152
+ for (const a of agents) {
153
+ const p = join(agentsPath, a, "wire.jsonl");
154
+ try {
155
+ files.push({ path: p, m: statSync(p).mtimeMs });
156
+ }
157
+ catch {
158
+ continue;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ files.sort((a, b) => b.m - a.m);
164
+ let input = 0, output = 0, kept = false;
165
+ let cacheRead, cacheWrite;
166
+ for (const { path: fp } of files.slice(0, MAX_SESSION_FILES)) {
167
+ let text;
168
+ try {
169
+ text = readFileSync(fp, "utf8").slice(0, MAX_SESSION_BYTES);
170
+ }
171
+ catch {
172
+ continue;
173
+ }
174
+ for (const line of text.split("\n")) {
175
+ if (!line.trim())
176
+ continue;
177
+ let recRaw;
178
+ try {
179
+ recRaw = JSON.parse(line);
180
+ }
181
+ catch {
182
+ continue;
183
+ }
184
+ const rec = recRaw;
185
+ // exact type+scope pin: the step.end loop event ECHOES the same usage (double-count),
186
+ // and any scope other than the live-verified "turn" has unknown fold semantics — skip
187
+ if (rec.type !== "usage.record" || rec.usageScope !== "turn")
188
+ continue;
189
+ if (typeof rec.time !== "number" || !Number.isFinite(rec.time) || rec.time < sinceMs)
190
+ continue;
191
+ const u = rec.usage;
192
+ if (!u || typeof u !== "object")
193
+ continue;
194
+ const uu = u;
195
+ // ambiguous row (core fields missing/non-numeric) resolves to unmetered, never 0
196
+ if (typeof uu.inputOther !== "number" || typeof uu.output !== "number")
197
+ continue;
198
+ input += uu.inputOther;
199
+ output += uu.output;
200
+ if (typeof uu.inputCacheRead === "number")
201
+ cacheRead = (cacheRead ?? 0) + uu.inputCacheRead;
202
+ if (typeof uu.inputCacheCreation === "number")
203
+ cacheWrite = (cacheWrite ?? 0) + uu.inputCacheCreation;
204
+ kept = true;
205
+ }
206
+ }
207
+ if (!kept)
208
+ return undefined; // nothing matched ⇒ unmetered, never {input:0,…}
209
+ const out = { input, output, ...(cacheRead !== undefined ? { cacheRead } : {}), ...(cacheWrite !== undefined ? { cacheWrite } : {}) };
210
+ const p = TokenUsageSchema.safeParse(out);
211
+ return p.success ? p.data : undefined;
212
+ }
213
+ catch {
214
+ return undefined; // missing home/workspaces.json / any throw ⇒ fail open
215
+ }
216
+ },
104
217
  listModels: async () => {
105
218
  const r = spawnSync("kimi", ["provider", "list", "--json"], { encoding: "utf8", timeout: 15000 });
106
219
  return r.error || r.status !== 0 ? [] : parseKimiModels(r.stdout || "");
@@ -67,6 +67,43 @@ function ladderFor(task, entry) {
67
67
  const escalate = task.routingHints?.escalate ?? entry?.escalate ?? true;
68
68
  return escalate ? ["retry", "escalate", "consult", "human"] : ["retry", "consult", "human"];
69
69
  }
70
+ // v1.58 frontier spread (operator ruling .planning/rulings/2026-07-18-frontier-spread-credits.md):
71
+ // every sub channel marginal-cost-ranks 0, so tier-equal frontier ties fell through all sort keys
72
+ // to discovery order — the first sub channel (claude-code:fable) served every frontier auto pick,
73
+ // and same-day quota exhaustion on that one channel interrupted two live pipeline agents. A
74
+ // deterministic task-keyed rotation now spreads each residual frontier tie across its whole tie
75
+ // group, so sol/k3-class channels serve frontier work as first-class candidates. Strictly the LAST
76
+ // tiebreak: it permutes only maximal runs already tied on every key above it (prefer band,
77
+ // marginal cost, tier — plus exploration bonus and learned score on the learned path), and only
78
+ // runs of zero-marginal-cost frontier SUB channels outside any prefer entry. Pins return upstream,
79
+ // denies filter upstream, prefer order is operator-explicit — none are touched.
80
+ // ponytail: frontier runs only — the ruled scope (ruling §1); spreading mid/cheap ties too = drop
81
+ // the tier guard. Rotation, not utilization: profile-driven spread would be inert exactly where
82
+ // the concentration bites (cold profiles, plan-time static routing, learned:off).
83
+ const spreadOffset = (task) => {
84
+ let h = 0;
85
+ for (const ch of `${task.id}\n${task.goal}`)
86
+ h = (h * 31 + ch.charCodeAt(0)) >>> 0;
87
+ return h;
88
+ };
89
+ function spreadFrontierTies(sorted, task, prefer, tied) {
90
+ const out = [...sorted];
91
+ const preferLen = prefer?.length ?? 0;
92
+ let i = 0;
93
+ while (i < out.length) {
94
+ let j = i + 1;
95
+ while (j < out.length && tied(out[i], out[j]))
96
+ j++;
97
+ const len = j - i;
98
+ if (len > 1 && out[i].tier === "frontier" && marginalCostRank(out[i]) === 0 && preferIndex(out[i], prefer) === preferLen) {
99
+ const run = out.slice(i, j);
100
+ const k = spreadOffset(task) % len;
101
+ out.splice(i, len, ...run.slice(k), ...run.slice(0, k));
102
+ }
103
+ i = j;
104
+ }
105
+ return out;
106
+ }
70
107
  function withoutExcluded(channels, exclude) {
71
108
  if (!exclude?.size)
72
109
  return channels;
@@ -174,8 +211,9 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
174
211
  for (const p of prefer ?? [])
175
212
  preflightPrefer(p);
176
213
  // key order is a contract: prefer > marginal cost > tier (cheapest sufficient) > learned score (v1.6 ROUTE-06)
177
- // > discovery order (same-tier fairness, D2). The learned score is the LAST key — it decides only the
178
- // discovery-order tail, never a pin/floor/prefer/cost/tier boundary (they all sort above it, ROUTE-08).
214
+ // > frontier spread (v1.58) > discovery order (same-tier fairness, D2). The learned score decides only the
215
+ // discovery-order tail, never a pin/floor/prefer/cost/tier boundary (they all sort above it, ROUTE-08);
216
+ // the spread rotates only what would otherwise fall to discovery order.
179
217
  const staticCmp = (a, b) => preferIndex(a, prefer) - preferIndex(b, prefer) || marginalCostRank(a) - marginalCostRank(b) || TIER_RANK[a.tier] - TIER_RANK[b.tier];
180
218
  const eligibleRaw = channels.filter((c) => TIER_RANK[c.tier] >= TIER_RANK[minTier]);
181
219
  if (!eligibleRaw.length) {
@@ -192,6 +230,11 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
192
230
  let eligible;
193
231
  let deviation;
194
232
  let learnedChosen = "";
233
+ let spreadDecided = false;
234
+ // v1.58: the spread is applied identically on both sort paths (and to the deviation baseline), so
235
+ // an empty/cold profile stays byte-identical to the 3-arg call (ROUTE-07) and a deviation always
236
+ // means learned evidence moved the pick — never the spread mislabeled as learning.
237
+ const spreadStatic = (sorted) => spreadFrontierTies(sorted, task, prefer, (a, b) => staticCmp(a, b) === 0);
195
238
  if (profile) {
196
239
  // scores precomputed ONCE over the eligible set — never inside the comparator (Pitfall 1).
197
240
  // ponytail: no epsilon — two finite scores subtract to a finite number (Phase 12 totality); an
@@ -210,7 +253,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
210
253
  const off = exploreOff(task, cfg, exploreCtx);
211
254
  const bonuses = new Map(eligibleRaw.map((c) => [channelKey(c), off ? 0 : explorationBonus(cellOf(profile, task.shape, channelKey(c), c.channel), cap)]));
212
255
  const bonusOf = (c) => bonuses.get(channelKey(c));
213
- const staticWinner = [...eligibleRaw].sort(staticCmp)[0];
256
+ const staticWinner = spreadStatic([...eligibleRaw].sort(staticCmp))[0];
214
257
  // Phase 34 ROUTE-17: prefer becomes a BAND + group-rep keys so exploration fires ACROSS prefer
215
258
  // entries, while intra-entry order (cost > tier > bonus > score) and every cold path stay byte-identical.
216
259
  // Cold reduces to preferIndex || cost || tier ≡ staticCmp (proof P1); rep keys use the group HEAD so
@@ -232,7 +275,11 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
232
275
  }));
233
276
  const kOf = (c) => keyOf.get(channelKey(c));
234
277
  const firstDiff = (a, b) => kOf(a).findIndex((v, i) => v !== kOf(b)[i]);
235
- eligible = [...eligibleRaw].sort((a, b) => { const i = firstDiff(a, b); return i === -1 ? 0 : kOf(a)[i] - kOf(b)[i]; });
278
+ const sortedByKey = [...eligibleRaw].sort((a, b) => { const i = firstDiff(a, b); return i === -1 ? 0 : kOf(a)[i] - kOf(b)[i]; });
279
+ // spread runs tied on the FULL learned key (bonus/score included) — a warm score or live probe
280
+ // bonus still decides its tie exactly as before; the spread rotates only the residual all-equal runs.
281
+ eligible = spreadFrontierTies(sortedByKey, task, prefer, (a, b) => firstDiff(a, b) === -1);
282
+ spreadDecided = channelKey(eligible[0]) !== channelKey(sortedByKey[0]);
236
283
  const w = eligible[0];
237
284
  const ru = channelKey(staticWinner) !== channelKey(w) ? staticWinner : eligible[1];
238
285
  // Phase 34 ROUTE-17: first-differing-key markers — probe when a rep or intra bonus key decided.
@@ -250,7 +297,9 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
250
297
  }
251
298
  }
252
299
  else {
253
- eligible = eligibleRaw.sort(staticCmp); // ROUTE-07/09: literally the v1.5 code path dead code cannot deviate
300
+ const sortedStatic = eligibleRaw.sort(staticCmp); // ROUTE-07/09: the v1.5 key order, spread applied as the last key
301
+ eligible = spreadStatic(sortedStatic);
302
+ spreadDecided = channelKey(eligible[0]) !== channelKey(sortedStatic[0]);
254
303
  }
255
304
  const bound = qualityBound(quality, advisoryFloor, taskFloorRaw) ??
256
305
  (taskFloor && TIER_RANK[taskFloor] >= TIER_RANK[baseTier] ? `floor ${taskFloor} (task hint${src})` :
@@ -261,8 +310,10 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
261
310
  const preferVia = preferFromAuto(task.shape, preferCtx)
262
311
  ? `via prefer (auto-modernized ${preferCtx.autoPrefer.derivedAt.slice(0, 10)})`
263
312
  : "via prefer";
264
- const chosenBy = learnedChosen || (prefer && preferIndex(eligible[0], prefer) < prefer.length
265
- ? preferVia : "cheapest sufficient tier");
313
+ // a spread-decided winner is never inside a prefer band (the spread skips those runs), so the
314
+ // three arms below are mutually exclusive by construction
315
+ const chosenBy = learnedChosen || (spreadDecided ? "via frontier spread"
316
+ : prefer && preferIndex(eligible[0], prefer) < prefer.length ? preferVia : "cheapest sufficient tier");
266
317
  maybeSlaLint(lints, task, profile, slaMinutes, eligible[0]);
267
318
  return { assignment: toAssignment(eligible[0]), ladder: ladderFor(task, entry), lints, provenance: `${degraded}${bound}, marginal-cost auto (${chosenBy})`, ...(deviation ? { deviation } : {}) };
268
319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.57.0",
3
+ "version": "1.58.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",