tines 0.0.169 → 0.0.171

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 (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +148 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,6 +5,10 @@ built for humans and their agents: work moves through workflows, every action is
5
5
  record, and a supervisor hands eligible issues to agents. This package talks to the same
6
6
  HTTP API the web app uses.
7
7
 
8
+ Awaiting-session continuation policy fields are staged and default off. Runtime continuation is
9
+ unavailable until support for the runner's provider is released; configuring thresholds does not
10
+ enable continuation.
11
+
8
12
  ```sh
9
13
  npm install -g tines # or, one-shot with no install:
10
14
  npx -y tines --help
package/dist/index.js CHANGED
@@ -3474,6 +3474,10 @@ function parsePrSpec(spec) {
3474
3474
  }
3475
3475
  var MODEL_TIERS = ["smartest", "balanced", "cheapest"];
3476
3476
  var RUNNER_ONLINE_WINDOW_MS = 2 * 60 * 1e3;
3477
+ var DEFAULT_RESUME_WINDOW_HOURS = 48;
3478
+ var DEFAULT_RESUME_MAX_TURNS = 25;
3479
+ var DEFAULT_RESUME_MAX_TOKENS = 1e5;
3480
+ var DEFAULT_RESUME_MAX_COST_USD = 2;
3477
3481
  var RUNNER_OFFLINE_FAIL_MS = 5 * 60 * 1e3;
3478
3482
  var LAUNCH_STALL_MS = 5 * 60 * 1e3;
3479
3483
  var RUN_KEY_SLACK_MS = 10 * 60 * 1e3;
@@ -4436,7 +4440,7 @@ function runRow(run) {
4436
4440
  run.issue_ref ? issueRef(run.issue_ref) : run.issue_id,
4437
4441
  run.runner_name,
4438
4442
  `${run.tier}${run.model ? ` (${run.model})` : ""}`,
4439
- run.status,
4443
+ `${run.status}${run.resumed_from_run_id ? ` \xB7 resumed run ${run.resumed_from_run_id}` : ""}`,
4440
4444
  runDurationLabel(run),
4441
4445
  runCostLabel(run) ?? "\u2014",
4442
4446
  timestamp(run.created_at)
@@ -4555,7 +4559,15 @@ function parseTargetSpec(spec) {
4555
4559
  }
4556
4560
 
4557
4561
  // src/config.ts
4558
- import { existsSync, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "node:fs";
4562
+ import {
4563
+ existsSync,
4564
+ mkdirSync,
4565
+ readFileSync as readFileSync3,
4566
+ renameSync,
4567
+ rmSync,
4568
+ unlinkSync,
4569
+ writeFileSync
4570
+ } from "node:fs";
4559
4571
  import { homedir } from "node:os";
4560
4572
  import { dirname, join } from "node:path";
4561
4573
  function defaultConfigDir() {
@@ -4571,8 +4583,15 @@ function readJsonFile(path2) {
4571
4583
  }
4572
4584
  function writeJsonFile(path2, value, { secret = false } = {}) {
4573
4585
  mkdirSync(dirname(path2), { recursive: true });
4574
- writeFileSync(path2, `${JSON.stringify(value, null, 2)}
4586
+ const tmp = `${path2}.tmp-${process.pid}`;
4587
+ try {
4588
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
4575
4589
  `, secret ? { mode: 384 } : {});
4590
+ renameSync(tmp, path2);
4591
+ } catch (e) {
4592
+ rmSync(tmp, { force: true });
4593
+ throw e;
4594
+ }
4576
4595
  }
4577
4596
  function configPath(dir = defaultConfigDir()) {
4578
4597
  return join(dir, "config.json");
@@ -6931,7 +6950,7 @@ import {
6931
6950
  mkdirSync as mkdirSync4,
6932
6951
  readFileSync as readFileSync9,
6933
6952
  realpathSync,
6934
- rmSync as rmSync2,
6953
+ rmSync as rmSync3,
6935
6954
  statSync as statSync4,
6936
6955
  unlinkSync as unlinkSync2,
6937
6956
  writeFileSync as writeFileSync3
@@ -7432,7 +7451,7 @@ function buildHarnessInvocation(spec, input) {
7432
7451
  file: "sh",
7433
7452
  args: [
7434
7453
  "-c",
7435
- `claude -p --output-format stream-json --verbose${input.model ? ` --model ${shellQuote(input.model)}` : ""} < ${shellQuote(input.promptFile)}`
7454
+ `claude -p${input.resumeSessionId ? ` --resume ${shellQuote(input.resumeSessionId)}` : ""} --output-format stream-json --verbose${input.model ? ` --model ${shellQuote(input.model)}` : ""} < ${shellQuote(input.promptFile)}`
7436
7455
  ]
7437
7456
  };
7438
7457
  case "codex":
@@ -7470,7 +7489,10 @@ function formatLaunchBanner(invocation, input, meta) {
7470
7489
  `model=${input.model ?? "(fixed)"}`,
7471
7490
  `timeout=${meta.timeoutMinutes}m`,
7472
7491
  `cli=${meta.cliVersion}`,
7473
- `workspace=${input.workspace}`
7492
+ `workspace=${input.workspace}`,
7493
+ // The one line that says a send-back reused a session rather than
7494
+ // re-exploring the repository from zero.
7495
+ ...input.resumeSessionId ? [`resumed=${meta.resumedFromRunId ?? input.resumeSessionId}`] : []
7474
7496
  ];
7475
7497
  return `$ ${formatLaunchCommand(invocation)}
7476
7498
  # tines runner: ${fields.join(" ")}
@@ -7534,7 +7556,7 @@ var RunTable = class {
7534
7556
  cleanup(run, outcome = "failed") {
7535
7557
  this.runs.delete(run.runId);
7536
7558
  this.effects.persist();
7537
- this.effects.release(run, { keep: this.keep(outcome), outcome });
7559
+ this.effects.release(run, { keep: this.keep(outcome, run), outcome });
7538
7560
  }
7539
7561
  /**
7540
7562
  * Ends a run: flush logs, finish-report, clean up. On a run someone
@@ -7555,11 +7577,15 @@ var RunTable = class {
7555
7577
  run.settled = true;
7556
7578
  run.endNote ??= error;
7557
7579
  run.drain?.();
7558
- if (this.keep(status)) this.effects.noteKept?.(run);
7580
+ if (this.keep(status, run)) this.effects.noteKept?.(run);
7559
7581
  await run.flush?.();
7582
+ const keptBeforeFinish = this.keep(status, run);
7560
7583
  try {
7561
7584
  await this.effects.finish(run, status, error, judgment);
7562
7585
  this.effects.log(`run ${run.runId} finished: ${status}${error ? ` (${error})` : ""}`);
7586
+ if (!keptBeforeFinish && this.keep(status, run)) {
7587
+ this.effects.log(`run ${run.runId} workspace kept for resume at ${run.workspace}`);
7588
+ }
7563
7589
  } catch (err) {
7564
7590
  this.effects.log(
7565
7591
  `finish report for run ${run.runId} not accepted: ${err instanceof Error ? err.message : String(err)}`
@@ -7814,7 +7840,7 @@ function message(err) {
7814
7840
  }
7815
7841
 
7816
7842
  // src/daemon/store.ts
7817
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync, statSync as statSync3 } from "node:fs";
7843
+ import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
7818
7844
  import { join as join4 } from "node:path";
7819
7845
  function credentialsKey(url, name2) {
7820
7846
  return `${url.replace(/\/+$/, "")}#${name2}`;
@@ -7915,7 +7941,7 @@ function pruneKeptWorkspaces(configDir, opts) {
7915
7941
  const removed = [];
7916
7942
  for (const entry of doomed) {
7917
7943
  try {
7918
- rmSync(entry.path, { recursive: true, force: true });
7944
+ rmSync2(entry.path, { recursive: true, force: true });
7919
7945
  removed.push(entry);
7920
7946
  } catch {
7921
7947
  }
@@ -8013,7 +8039,7 @@ async function runDaemon(opts) {
8013
8039
  sweepKeptWorkspaces();
8014
8040
  const settleWorkspace = (workspace, keep, marker) => {
8015
8041
  if (!keep) {
8016
- rmSync2(workspace, { recursive: true, force: true });
8042
+ rmSync3(workspace, { recursive: true, force: true });
8017
8043
  return;
8018
8044
  }
8019
8045
  if (!existsSync5(workspace)) return;
@@ -8087,13 +8113,19 @@ async function runDaemon(opts) {
8087
8113
  {
8088
8114
  finish: async (run, status, error, judgment) => {
8089
8115
  const summary = run.renderer?.summary();
8090
- await client2.finishRun(run.runId, {
8116
+ const ended = await client2.finishRun(run.runId, {
8091
8117
  status,
8092
8118
  ...error ? { error } : {},
8093
8119
  ...judgment,
8094
8120
  usage: summary?.usage ?? { cost_source: "none" },
8095
- ...summary?.providerSessionId ? { provider_session_id: summary.providerSessionId } : {}
8121
+ ...summary?.providerSessionId ? { provider_session_id: summary.providerSessionId } : {},
8122
+ ...summary?.numTurns !== void 0 ? { turn_count: summary.numTurns } : {},
8123
+ // The whole conversation's turns, which for a resumed run is
8124
+ // more than this run's own: the size guard reads this one.
8125
+ ...summary?.numTurns !== void 0 ? { conversation_turn_count: run.priorTurns + summary.numTurns } : {},
8126
+ workspace_path: run.workspace
8096
8127
  });
8128
+ if (ended?.resume_expires_at) run.keepForResume = true;
8097
8129
  },
8098
8130
  release: (run, { keep, outcome }) => {
8099
8131
  if (run.timeout) clearTimeout(run.timeout);
@@ -8122,7 +8154,11 @@ async function runDaemon(opts) {
8122
8154
  },
8123
8155
  log
8124
8156
  },
8125
- { keep: (outcome) => keepWorkspace(opts.keepWorkspaces, outcome) }
8157
+ {
8158
+ // A workspace the server retained for a resume is kept whatever the
8159
+ // --keep-workspaces mode says; pruning still bounds it by age/count.
8160
+ keep: (outcome, run) => run.keepForResume || keepWorkspace(opts.keepWorkspaces, outcome)
8161
+ }
8126
8162
  );
8127
8163
  for (const orphan of loadDaemonState(statePath)) {
8128
8164
  if (pidAlive(orphan.pid)) {
@@ -8168,7 +8204,13 @@ async function runDaemon(opts) {
8168
8204
  const launch = async (assignment) => {
8169
8205
  const runId = assignment.run.id;
8170
8206
  if (table2.has(runId)) return;
8171
- const workspace = join5(workspacesDir(opts.configDir), runId);
8207
+ const resume = assignment.resume && existsSync5(assignment.resume.workspace_path) ? assignment.resume : null;
8208
+ if (assignment.resume && !resume) {
8209
+ log(
8210
+ `run ${runId}: resume workspace ${assignment.resume.workspace_path} is gone; launching fresh`
8211
+ );
8212
+ }
8213
+ const workspace = resume ? resume.workspace_path : join5(workspacesDir(opts.configDir), runId);
8172
8214
  const issueLabel = assignment.run.issue_ref ? `${assignment.run.issue_ref.project_name}/${assignment.run.issue_ref.number}` : void 0;
8173
8215
  const run = {
8174
8216
  runId,
@@ -8178,6 +8220,8 @@ async function runDaemon(opts) {
8178
8220
  timedOut: false,
8179
8221
  settled: false,
8180
8222
  keyFingerprint: assignment.run_key.slice(0, 14),
8223
+ priorTurns: resume?.prior_turn_count ?? 0,
8224
+ keepForResume: false,
8181
8225
  batcher: new LogBatcher(
8182
8226
  (chunk, seq) => client2.appendRunLog(runId, { chunk, seq }).then(() => {
8183
8227
  }),
@@ -8187,26 +8231,32 @@ async function runDaemon(opts) {
8187
8231
  run.flush = () => run.batcher.flush();
8188
8232
  table2.track(run);
8189
8233
  log(
8190
- `run ${runId} assigned (issue ${issueLabel ?? assignment.run.issue_id}); materializing workspace`
8234
+ resume ? `run ${runId} assigned (issue ${issueLabel ?? assignment.run.issue_id}); resuming run ${resume.previous_run_id} in ${workspace}` : `run ${runId} assigned (issue ${issueLabel ?? assignment.run.issue_id}); materializing workspace`
8191
8235
  );
8192
8236
  try {
8193
- rmSync2(workspace, { recursive: true, force: true });
8194
- mkdirSync4(workspace, { recursive: true });
8237
+ if (!resume) {
8238
+ rmSync3(workspace, { recursive: true, force: true });
8239
+ mkdirSync4(workspace, { recursive: true });
8240
+ } else {
8241
+ rmSync3(join5(workspace, "kept.json"), { force: true });
8242
+ }
8195
8243
  writeFileSync3(join5(workspace, "prompt.md"), `${assignment.prompt}
8196
8244
  `);
8197
- for (const skill of assignment.bundle.skills) {
8198
- for (const file of skill.files) {
8199
- const target = join5(workspace, "skills", skill.name, file.path);
8200
- mkdirSync4(dirname4(target), { recursive: true });
8201
- writeFileSync3(target, file.content);
8245
+ if (!resume)
8246
+ for (const skill of assignment.bundle.skills) {
8247
+ for (const file of skill.files) {
8248
+ const target = join5(workspace, "skills", skill.name, file.path);
8249
+ mkdirSync4(dirname4(target), { recursive: true });
8250
+ writeFileSync3(target, file.content);
8251
+ }
8202
8252
  }
8203
- }
8204
- writeFileSync3(
8205
- join5(workspace, "repos.json"),
8206
- `${JSON.stringify(assignment.bundle.repos, null, 2)}
8253
+ if (!resume)
8254
+ writeFileSync3(
8255
+ join5(workspace, "repos.json"),
8256
+ `${JSON.stringify(assignment.bundle.repos, null, 2)}
8207
8257
  `
8208
- );
8209
- for (const repo of assignment.bundle.repos) {
8258
+ );
8259
+ for (const repo of resume ? [] : assignment.bundle.repos) {
8210
8260
  if (run.settled) return table2.cleanup(run);
8211
8261
  const args = [
8212
8262
  "clone",
@@ -8238,7 +8288,8 @@ async function runDaemon(opts) {
8238
8288
  workspace,
8239
8289
  promptFile: join5(workspace, "prompt.md"),
8240
8290
  prompt: assignment.prompt,
8241
- model: assignment.run.model
8291
+ model: assignment.run.model,
8292
+ ...resume ? { resumeSessionId: resume.provider_session_id } : {}
8242
8293
  };
8243
8294
  const invocation = buildHarnessInvocation(
8244
8295
  { harness: opts.harness, command: opts.command },
@@ -8248,7 +8299,8 @@ async function runDaemon(opts) {
8248
8299
  formatLaunchBanner(invocation, harnessInput, {
8249
8300
  harness: opts.harness,
8250
8301
  timeoutMinutes: assignment.timeout_minutes,
8251
- cliVersion: DAEMON_VERSION
8302
+ cliVersion: DAEMON_VERSION,
8303
+ ...resume ? { resumedFromRunId: resume.previous_run_id } : {}
8252
8304
  })
8253
8305
  );
8254
8306
  const child = spawn2(invocation.file, invocation.args, {
@@ -8461,6 +8513,29 @@ function runGit(args, cwd, batcher) {
8461
8513
  }
8462
8514
 
8463
8515
  // src/commands/runners.ts
8516
+ function parseBoolean(value) {
8517
+ if (value === "true") return true;
8518
+ if (value === "false") return false;
8519
+ throw new InvalidArgumentError("must be true or false");
8520
+ }
8521
+ function boundedInteger(min, max) {
8522
+ return (value) => {
8523
+ if (!/^\d+$/.test(value))
8524
+ throw new InvalidArgumentError(`must be an integer between ${min} and ${max}`);
8525
+ const parsed = Number(value);
8526
+ if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {
8527
+ throw new InvalidArgumentError(`must be an integer between ${min} and ${max}`);
8528
+ }
8529
+ return parsed;
8530
+ };
8531
+ }
8532
+ function resumeCost(value) {
8533
+ const parsed = Number(value);
8534
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1e3) {
8535
+ throw new InvalidArgumentError("must be greater than 0 and at most 1000");
8536
+ }
8537
+ return parsed;
8538
+ }
8464
8539
  function printTierTable(runner) {
8465
8540
  if (!runner.tier_models && !runner.tiers) {
8466
8541
  console.log("tiers: don't apply to this runner (fixed configuration)");
@@ -8527,6 +8602,12 @@ function register8(program3) {
8527
8602
  if (runner.type !== "local") {
8528
8603
  console.log(`api key: ${runner.has_api_key ? "set (write-only)" : "missing"}`);
8529
8604
  }
8605
+ console.log(
8606
+ `resume awaiting sessions: ${runner.resume_enabled ?? false ? "enabled" : "disabled"} window: ${runner.resume_window_hours ?? DEFAULT_RESUME_WINDOW_HOURS}h (staged; runtime continuation unavailable)`
8607
+ );
8608
+ console.log(
8609
+ `resume limits: ${runner.resume_max_turns ?? DEFAULT_RESUME_MAX_TURNS} local turns ${(runner.resume_max_tokens ?? DEFAULT_RESUME_MAX_TOKENS).toLocaleString()} managed tokens $${runner.resume_max_cost_usd ?? DEFAULT_RESUME_MAX_COST_USD} managed cost`
8610
+ );
8530
8611
  if (runner.budget) {
8531
8612
  const b = runner.budget;
8532
8613
  const parts = [];
@@ -8540,6 +8621,39 @@ function register8(program3) {
8540
8621
  printTierTable(runner);
8541
8622
  }
8542
8623
  );
8624
+ withCommon(
8625
+ runners.command("edit <name>").description("Edit staged awaiting-session continuation policy (runtime unavailable)").option(
8626
+ "--resume-enabled <true|false>",
8627
+ "stage opt-in policy (enabling is unavailable until provider support ships)",
8628
+ parseBoolean
8629
+ ).option("--resume-window-hours <n>", "continuation window (1-168)", boundedInteger(1, 168)).option(
8630
+ "--resume-max-turns <n>",
8631
+ "local conversation turn limit (1-1000)",
8632
+ boundedInteger(1, 1e3)
8633
+ ).option(
8634
+ "--resume-max-tokens <n>",
8635
+ "managed conversation token limit (1-10000000)",
8636
+ boundedInteger(1, 1e7)
8637
+ ).option("--resume-max-cost-usd <n>", "managed conversation cost limit (0-1000]", resumeCost)
8638
+ ).action(
8639
+ async (ref, opts) => {
8640
+ const patch = {
8641
+ ...opts.resumeEnabled !== void 0 ? { resume_enabled: opts.resumeEnabled } : {},
8642
+ ...opts.resumeWindowHours !== void 0 ? { resume_window_hours: opts.resumeWindowHours } : {},
8643
+ ...opts.resumeMaxTurns !== void 0 ? { resume_max_turns: opts.resumeMaxTurns } : {},
8644
+ ...opts.resumeMaxTokens !== void 0 ? { resume_max_tokens: opts.resumeMaxTokens } : {},
8645
+ ...opts.resumeMaxCostUsd !== void 0 ? { resume_max_cost_usd: opts.resumeMaxCostUsd } : {}
8646
+ };
8647
+ if (Object.keys(patch).length === 0) die("provide at least one resume setting");
8648
+ const api = client(opts);
8649
+ const runner = await resolveRunner(api, ref);
8650
+ const updated = await api.updateRunner(runner.id, patch);
8651
+ if (opts.json) return printJson(updated);
8652
+ console.log(
8653
+ `updated runner "${updated.name}"; awaiting-session resume is ${updated.resume_enabled ? "enabled" : "disabled"}`
8654
+ );
8655
+ }
8656
+ );
8543
8657
  withCommon(
8544
8658
  runners.command("tiers <name>").description("Show or edit a runner's tier\u2192model mapping").option("--default <tier>", "set the default tier (used by targets without an explicit tier)").option("--set <tier=model...>", "override a tier with an exact model id (repeatable)").option("--unset <tier...>", "drop an override, falling back to the built-in (repeatable)")
8545
8659
  ).action(
@@ -8836,7 +8950,9 @@ function register8(program3) {
8836
8950
  const api = client(opts);
8837
8951
  const run = await api.getRun(id);
8838
8952
  if (opts.json) return printJson(run);
8839
- console.log(`${run.id} ${run.status} on ${run.runner_name}`);
8953
+ console.log(
8954
+ `${run.id} ${run.status} on ${run.runner_name}${run.resumed_from_run_id ? ` \xB7 resumed run ${run.resumed_from_run_id}` : ""}`
8955
+ );
8840
8956
  if (run.issue_ref) console.log(`issue: ${issueRef(run.issue_ref)} \u2014 ${run.issue_ref.title}`);
8841
8957
  console.log(`tier: ${run.tier} model: ${run.model ?? "(n/a)"}`);
8842
8958
  console.log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.169",
3
+ "version": "0.0.171",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",