tines 0.0.82 → 0.0.84

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/index.js +246 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync3 } from "node:fs";
4
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
5
5
  import { hostname as hostname2 } from "node:os";
6
6
  import { basename, dirname as dirname4, join as join4 } from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
@@ -40,7 +40,15 @@ function readBodyValue(value, stdin = processStdin) {
40
40
 
41
41
  // src/daemon/daemon.ts
42
42
  import { spawn as spawn2 } from "node:child_process";
43
- import { mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
43
+ import {
44
+ createWriteStream,
45
+ mkdirSync as mkdirSync3,
46
+ readFileSync as readFileSync4,
47
+ rmSync,
48
+ statSync,
49
+ unlinkSync,
50
+ writeFileSync as writeFileSync2
51
+ } from "node:fs";
44
52
  import { hostname, platform, arch } from "node:os";
45
53
  import { dirname as dirname3, join as join3 } from "node:path";
46
54
 
@@ -108,6 +116,8 @@ var RUNNER_OFFLINE_FAIL_MS = 5 * 60 * 1e3;
108
116
  var LAUNCH_STALL_MS = 5 * 60 * 1e3;
109
117
  var RUN_KEY_SLACK_MS = 10 * 60 * 1e3;
110
118
  var RUN_LOG_MAX_BYTES = 256 * 1024;
119
+ var RUN_LOG_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
120
+ var RUN_LOG_RAW_MAX_BYTES = 64 * 1024 * 1024;
111
121
  var MODEL_PREDECESSORS = {
112
122
  "claude-fable-5": ["claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"],
113
123
  "claude-opus-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-opus-4-5", "claude-opus-4-1"],
@@ -497,6 +507,19 @@ function createApiClient(options) {
497
507
  // Agent runs
498
508
  listRuns: (filters = {}) => get(`/api/v1/runs${query(filters)}`),
499
509
  getRun: (id) => get(`/api/v1/runs/${id}`),
510
+ /**
511
+ * The run's complete log (not the 256 KB tail `getRun` returns) as a
512
+ * streamed Response, so a multi-megabyte log never has to be held in
513
+ * memory. `raw` asks for the unrendered harness stream instead.
514
+ */
515
+ getRunLogFull: (id, opts = {}) => raw("GET", `/api/v1/runs/${id}/log${opts.raw ? "?raw=1" : ""}`, {
516
+ headers: { accept: "text/plain" }
517
+ }),
518
+ /** Daemon-only: uploads the raw harness stream for a settled run. */
519
+ putRunLogRaw: (id, body) => raw("PUT", `/api/v1/runs/${id}/log/raw`, {
520
+ body,
521
+ headers: { "content-type": "application/x-ndjson", "content-length": String(body.byteLength) }
522
+ }),
500
523
  cancelRun: (id) => request("POST", `/api/v1/runs/${id}/cancel`),
501
524
  // Routing rules (one per exact scope; responses carry shadow hints)
502
525
  listRoutingRules: () => get("/api/v1/routing-rules"),
@@ -550,6 +573,101 @@ var PLACEHOLDER_DESCRIPTIONS = {
550
573
  };
551
574
  var TEMPLATE_PLACEHOLDERS = Object.keys(PLACEHOLDER_DESCRIPTIONS).map((key) => ({ key, description: PLACEHOLDER_DESCRIPTIONS[key] }));
552
575
 
576
+ // src/daemon/claude-stream.ts
577
+ function clip(value, max) {
578
+ return value.length > max ? `${value.slice(0, max)}\u2026` : value;
579
+ }
580
+ function resultText(content) {
581
+ if (typeof content === "string") return content.trim();
582
+ if (Array.isArray(content)) {
583
+ return content.map((block) => typeof block?.text === "string" ? block.text : "").join("").trim();
584
+ }
585
+ return "";
586
+ }
587
+ function renderStreamEvent(event) {
588
+ switch (event.type) {
589
+ case "system":
590
+ if (event.subtype === "init") {
591
+ return [`[session] started${event.model ? ` (model ${event.model})` : ""}`];
592
+ }
593
+ return [];
594
+ case "assistant": {
595
+ const lines = [];
596
+ for (const block of event.message?.content ?? []) {
597
+ if (block.type === "text" && block.text?.trim()) {
598
+ lines.push(`[agent] ${clip(block.text.trim(), 2e3)}`);
599
+ } else if (block.type === "tool_use") {
600
+ const args = block.input === void 0 ? "" : JSON.stringify(block.input);
601
+ lines.push(`[tool] ${block.name ?? "tool"} ${clip(args, 300)}`.trimEnd());
602
+ }
603
+ }
604
+ return lines;
605
+ }
606
+ case "user": {
607
+ const lines = [];
608
+ for (const block of event.message?.content ?? []) {
609
+ if (block.type === "tool_result" && block.is_error) {
610
+ lines.push(`[tool] error: ${clip(resultText(block.content), 300)}`);
611
+ }
612
+ }
613
+ return lines;
614
+ }
615
+ case "result": {
616
+ const parts = [];
617
+ if (typeof event.num_turns === "number") parts.push(`${event.num_turns} turns`);
618
+ if (typeof event.total_cost_usd === "number") parts.push(`$${event.total_cost_usd.toFixed(2)}`);
619
+ const detail = parts.length > 0 ? ` (${parts.join(", ")})` : "";
620
+ const lines = [`[session] result: ${event.subtype ?? "done"}${detail}`];
621
+ if (event.is_error && event.result) lines.push(`[error] ${clip(event.result, 2e3)}`);
622
+ return lines;
623
+ }
624
+ default:
625
+ return [];
626
+ }
627
+ }
628
+ var ClaudeStreamRenderer = class {
629
+ constructor(emit) {
630
+ this.emit = emit;
631
+ }
632
+ emit;
633
+ pending = "";
634
+ write(chunk) {
635
+ this.pending += chunk;
636
+ let nl = this.pending.indexOf("\n");
637
+ while (nl !== -1) {
638
+ this.line(this.pending.slice(0, nl));
639
+ this.pending = this.pending.slice(nl + 1);
640
+ nl = this.pending.indexOf("\n");
641
+ }
642
+ }
643
+ /** Renders any trailing partial line. Call once the harness has exited. */
644
+ finish() {
645
+ if (this.pending) {
646
+ this.line(this.pending);
647
+ this.pending = "";
648
+ }
649
+ }
650
+ line(raw) {
651
+ const trimmed = raw.trim();
652
+ if (!trimmed) return;
653
+ if (!trimmed.startsWith("{")) {
654
+ this.emit(`${raw}
655
+ `);
656
+ return;
657
+ }
658
+ let event;
659
+ try {
660
+ event = JSON.parse(trimmed);
661
+ } catch {
662
+ this.emit(`${raw}
663
+ `);
664
+ return;
665
+ }
666
+ for (const line of renderStreamEvent(event)) this.emit(`${line}
667
+ `);
668
+ }
669
+ };
670
+
553
671
  // src/daemon/cli-refresh.ts
554
672
  import { spawn } from "node:child_process";
555
673
  import { existsSync, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
@@ -571,7 +689,7 @@ function buildHarnessInvocation(spec, input) {
571
689
  file: "sh",
572
690
  args: [
573
691
  "-c",
574
- `claude -p${input.model ? ` --model ${shellQuote(input.model)}` : ""} < ${shellQuote(input.promptFile)}`
692
+ `claude -p --output-format stream-json --verbose${input.model ? ` --model ${shellQuote(input.model)}` : ""} < ${shellQuote(input.promptFile)}`
575
693
  ]
576
694
  };
577
695
  case "codex":
@@ -626,6 +744,7 @@ var RunTable = class {
626
744
  async finishAndCleanup(run, status, error) {
627
745
  if (run.settled) return this.cleanup(run);
628
746
  run.settled = true;
747
+ run.drain?.();
629
748
  await run.flush?.();
630
749
  try {
631
750
  await this.effects.finish(run, status, error);
@@ -661,10 +780,13 @@ var LogBatcher = class {
661
780
  buffer = "";
662
781
  timer = null;
663
782
  sending = Promise.resolve();
783
+ seq = 0;
784
+ /** Chunks awaiting a successful send, oldest first. */
785
+ pending = [];
664
786
  append(text2) {
665
787
  if (!text2) return;
666
788
  this.buffer += text2;
667
- if (Buffer.byteLength(this.buffer, "utf8") >= (this.opts.maxBytes ?? 8 * 1024)) {
789
+ if (Buffer.byteLength(this.buffer, "utf8") >= (this.opts.maxBytes ?? 32 * 1024)) {
668
790
  void this.flush();
669
791
  } else if (!this.timer) {
670
792
  this.timer = setTimeout(() => void this.flush(), this.opts.intervalMs ?? 2e3);
@@ -677,13 +799,53 @@ var LogBatcher = class {
677
799
  clearTimeout(this.timer);
678
800
  this.timer = null;
679
801
  }
680
- const chunk = this.buffer;
681
- this.buffer = "";
682
- if (chunk) {
683
- this.sending = this.sending.then(() => this.send(chunk)).catch((err) => this.opts.onError?.(err));
802
+ if (this.buffer) {
803
+ this.pending.push({ chunk: this.buffer, seq: ++this.seq });
804
+ this.buffer = "";
805
+ this.trimPending();
684
806
  }
807
+ if (this.pending.length === 0) return this.sending;
808
+ this.sending = this.sending.then(() => this.drain());
685
809
  return this.sending;
686
810
  }
811
+ /**
812
+ * Sends queued chunks in order, stopping at the first failure so the
813
+ * survivors keep their place — and their seq, which is what lets the
814
+ * server recognise a resend of a chunk it already applied.
815
+ */
816
+ async drain() {
817
+ while (this.pending.length > 0) {
818
+ const next = this.pending[0];
819
+ try {
820
+ await this.send(next.chunk, next.seq);
821
+ } catch (err) {
822
+ this.opts.onError?.(err);
823
+ return;
824
+ }
825
+ this.pending.shift();
826
+ }
827
+ }
828
+ /**
829
+ * Bounds the retry backlog: a daemon that cannot reach the supervisor for
830
+ * a long time must not grow its heap without limit. The oldest chunks go
831
+ * first, with a marker so the gap is visible in the log rather than
832
+ * silent.
833
+ */
834
+ trimPending() {
835
+ const cap = this.opts.maxPendingBytes ?? 1024 * 1024;
836
+ let held = this.pending.reduce((n, p) => n + Buffer.byteLength(p.chunk, "utf8"), 0);
837
+ if (held <= cap) return;
838
+ let lost = 0;
839
+ while (this.pending.length > 1 && held > cap) {
840
+ const dropped = this.pending.shift();
841
+ const bytes = Buffer.byteLength(dropped.chunk, "utf8");
842
+ held -= bytes;
843
+ lost += bytes;
844
+ }
845
+ const head = this.pending[0];
846
+ head.chunk = `[log] ${lost} bytes lost (supervisor unreachable)
847
+ ${head.chunk}`;
848
+ }
687
849
  };
688
850
  var AMBIENT_CLI = { binDir: null, version: null, source: "ambient" };
689
851
  var CliRefresher = class {
@@ -884,6 +1046,32 @@ function processStartTimeMs(pid) {
884
1046
 
885
1047
  // src/daemon/daemon.ts
886
1048
  var CLI_REFRESH_TTL_MS = 10 * 6e4;
1049
+ async function uploadRawLog(run) {
1050
+ const path2 = run.rawSpoolPath;
1051
+ if (!path2 || !run.rawUpload) return;
1052
+ run.rawSpoolPath = void 0;
1053
+ try {
1054
+ await new Promise((resolve) => run.rawSpool?.end(resolve) ?? resolve());
1055
+ const size = statSync(path2).size;
1056
+ if (size > 0) {
1057
+ let body = readFileSync4(path2);
1058
+ if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
1059
+ const marker = Buffer.from(
1060
+ `{"type":"tines_truncated","dropped_bytes":${body.byteLength - RUN_LOG_RAW_MAX_BYTES}}
1061
+ `
1062
+ );
1063
+ body = Buffer.concat([marker, body.subarray(body.byteLength - RUN_LOG_RAW_MAX_BYTES + marker.byteLength)]);
1064
+ }
1065
+ await run.rawUpload(body);
1066
+ }
1067
+ } catch {
1068
+ } finally {
1069
+ try {
1070
+ unlinkSync(path2);
1071
+ } catch {
1072
+ }
1073
+ }
1074
+ }
887
1075
  var log = (message3) => console.log(`[${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)}] ${message3}`);
888
1076
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
889
1077
  function killTree(pid, signal) {
@@ -943,7 +1131,9 @@ async function runDaemon(opts) {
943
1131
  },
944
1132
  release: (run) => {
945
1133
  if (run.timeout) clearTimeout(run.timeout);
1134
+ run.renderer?.finish();
946
1135
  rmSync(run.workspace, { recursive: true, force: true });
1136
+ void uploadRawLog(run);
947
1137
  },
948
1138
  persist: () => {
949
1139
  const entries = table2.values().filter((run) => run.child?.pid !== void 0).map((run) => ({
@@ -999,10 +1189,11 @@ async function runDaemon(opts) {
999
1189
  timedOut: false,
1000
1190
  settled: false,
1001
1191
  keyFingerprint: assignment.run_key.slice(0, 14),
1002
- batcher: new LogBatcher((chunk) => client2.appendRunLog(runId, { chunk }).then(() => {
1003
- }), {
1004
- onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`)
1005
- })
1192
+ batcher: new LogBatcher(
1193
+ (chunk, seq) => client2.appendRunLog(runId, { chunk, seq }).then(() => {
1194
+ }),
1195
+ { onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`) }
1196
+ )
1006
1197
  };
1007
1198
  run.flush = () => run.batcher.flush();
1008
1199
  table2.track(run);
@@ -1066,7 +1257,22 @@ async function runDaemon(opts) {
1066
1257
  run.spawnedAt = Date.now();
1067
1258
  table2.persist();
1068
1259
  log(`run ${runId}: launched ${invocation.file} (pid ${child.pid})`);
1069
- child.stdout?.on("data", (data) => run.batcher.append(data.toString("utf8")));
1260
+ if (opts.harness === "claude_code") {
1261
+ const renderer = new ClaudeStreamRenderer((line) => run.batcher.append(line));
1262
+ run.renderer = renderer;
1263
+ run.drain = () => renderer.finish();
1264
+ const spoolPath = join3(opts.configDir, "rawlogs", `${runId}.ndjson`);
1265
+ mkdirSync3(dirname3(spoolPath), { recursive: true });
1266
+ run.rawSpoolPath = spoolPath;
1267
+ run.rawSpool = createWriteStream(spoolPath);
1268
+ run.rawUpload = (body) => client2.putRunLogRaw(runId, body);
1269
+ child.stdout?.on("data", (data) => {
1270
+ run.rawSpool?.write(data);
1271
+ renderer.write(data.toString("utf8"));
1272
+ });
1273
+ } else {
1274
+ child.stdout?.on("data", (data) => run.batcher.append(data.toString("utf8")));
1275
+ }
1070
1276
  child.stderr?.on("data", (data) => run.batcher.append(data.toString("utf8")));
1071
1277
  run.timeout = setTimeout(
1072
1278
  () => {
@@ -4643,14 +4849,14 @@ function readJsonBody(inline, file) {
4643
4849
  if (file !== void 0 && file !== "-") {
4644
4850
  let raw;
4645
4851
  try {
4646
- raw = readFileSync4(file, "utf8");
4852
+ raw = readFileSync5(file, "utf8");
4647
4853
  } catch (err) {
4648
4854
  die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
4649
4855
  }
4650
4856
  return parseJsonObject(raw, file);
4651
4857
  }
4652
4858
  if (file === "-" || !process.stdin.isTTY) {
4653
- const raw = readFileSync4(0, "utf8");
4859
+ const raw = readFileSync5(0, "utf8");
4654
4860
  if (raw.trim() === "") {
4655
4861
  if (file === "-") die("no JSON on stdin");
4656
4862
  return void 0;
@@ -4792,7 +4998,7 @@ function parseFileSpec(spec) {
4792
4998
  }
4793
4999
  const file = source.slice(1);
4794
5000
  try {
4795
- return { path: path2, content: readFileSync4(file, "utf8") };
5001
+ return { path: path2, content: readFileSync5(file, "utf8") };
4796
5002
  } catch (err) {
4797
5003
  die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
4798
5004
  }
@@ -4990,7 +5196,7 @@ warning: ${w}`);
4990
5196
  function cliVersion() {
4991
5197
  try {
4992
5198
  const manifest = new URL("../package.json", import.meta.url);
4993
- return JSON.parse(readFileSync4(manifest, "utf8")).version ?? "0.0.0-unknown";
5199
+ return JSON.parse(readFileSync5(manifest, "utf8")).version ?? "0.0.0-unknown";
4994
5200
  } catch {
4995
5201
  return "0.0.0-unknown";
4996
5202
  }
@@ -5451,7 +5657,7 @@ function walkFolder(dir) {
5451
5657
  const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
5452
5658
  if (entry.isDirectory()) walk(nextAbs, nextRel);
5453
5659
  else if (entry.isFile()) {
5454
- files.push({ path: nextRel, contentType: sniffContentType(entry.name), bytes: readFileSync4(nextAbs) });
5660
+ files.push({ path: nextRel, contentType: sniffContentType(entry.name), bytes: readFileSync5(nextAbs) });
5455
5661
  }
5456
5662
  }
5457
5663
  };
@@ -5526,7 +5732,7 @@ withCommon(
5526
5732
  const issue = await resolveIssue(api, ref);
5527
5733
  let artifact;
5528
5734
  if (opts.folder !== void 0) {
5529
- if (!existsSync3(opts.folder) || !statSync(opts.folder).isDirectory()) {
5735
+ if (!existsSync3(opts.folder) || !statSync2(opts.folder).isDirectory()) {
5530
5736
  die(`--folder needs a directory, got "${opts.folder}"`);
5531
5737
  }
5532
5738
  const files = walkFolder(opts.folder);
@@ -5538,7 +5744,7 @@ withCommon(
5538
5744
  } else if (opts.file !== void 0) {
5539
5745
  let bytes;
5540
5746
  try {
5541
- bytes = readFileSync4(opts.file);
5747
+ bytes = readFileSync5(opts.file);
5542
5748
  } catch (err) {
5543
5749
  die(`cannot read ${opts.file}: ${err instanceof Error ? err.message : String(err)}`);
5544
5750
  }
@@ -5615,7 +5821,7 @@ withCommon(
5615
5821
  if (opts.out === void 0) {
5616
5822
  die(`artifact "${name2}" is a folder \u2014 pass --out <dir> to write its tree`);
5617
5823
  }
5618
- if (existsSync3(opts.out) && !statSync(opts.out).isDirectory()) {
5824
+ if (existsSync3(opts.out) && !statSync2(opts.out).isDirectory()) {
5619
5825
  die(`--out for a folder must be a directory, and "${opts.out}" is a file`);
5620
5826
  }
5621
5827
  const files = version.files ?? [];
@@ -5638,7 +5844,7 @@ withCommon(
5638
5844
  const bytes = Buffer.from(content.bytes);
5639
5845
  if (opts.out !== void 0) {
5640
5846
  let target2 = opts.out;
5641
- if (existsSync3(target2) && statSync(target2).isDirectory()) {
5847
+ if (existsSync3(target2) && statSync2(target2).isDirectory()) {
5642
5848
  target2 = join4(target2, version.filename ?? name2);
5643
5849
  }
5644
5850
  writeFileSync3(target2, bytes);
@@ -6457,7 +6663,7 @@ withList(
6457
6663
  });
6458
6664
  });
6459
6665
  withCommon(
6460
- runsCmd.command("show <id>").description("Show a run; --logs prints the captured log tail").option("--logs", "print the log tail")
6666
+ runsCmd.command("show <id>").description("Show a run; --logs prints the captured log tail, --logs --full the whole log").option("--logs", "print the log tail").option("--full", "with --logs: print the complete log, not the 256 KB tail").option("--raw", "with --logs --full: print the unrendered harness stream instead")
6461
6667
  ).action(async (id, opts) => {
6462
6668
  const api = client(opts);
6463
6669
  const run = await api.getRun(id);
@@ -6486,8 +6692,24 @@ withCommon(
6486
6692
  if (run.error) console.log(`error: ${run.error}`);
6487
6693
  if (opts.logs) {
6488
6694
  console.log("");
6695
+ if (opts.full || opts.raw) {
6696
+ const res = await api.getRunLogFull(id, { raw: opts.raw });
6697
+ const body = res.body;
6698
+ if (!body) return;
6699
+ const reader = body.getReader();
6700
+ const decoder = new TextDecoder();
6701
+ for (; ; ) {
6702
+ const { done, value } = await reader.read();
6703
+ if (done) break;
6704
+ if (value) process.stdout.write(decoder.decode(value, { stream: true }));
6705
+ }
6706
+ process.stdout.write(decoder.decode());
6707
+ return;
6708
+ }
6489
6709
  if (run.log_bytes_dropped > 0) {
6490
- console.log(`[${Math.round(run.log_bytes_dropped / 1024)} KB truncated from the head]`);
6710
+ console.log(
6711
+ `[${Math.round(run.log_bytes_dropped / 1024)} KB truncated from the head \u2014 ` + (run.log_expired ? "past its retention window; only this tail remains]" : `run \`tines runs show ${run.id} --logs --full\` for the complete ${Math.round(run.log_full_bytes / 1024)} KB log]`)
6712
+ );
6491
6713
  }
6492
6714
  console.log(run.log || "(no log output captured)");
6493
6715
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.82",
3
+ "version": "0.0.84",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",