offhands 0.1.18 → 0.1.19

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 (2) hide show
  1. package/dist/daemon.mjs +95 -11
  2. package/package.json +1 -1
package/dist/daemon.mjs CHANGED
@@ -44206,6 +44206,7 @@ import { createServer as createServer3 } from "node:net";
44206
44206
  import { tmpdir as tmpdir4 } from "node:os";
44207
44207
  import { join as join13, resolve as resolve5 } from "node:path";
44208
44208
  var MIN_PAIRS_FOR_CLAIM = 10;
44209
+ var USAGE_GRACE_MS = 2e3;
44209
44210
  var median2 = (xs) => {
44210
44211
  if (xs.length === 0) return null;
44211
44212
  const s2 = [...xs].sort((a2, b2) => a2 - b2);
@@ -44225,6 +44226,33 @@ function signTestP(a2, b2) {
44225
44226
  }
44226
44227
  return Math.min(1, 2 * cumulative / 2 ** n3);
44227
44228
  }
44229
+ function signedRankP(diffs) {
44230
+ const d2 = diffs.filter((x2) => x2 !== 0);
44231
+ const n3 = d2.length;
44232
+ if (n3 === 0) return null;
44233
+ const order = d2.map((x2, i2) => ({ a: Math.abs(x2), i: i2 })).sort((p2, q2) => p2.a - q2.a);
44234
+ const rank2 = new Array(n3);
44235
+ for (let lo = 0; lo < n3; ) {
44236
+ let hi = lo;
44237
+ while (hi + 1 < n3 && order[hi + 1].a === order[lo].a) hi++;
44238
+ const avgRank2 = lo + 1 + (hi + 1);
44239
+ for (let k2 = lo; k2 <= hi; k2++) rank2[order[k2].i] = avgRank2;
44240
+ lo = hi + 1;
44241
+ }
44242
+ const total2 = rank2.reduce((s2, r2) => s2 + r2, 0);
44243
+ const observed2 = d2.reduce((s2, x2, i2) => s2 + (x2 > 0 ? rank2[i2] : 0), 0);
44244
+ let counts = new Array(total2 + 1).fill(0);
44245
+ counts[0] = 1;
44246
+ for (const r2 of rank2) {
44247
+ const next = counts.slice();
44248
+ for (let s2 = 0; s2 + r2 <= total2; s2++) if (counts[s2]) next[s2 + r2] += counts[s2];
44249
+ counts = next;
44250
+ }
44251
+ const dev = Math.abs(2 * observed2 - total2);
44252
+ let extreme = 0;
44253
+ for (let s2 = 0; s2 <= total2; s2++) if (Math.abs(2 * s2 - total2) >= dev) extreme += counts[s2];
44254
+ return Math.min(1, extreme / 2 ** n3);
44255
+ }
44228
44256
  function recallOf(answer, expect) {
44229
44257
  if (!expect || expect.length === 0) return null;
44230
44258
  const hay = answer.toLowerCase().replace(/\\/g, "/");
@@ -44241,10 +44269,12 @@ function summarize(name, pairs, pick) {
44241
44269
  medianContext: median2(both.map((x2) => x2.c)),
44242
44270
  medianPlain: median2(both.map((x2) => x2.q)),
44243
44271
  medianDiff: median2(both.map((x2) => x2.c - x2.q)),
44272
+ meanDiff: mean(both.map((x2) => x2.c - x2.q)),
44244
44273
  contextLower,
44245
44274
  plainLower,
44246
44275
  ties: both.length - contextLower - plainLower,
44247
- p: both.length ? signTestP(contextLower, plainLower) : null
44276
+ p: both.length ? signTestP(contextLower, plainLower) : null,
44277
+ pSignedRank: signedRankP(both.map((x2) => x2.c - x2.q))
44248
44278
  };
44249
44279
  }
44250
44280
  function analyzePairs(pairs, prompts) {
@@ -44286,11 +44316,12 @@ function analyzePairs(pairs, prompts) {
44286
44316
  const enough = usable.length >= MIN_PAIRS_FOR_CLAIM;
44287
44317
  for (const m3 of metrics) {
44288
44318
  if (m3.n === 0) continue;
44289
- const dir = m3.contextLower > m3.plainLower ? "lower" : "higher";
44290
- if (enough && m3.n >= MIN_PAIRS_FOR_CLAIM && m3.p !== null && m3.p < 0.05 && m3.contextLower !== m3.plainLower) {
44291
- conclusions.push(`${m3.name}: ${dir} with context in ${Math.max(m3.contextLower, m3.plainLower)} of ${m3.n} pairs (p=${m3.p.toFixed(3)}) \u2014 a measurable difference in this sample.`);
44319
+ const dir = (m3.meanDiff ?? 0) < 0 ? "lower" : "higher";
44320
+ const ps = `signed-rank p=${m3.pSignedRank === null ? "\u2014" : m3.pSignedRank.toFixed(3)}, sign-test p=${m3.p === null ? "\u2014" : m3.p.toFixed(3)}`;
44321
+ if (enough && m3.n >= MIN_PAIRS_FOR_CLAIM && m3.pSignedRank !== null && m3.pSignedRank < 0.05) {
44322
+ conclusions.push(`${m3.name}: ${dir} with context (mean difference ${(m3.meanDiff ?? 0).toFixed(2)}; ${ps}; ${m3.contextLower} lower / ${m3.plainLower} higher / ${m3.ties} tied) \u2014 a measurable difference in this sample.`);
44292
44323
  } else {
44293
- conclusions.push(`${m3.name}: no reliable difference detected (${m3.contextLower} lower with context, ${m3.plainLower} lower without, ${m3.ties} tied; p=${m3.p === null ? "\u2014" : m3.p.toFixed(3)}).`);
44324
+ conclusions.push(`${m3.name}: no reliable difference detected (${m3.contextLower} lower with context, ${m3.plainLower} lower without, ${m3.ties} tied; ${ps}).`);
44294
44325
  }
44295
44326
  }
44296
44327
  if (usable.length === 0 && errorMessages.length > 0) {
@@ -44332,17 +44363,17 @@ function formatAbReport(r2, meta) {
44332
44363
  for (const m3 of r2.errorMessages.slice(0, 3)) L2.push(` ${m3.runs} run(s): ${m3.message}`);
44333
44364
  }
44334
44365
  L2.push("");
44335
- L2.push("Metric with ctx plain median diff ctx lower / plain lower / tie sign-test p");
44366
+ L2.push("Metric with ctx plain median diff mean diff ctx lower / plain lower / tie signed-rank p sign-test p");
44336
44367
  for (const m3 of r2.metrics) {
44337
44368
  const digits = m3.name.startsWith("Cost") ? 4 : m3.name.startsWith("Duration") ? 0 : 1;
44338
44369
  L2.push(
44339
- `${m3.name.padEnd(19)} ${fmt(m3.medianContext, digits).padEnd(10)} ${fmt(m3.medianPlain, digits).padEnd(9)} ${fmt(m3.medianDiff, digits).padEnd(13)} ${`${m3.contextLower} / ${m3.plainLower} / ${m3.ties}`.padEnd(30)} ${m3.p === null ? "\u2014" : m3.p.toFixed(3)} (n=${m3.n})`
44370
+ `${m3.name.padEnd(19)} ${fmt(m3.medianContext, digits).padEnd(10)} ${fmt(m3.medianPlain, digits).padEnd(9)} ${fmt(m3.medianDiff, digits).padEnd(12)} ${fmt(m3.meanDiff, digits).padEnd(11)} ${`${m3.contextLower} / ${m3.plainLower} / ${m3.ties}`.padEnd(30)} ${(m3.pSignedRank === null ? "\u2014" : m3.pSignedRank.toFixed(3)).padEnd(15)} ${m3.p === null ? "\u2014" : m3.p.toFixed(3)} (n=${m3.n})`
44340
44371
  );
44341
44372
  }
44342
44373
  const q2 = r2.quality;
44343
44374
  if (q2.pairsWithGroundTruth > 0) {
44344
44375
  L2.push(
44345
- `${"Answer recall".padEnd(19)} ${fmt(q2.meanRecallContext, 2).padEnd(10)} ${fmt(q2.meanRecallPlain, 2).padEnd(9)} ${"".padEnd(13)} ${`${q2.contextHigher} / ${q2.plainHigher} / ${q2.ties}`.padEnd(30)} ${q2.p === null ? "\u2014" : q2.p.toFixed(3)} (n=${q2.pairsWithGroundTruth}; higher is better)`
44376
+ `${"Answer recall".padEnd(19)} ${fmt(q2.meanRecallContext, 2).padEnd(10)} ${fmt(q2.meanRecallPlain, 2).padEnd(9)} ${"".padEnd(12)} ${"".padEnd(11)} ${`${q2.contextHigher} / ${q2.plainHigher} / ${q2.ties}`.padEnd(30)} ${"".padEnd(15)} ${q2.p === null ? "\u2014" : q2.p.toFixed(3)} (n=${q2.pairsWithGroundTruth}; higher is better)`
44346
44377
  );
44347
44378
  } else {
44348
44379
  L2.push("Answer recall not measured (no prompt listed expected files)");
@@ -44351,7 +44382,7 @@ function formatAbReport(r2, meta) {
44351
44382
  for (const c2 of r2.conclusions) L2.push(`\u2022 ${c2}`);
44352
44383
  L2.push("");
44353
44384
  L2.push(
44354
- "One model, one repository, this prompt set. A sign test on paired runs; no savings figure is computed or implied, and none should be quoted from this."
44385
+ "One model, one repository, this prompt set. Exact sign test and signed-rank test on paired runs; no savings figure is computed or implied, and none should be quoted from this."
44355
44386
  );
44356
44387
  return L2.join("\n");
44357
44388
  }
@@ -44462,6 +44493,7 @@ async function createAbHarness(cfg) {
44462
44493
  let detach = () => {
44463
44494
  };
44464
44495
  let timer;
44496
+ let doneAt;
44465
44497
  const finish = () => {
44466
44498
  if (settled) return;
44467
44499
  settled = true;
@@ -44473,7 +44505,8 @@ async function createAbHarness(cfg) {
44473
44505
  timedOut,
44474
44506
  ...error ? { error } : {},
44475
44507
  toolCalls,
44476
- durationMs: Date.now() - t0,
44508
+ // Measured to the agent's own `done`, not to when we stopped listening.
44509
+ durationMs: (doneAt ?? Date.now()) - t0,
44477
44510
  costUsd,
44478
44511
  contextTokens,
44479
44512
  contextAttached,
@@ -44483,6 +44516,10 @@ async function createAbHarness(cfg) {
44483
44516
  };
44484
44517
  const sink = (m3) => {
44485
44518
  if (m3.sessionId !== session.id) return;
44519
+ if (m3.type === "receipt" && doneAt !== void 0) {
44520
+ finish();
44521
+ return;
44522
+ }
44486
44523
  if (m3.type === "intelligence-review") {
44487
44524
  contextAttached = true;
44488
44525
  filesSelected = m3.filesSelected;
@@ -44502,7 +44539,10 @@ async function createAbHarness(cfg) {
44502
44539
  } else if (e.type === "error") {
44503
44540
  error = String(e.message ?? "error");
44504
44541
  finish();
44505
- } else if (e.type === "done") finish();
44542
+ } else if (e.type === "done") {
44543
+ doneAt = Date.now();
44544
+ setTimeout(finish, USAGE_GRACE_MS);
44545
+ }
44506
44546
  };
44507
44547
  detach = manager2.attach(sink);
44508
44548
  timer = setTimeout(() => {
@@ -44525,7 +44565,51 @@ function flag(args2, name) {
44525
44565
  const i2 = args2.indexOf(name);
44526
44566
  return i2 >= 0 ? args2[i2 + 1] : void 0;
44527
44567
  }
44568
+ function intelAbMerge(args2, mergeAt, out) {
44569
+ const files = args2.slice(mergeAt + 1).filter((a2) => !a2.startsWith("--"));
44570
+ if (files.length === 0) {
44571
+ out("usage: offhands intel-ab --merge <result.json> [<result.json> ...] [--json]");
44572
+ return 1;
44573
+ }
44574
+ const prompts = [];
44575
+ const pairs = [];
44576
+ const workspaces = [];
44577
+ let runner = "";
44578
+ let model;
44579
+ for (const f2 of files) {
44580
+ let saved;
44581
+ try {
44582
+ saved = JSON.parse(readFileSync6(resolve5(f2), "utf8"));
44583
+ } catch (e) {
44584
+ out(`could not read ${f2}: ${e instanceof Error ? e.message : String(e)}`);
44585
+ return 1;
44586
+ }
44587
+ if (!Array.isArray(saved?.prompts) || !Array.isArray(saved?.pairs)) {
44588
+ out(`${f2} is not an intel-ab --out result (needs "prompts" and "pairs")`);
44589
+ return 1;
44590
+ }
44591
+ const offset = prompts.length;
44592
+ prompts.push(...saved.prompts);
44593
+ for (const p2 of saved.pairs) pairs.push({ ...p2, promptIndex: p2.promptIndex + offset });
44594
+ workspaces.push(String(saved.workspace ?? f2));
44595
+ runner = runner || String(saved.runnerId ?? "");
44596
+ model = model ?? saved.model;
44597
+ }
44598
+ const report = analyzePairs(pairs, prompts);
44599
+ out(
44600
+ args2.includes("--json") ? JSON.stringify(report, null, 2) : formatAbReport(report, {
44601
+ workspace: workspaces.join(" + "),
44602
+ runner: runner || "unknown",
44603
+ ...model ? { model } : {},
44604
+ prompts: prompts.length,
44605
+ repeats: Math.max(1, Math.round(pairs.length / Math.max(prompts.length, 1)))
44606
+ })
44607
+ );
44608
+ return 0;
44609
+ }
44528
44610
  async function intelAbCli(args2, out = console.log, deps = {}) {
44611
+ const mergeAt = args2.indexOf("--merge");
44612
+ if (mergeAt >= 0) return intelAbMerge(args2, mergeAt, out);
44529
44613
  const workspaceArg = flag(args2, "--workspace");
44530
44614
  const promptsArg = flag(args2, "--prompts");
44531
44615
  const runnerId = flag(args2, "--runner") ?? "claude-code";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offhands",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Phone → coding-agent relay. Daemon runs on your laptop, PWA on your phone. E2E encrypted.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",