tines 0.0.114 → 0.0.116

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 +98 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3547,8 +3547,8 @@ var selfRef = () => ({ kind: "self-ref" });
3547
3547
  function str(v) {
3548
3548
  return v === null || v === void 0 ? "" : String(v);
3549
3549
  }
3550
- function joinChanged(v, sep) {
3551
- return Array.isArray(v) ? v.join(sep) : "";
3550
+ function joinChanged(v, sep2) {
3551
+ return Array.isArray(v) ? v.join(sep2) : "";
3552
3552
  }
3553
3553
  function action(type) {
3554
3554
  return type.split(".")[1] ?? type;
@@ -4204,11 +4204,11 @@ function assertNewStatesHavePrompts(states, prompts) {
4204
4204
  }
4205
4205
  }
4206
4206
  function parseScheduleRef(ref) {
4207
- const sep = ref.indexOf("/");
4208
- if (sep < 1 || sep === ref.length - 1) {
4207
+ const sep2 = ref.indexOf("/");
4208
+ if (sep2 < 1 || sep2 === ref.length - 1) {
4209
4209
  throw new CliError(`schedule reference must look like <project>/<name>, got "${ref}"`);
4210
4210
  }
4211
- return { project: ref.slice(0, sep), name: ref.slice(sep + 1) };
4211
+ return { project: ref.slice(0, sep2), name: ref.slice(sep2 + 1) };
4212
4212
  }
4213
4213
  function parseIssueRef(ref) {
4214
4214
  const match = ref.match(/^(.+)\/(\d+)$/);
@@ -4216,12 +4216,12 @@ function parseIssueRef(ref) {
4216
4216
  return { project: match[1], number: Number.parseInt(match[2], 10) };
4217
4217
  }
4218
4218
  function parseFileSpec(spec) {
4219
- const sep = spec.indexOf("=");
4220
- if (sep < 1 || sep === spec.length - 1) {
4219
+ const sep2 = spec.indexOf("=");
4220
+ if (sep2 < 1 || sep2 === spec.length - 1) {
4221
4221
  throw new CliError(`--file must look like <path>=@<local-file>, got "${spec}"`);
4222
4222
  }
4223
- const path2 = spec.slice(0, sep);
4224
- const source = spec.slice(sep + 1);
4223
+ const path2 = spec.slice(0, sep2);
4224
+ const source = spec.slice(sep2 + 1);
4225
4225
  if (!source.startsWith("@")) {
4226
4226
  throw new CliError(
4227
4227
  `skill file content always comes from a local file: --file ${path2}=@<local-file>`
@@ -4235,10 +4235,10 @@ function parseFileSpec(spec) {
4235
4235
  }
4236
4236
  }
4237
4237
  function parseTargetSpec(spec) {
4238
- const sep = spec.lastIndexOf(":");
4239
- if (sep === -1) return { name: spec };
4240
- const name2 = spec.slice(0, sep);
4241
- const tier = spec.slice(sep + 1);
4238
+ const sep2 = spec.lastIndexOf(":");
4239
+ if (sep2 === -1) return { name: spec };
4240
+ const name2 = spec.slice(0, sep2);
4241
+ const tier = spec.slice(sep2 + 1);
4242
4242
  if (!name2) throw new CliError(`target must look like <runner>[:tier], got "${spec}"`);
4243
4243
  if (!MODEL_TIERS.includes(tier)) {
4244
4244
  throw new CliError(`unknown tier "${tier}" in "${spec}" (tiers: ${MODEL_TIERS.join(", ")})`);
@@ -4421,12 +4421,12 @@ async function resolveIssue(api, ref) {
4421
4421
  return api.getIssueByNumber(proj.id, number);
4422
4422
  }
4423
4423
  async function resolveStateFlag(api, ref) {
4424
- const sep = ref.indexOf("/");
4425
- if (sep < 1 || sep === ref.length - 1) {
4424
+ const sep2 = ref.indexOf("/");
4425
+ if (sep2 < 1 || sep2 === ref.length - 1) {
4426
4426
  die(`--state must look like <workflow>/<state>, got "${ref}"`);
4427
4427
  }
4428
- const workflow = await resolveWorkflow(api, ref.slice(0, sep));
4429
- const stateRef = ref.slice(sep + 1);
4428
+ const workflow = await resolveWorkflow(api, ref.slice(0, sep2));
4429
+ const stateRef = ref.slice(sep2 + 1);
4430
4430
  const state = workflow.states.find((s) => s.name === stateRef) ?? workflow.states.find((s) => s.id === stateRef);
4431
4431
  if (!state) {
4432
4432
  die(
@@ -5964,6 +5964,7 @@ import {
5964
5964
  existsSync as existsSync4,
5965
5965
  mkdirSync as mkdirSync4,
5966
5966
  readFileSync as readFileSync9,
5967
+ realpathSync,
5967
5968
  rmSync as rmSync2,
5968
5969
  statSync as statSync3,
5969
5970
  unlinkSync as unlinkSync2,
@@ -6085,7 +6086,7 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as rea
6085
6086
  import { dirname as dirname3, join as join3 } from "node:path";
6086
6087
 
6087
6088
  // src/daemon/support.ts
6088
- import { delimiter } from "node:path";
6089
+ import { delimiter, sep } from "node:path";
6089
6090
  var HARNESS_KINDS = ["claude_code", "codex", "custom"];
6090
6091
  var KEEP_WORKSPACES_MODES = ["never", "failed", "always"];
6091
6092
  function keepWorkspace(mode, outcome) {
@@ -6333,6 +6334,14 @@ var CliRefresher = class {
6333
6334
  cached = AMBIENT_CLI;
6334
6335
  lastAttempt = null;
6335
6336
  inFlight = null;
6337
+ /**
6338
+ * Resolves once no install is in flight, starting none. A daemon about to
6339
+ * exit for a self-update waits on this so it never restarts into a
6340
+ * half-extracted prefix.
6341
+ */
6342
+ settled() {
6343
+ return this.inFlight ? this.inFlight.then(() => void 0) : Promise.resolve();
6344
+ }
6336
6345
  /** The current CLI, refreshing first when the TTL elapsed. Never rejects. */
6337
6346
  ensure() {
6338
6347
  if (this.inFlight) return this.inFlight;
@@ -6349,6 +6358,26 @@ var CliRefresher = class {
6349
6358
  return this.inFlight;
6350
6359
  }
6351
6360
  };
6361
+ function isNewerVersion(candidate, current) {
6362
+ const parse = (v) => /^\d+(\.\d+)*$/.test(v) ? v.split(".").map(Number) : null;
6363
+ const a = parse(candidate);
6364
+ const b = parse(current);
6365
+ if (!a || !b) return false;
6366
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
6367
+ const x = a[i] ?? 0;
6368
+ const y = b[i] ?? 0;
6369
+ if (x !== y) return x > y;
6370
+ }
6371
+ return false;
6372
+ }
6373
+ function pendingSelfUpdate(cli, running) {
6374
+ if (cli.source === "ambient" || !cli.version) return null;
6375
+ return isNewerVersion(cli.version, running) ? cli.version : null;
6376
+ }
6377
+ function pathWithin(path2, dir) {
6378
+ const root = dir.endsWith(sep) ? dir : dir + sep;
6379
+ return path2.startsWith(root);
6380
+ }
6352
6381
  function buildSpawnEnv(base, opts) {
6353
6382
  const env = {
6354
6383
  ...base,
@@ -6688,7 +6717,28 @@ async function runDaemon(opts) {
6688
6717
  const refresher = new CliRefresher(() => installAgentCli({ configDir: opts.configDir, log }), {
6689
6718
  ttlMs: CLI_REFRESH_TTL_MS
6690
6719
  });
6691
- const ensureCli = () => opts.cliRefresh ? refresher.ensure() : Promise.resolve(AMBIENT_CLI);
6720
+ const prefix = agentCliPrefix(opts.configDir);
6721
+ const selfPath = realpathOrNull(process.argv[1]);
6722
+ const managedInstall = selfPath !== null && pathWithin(selfPath, realpathOrNull(prefix) ?? prefix);
6723
+ const selfUpdate = opts.selfUpdate && opts.cliRefresh && managedInstall;
6724
+ if (opts.selfUpdate && opts.cliRefresh) {
6725
+ log(
6726
+ managedInstall ? `self-update: on \u2014 this daemon (tines ${DAEMON_VERSION}) runs from ${prefix} and will restart once idle after a newer release is installed there` : `self-update: off \u2014 this daemon (tines ${DAEMON_VERSION}) runs from ${selfPath ?? process.argv[1] ?? "?"}, not ${prefix}; launch it from ${join5(prefix, "node_modules", ".bin", "tines")} to have refreshes apply on restart (docs/runner-daemon.md)`
6727
+ );
6728
+ }
6729
+ let pendingUpdate = null;
6730
+ const noteCli = (cli) => {
6731
+ if (!selfUpdate || pendingUpdate) return cli;
6732
+ const newer = pendingSelfUpdate(cli, DAEMON_VERSION);
6733
+ if (newer) {
6734
+ pendingUpdate = newer;
6735
+ log(
6736
+ `tines ${newer} is installed in ${prefix} (this daemon is ${DAEMON_VERSION}); draining \u2014 no new runs accepted, restarting once the in-flight ones finish`
6737
+ );
6738
+ }
6739
+ return cli;
6740
+ };
6741
+ const ensureCli = () => opts.cliRefresh ? refresher.ensure().then(noteCli) : Promise.resolve(AMBIENT_CLI);
6692
6742
  const cliLabel = (cli) => cli.source === "ambient" ? `ambient PATH (${opts.cliRefresh ? "refresh failed" : "refresh disabled"})` : `tines ${cli.version ?? "unknown"} (daemon-managed${cli.source === "stale" ? ", last-good copy" : ""})`;
6693
6743
  log(`agent CLI: ${cliLabel(await ensureCli())}`);
6694
6744
  const table2 = new RunTable(
@@ -6946,7 +6996,11 @@ async function runDaemon(opts) {
6946
6996
  try {
6947
6997
  const res = await client2.pollRunner(creds.runner_id, {
6948
6998
  owned_runs: table2.ids(),
6949
- max_concurrent: opts.maxConcurrent
6999
+ max_concurrent: opts.maxConcurrent,
7000
+ // Stated on every poll while pending; absent otherwise, which
7001
+ // the server reads as "not draining" — so a daemon that died
7002
+ // mid-drain cannot pin its runner shut past its relaunch.
7003
+ ...pendingUpdate ? { draining: true } : {}
6950
7004
  });
6951
7005
  failures = 0;
6952
7006
  for (const runId of res.cancels) killWithoutFinish(runId);
@@ -6958,6 +7012,14 @@ async function runDaemon(opts) {
6958
7012
  }
6959
7013
  void launch(assignment);
6960
7014
  }
7015
+ if (selfUpdate && !pendingUpdate) void ensureCli();
7016
+ if (pendingUpdate && res.assignments.length === 0 && table2.size === 0) {
7017
+ await refresher.settled();
7018
+ log(
7019
+ `exiting to restart as tines ${pendingUpdate} \u2014 the service manager relaunches this daemon (KeepAlive / Restart=always)`
7020
+ );
7021
+ process.exit(0);
7022
+ }
6961
7023
  } catch (err) {
6962
7024
  if (err instanceof ApiError && err.status === 401) {
6963
7025
  for (const run of table2.values()) {
@@ -6988,6 +7050,14 @@ async function runDaemon(opts) {
6988
7050
  function message2(err) {
6989
7051
  return err instanceof Error ? err.message : String(err);
6990
7052
  }
7053
+ function realpathOrNull(path2) {
7054
+ if (!path2) return null;
7055
+ try {
7056
+ return realpathSync(path2);
7057
+ } catch {
7058
+ return null;
7059
+ }
7060
+ }
6991
7061
  function runGit(args, cwd, batcher) {
6992
7062
  return new Promise((resolve) => {
6993
7063
  const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -7239,6 +7309,9 @@ function register8(program3) {
7239
7309
  ).option(
7240
7310
  "--no-cli-refresh",
7241
7311
  "do not install/refresh the agent-facing tines CLI from npm (harnesses use the ambient PATH)"
7312
+ ).option(
7313
+ "--no-self-update",
7314
+ "do not exit for the service manager to relaunch a newer daemon (only applies when launched from the daemon-managed prefix)"
7242
7315
  ).option(
7243
7316
  "--keep-workspaces <mode>",
7244
7317
  "keep settled runs' workspaces for debugging: never | failed | always",
@@ -7294,6 +7367,7 @@ function register8(program3) {
7294
7367
  pollIntervalMs: opts.pollInterval * 1e3,
7295
7368
  configDir: defaultConfigDir(),
7296
7369
  cliRefresh: opts.cliRefresh,
7370
+ selfUpdate: opts.selfUpdate,
7297
7371
  keepWorkspaces,
7298
7372
  keepWorkspacesForHours: opts.keepWorkspacesFor,
7299
7373
  keepWorkspacesMax: opts.keepWorkspacesMax
@@ -7683,12 +7757,12 @@ function register10(program3) {
7683
7757
  const api = client(opts);
7684
7758
  const overrides = {};
7685
7759
  for (const spec of opts.state) {
7686
- const sep = spec.lastIndexOf("=");
7687
- if (sep < 1 || sep === spec.length - 1) {
7760
+ const sep2 = spec.lastIndexOf("=");
7761
+ if (sep2 < 1 || sep2 === spec.length - 1) {
7688
7762
  die(`--state must look like <workflow>/<state>=<n>, got "${spec}"`);
7689
7763
  }
7690
- const limit = Number.parseInt(spec.slice(sep + 1), 10);
7691
- const { state } = await resolveStateFlag(api, spec.slice(0, sep));
7764
+ const limit = Number.parseInt(spec.slice(sep2 + 1), 10);
7765
+ const { state } = await resolveStateFlag(api, spec.slice(0, sep2));
7692
7766
  overrides[state.id] = limit;
7693
7767
  }
7694
7768
  const settings = await api.updateSupervisorSettings({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.114",
3
+ "version": "0.0.116",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",