tines 0.0.170 → 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 (2) hide show
  1. package/dist/index.js +78 -30
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4559,7 +4559,15 @@ function parseTargetSpec(spec) {
4559
4559
  }
4560
4560
 
4561
4561
  // src/config.ts
4562
- 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";
4563
4571
  import { homedir } from "node:os";
4564
4572
  import { dirname, join } from "node:path";
4565
4573
  function defaultConfigDir() {
@@ -4575,8 +4583,15 @@ function readJsonFile(path2) {
4575
4583
  }
4576
4584
  function writeJsonFile(path2, value, { secret = false } = {}) {
4577
4585
  mkdirSync(dirname(path2), { recursive: true });
4578
- writeFileSync(path2, `${JSON.stringify(value, null, 2)}
4586
+ const tmp = `${path2}.tmp-${process.pid}`;
4587
+ try {
4588
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
4579
4589
  `, secret ? { mode: 384 } : {});
4590
+ renameSync(tmp, path2);
4591
+ } catch (e) {
4592
+ rmSync(tmp, { force: true });
4593
+ throw e;
4594
+ }
4580
4595
  }
4581
4596
  function configPath(dir = defaultConfigDir()) {
4582
4597
  return join(dir, "config.json");
@@ -6935,7 +6950,7 @@ import {
6935
6950
  mkdirSync as mkdirSync4,
6936
6951
  readFileSync as readFileSync9,
6937
6952
  realpathSync,
6938
- rmSync as rmSync2,
6953
+ rmSync as rmSync3,
6939
6954
  statSync as statSync4,
6940
6955
  unlinkSync as unlinkSync2,
6941
6956
  writeFileSync as writeFileSync3
@@ -7436,7 +7451,7 @@ function buildHarnessInvocation(spec, input) {
7436
7451
  file: "sh",
7437
7452
  args: [
7438
7453
  "-c",
7439
- `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)}`
7440
7455
  ]
7441
7456
  };
7442
7457
  case "codex":
@@ -7474,7 +7489,10 @@ function formatLaunchBanner(invocation, input, meta) {
7474
7489
  `model=${input.model ?? "(fixed)"}`,
7475
7490
  `timeout=${meta.timeoutMinutes}m`,
7476
7491
  `cli=${meta.cliVersion}`,
7477
- `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}`] : []
7478
7496
  ];
7479
7497
  return `$ ${formatLaunchCommand(invocation)}
7480
7498
  # tines runner: ${fields.join(" ")}
@@ -7538,7 +7556,7 @@ var RunTable = class {
7538
7556
  cleanup(run, outcome = "failed") {
7539
7557
  this.runs.delete(run.runId);
7540
7558
  this.effects.persist();
7541
- this.effects.release(run, { keep: this.keep(outcome), outcome });
7559
+ this.effects.release(run, { keep: this.keep(outcome, run), outcome });
7542
7560
  }
7543
7561
  /**
7544
7562
  * Ends a run: flush logs, finish-report, clean up. On a run someone
@@ -7559,11 +7577,15 @@ var RunTable = class {
7559
7577
  run.settled = true;
7560
7578
  run.endNote ??= error;
7561
7579
  run.drain?.();
7562
- if (this.keep(status)) this.effects.noteKept?.(run);
7580
+ if (this.keep(status, run)) this.effects.noteKept?.(run);
7563
7581
  await run.flush?.();
7582
+ const keptBeforeFinish = this.keep(status, run);
7564
7583
  try {
7565
7584
  await this.effects.finish(run, status, error, judgment);
7566
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
+ }
7567
7589
  } catch (err) {
7568
7590
  this.effects.log(
7569
7591
  `finish report for run ${run.runId} not accepted: ${err instanceof Error ? err.message : String(err)}`
@@ -7818,7 +7840,7 @@ function message(err) {
7818
7840
  }
7819
7841
 
7820
7842
  // src/daemon/store.ts
7821
- 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";
7822
7844
  import { join as join4 } from "node:path";
7823
7845
  function credentialsKey(url, name2) {
7824
7846
  return `${url.replace(/\/+$/, "")}#${name2}`;
@@ -7919,7 +7941,7 @@ function pruneKeptWorkspaces(configDir, opts) {
7919
7941
  const removed = [];
7920
7942
  for (const entry of doomed) {
7921
7943
  try {
7922
- rmSync(entry.path, { recursive: true, force: true });
7944
+ rmSync2(entry.path, { recursive: true, force: true });
7923
7945
  removed.push(entry);
7924
7946
  } catch {
7925
7947
  }
@@ -8017,7 +8039,7 @@ async function runDaemon(opts) {
8017
8039
  sweepKeptWorkspaces();
8018
8040
  const settleWorkspace = (workspace, keep, marker) => {
8019
8041
  if (!keep) {
8020
- rmSync2(workspace, { recursive: true, force: true });
8042
+ rmSync3(workspace, { recursive: true, force: true });
8021
8043
  return;
8022
8044
  }
8023
8045
  if (!existsSync5(workspace)) return;
@@ -8091,13 +8113,19 @@ async function runDaemon(opts) {
8091
8113
  {
8092
8114
  finish: async (run, status, error, judgment) => {
8093
8115
  const summary = run.renderer?.summary();
8094
- await client2.finishRun(run.runId, {
8116
+ const ended = await client2.finishRun(run.runId, {
8095
8117
  status,
8096
8118
  ...error ? { error } : {},
8097
8119
  ...judgment,
8098
8120
  usage: summary?.usage ?? { cost_source: "none" },
8099
- ...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
8100
8127
  });
8128
+ if (ended?.resume_expires_at) run.keepForResume = true;
8101
8129
  },
8102
8130
  release: (run, { keep, outcome }) => {
8103
8131
  if (run.timeout) clearTimeout(run.timeout);
@@ -8126,7 +8154,11 @@ async function runDaemon(opts) {
8126
8154
  },
8127
8155
  log
8128
8156
  },
8129
- { 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
+ }
8130
8162
  );
8131
8163
  for (const orphan of loadDaemonState(statePath)) {
8132
8164
  if (pidAlive(orphan.pid)) {
@@ -8172,7 +8204,13 @@ async function runDaemon(opts) {
8172
8204
  const launch = async (assignment) => {
8173
8205
  const runId = assignment.run.id;
8174
8206
  if (table2.has(runId)) return;
8175
- 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);
8176
8214
  const issueLabel = assignment.run.issue_ref ? `${assignment.run.issue_ref.project_name}/${assignment.run.issue_ref.number}` : void 0;
8177
8215
  const run = {
8178
8216
  runId,
@@ -8182,6 +8220,8 @@ async function runDaemon(opts) {
8182
8220
  timedOut: false,
8183
8221
  settled: false,
8184
8222
  keyFingerprint: assignment.run_key.slice(0, 14),
8223
+ priorTurns: resume?.prior_turn_count ?? 0,
8224
+ keepForResume: false,
8185
8225
  batcher: new LogBatcher(
8186
8226
  (chunk, seq) => client2.appendRunLog(runId, { chunk, seq }).then(() => {
8187
8227
  }),
@@ -8191,26 +8231,32 @@ async function runDaemon(opts) {
8191
8231
  run.flush = () => run.batcher.flush();
8192
8232
  table2.track(run);
8193
8233
  log(
8194
- `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`
8195
8235
  );
8196
8236
  try {
8197
- rmSync2(workspace, { recursive: true, force: true });
8198
- 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
+ }
8199
8243
  writeFileSync3(join5(workspace, "prompt.md"), `${assignment.prompt}
8200
8244
  `);
8201
- for (const skill of assignment.bundle.skills) {
8202
- for (const file of skill.files) {
8203
- const target = join5(workspace, "skills", skill.name, file.path);
8204
- mkdirSync4(dirname4(target), { recursive: true });
8205
- 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
+ }
8206
8252
  }
8207
- }
8208
- writeFileSync3(
8209
- join5(workspace, "repos.json"),
8210
- `${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)}
8211
8257
  `
8212
- );
8213
- for (const repo of assignment.bundle.repos) {
8258
+ );
8259
+ for (const repo of resume ? [] : assignment.bundle.repos) {
8214
8260
  if (run.settled) return table2.cleanup(run);
8215
8261
  const args = [
8216
8262
  "clone",
@@ -8242,7 +8288,8 @@ async function runDaemon(opts) {
8242
8288
  workspace,
8243
8289
  promptFile: join5(workspace, "prompt.md"),
8244
8290
  prompt: assignment.prompt,
8245
- model: assignment.run.model
8291
+ model: assignment.run.model,
8292
+ ...resume ? { resumeSessionId: resume.provider_session_id } : {}
8246
8293
  };
8247
8294
  const invocation = buildHarnessInvocation(
8248
8295
  { harness: opts.harness, command: opts.command },
@@ -8252,7 +8299,8 @@ async function runDaemon(opts) {
8252
8299
  formatLaunchBanner(invocation, harnessInput, {
8253
8300
  harness: opts.harness,
8254
8301
  timeoutMinutes: assignment.timeout_minutes,
8255
- cliVersion: DAEMON_VERSION
8302
+ cliVersion: DAEMON_VERSION,
8303
+ ...resume ? { resumedFromRunId: resume.previous_run_id } : {}
8256
8304
  })
8257
8305
  );
8258
8306
  const child = spawn2(invocation.file, invocation.args, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.170",
3
+ "version": "0.0.171",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",