tines 0.0.203 → 0.0.205
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.
- package/README.md +5 -3
- package/dist/index.js +1136 -374
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3407,8 +3407,8 @@ var STATE_CATEGORIES = [
|
|
|
3407
3407
|
"awaiting_human",
|
|
3408
3408
|
"done"
|
|
3409
3409
|
];
|
|
3410
|
-
function runRefLabel(
|
|
3411
|
-
return
|
|
3410
|
+
function runRefLabel(run2) {
|
|
3411
|
+
return run2.issue_ref ? `run on ${run2.issue_ref.project_name}/${run2.issue_ref.number}` : `run ${run2.run_id}`;
|
|
3412
3412
|
}
|
|
3413
3413
|
function actorLabel(actor) {
|
|
3414
3414
|
if (actor.run) {
|
|
@@ -3530,28 +3530,28 @@ function ageLabel(at, now = Date.now()) {
|
|
|
3530
3530
|
function prUrlOf(v) {
|
|
3531
3531
|
return v.pr_repo_url && v.pr_number !== null ? `${v.pr_repo_url}/pull/${v.pr_number}` : null;
|
|
3532
3532
|
}
|
|
3533
|
-
function runDurationLabel(
|
|
3534
|
-
if (!
|
|
3535
|
-
const seconds = Math.max(0, Math.round(((
|
|
3533
|
+
function runDurationLabel(run2, now = Date.now()) {
|
|
3534
|
+
if (!run2.started_at) return "\u2014";
|
|
3535
|
+
const seconds = Math.max(0, Math.round(((run2.ended_at ?? now) - run2.started_at) / 1e3));
|
|
3536
3536
|
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m`;
|
|
3537
3537
|
}
|
|
3538
|
-
function runCostLabel(
|
|
3539
|
-
const
|
|
3540
|
-
if (!
|
|
3541
|
-
if (
|
|
3542
|
-
const dollars =
|
|
3543
|
-
const source =
|
|
3538
|
+
function runCostLabel(run2) {
|
|
3539
|
+
const usage2 = run2.usage;
|
|
3540
|
+
if (!usage2) return null;
|
|
3541
|
+
if (usage2.cost_usd !== void 0) {
|
|
3542
|
+
const dollars = usage2.cost_usd === 0 ? "$0" : usage2.cost_usd < 0.01 ? "<$0.01" : `$${usage2.cost_usd.toFixed(2)}`;
|
|
3543
|
+
const source = usage2.cost_source === "provider" ? "Reported" : usage2.cost_source === "priced" ? "Estimated" : "Recorded";
|
|
3544
3544
|
return `${dollars} ${source}`;
|
|
3545
3545
|
}
|
|
3546
|
-
if (
|
|
3547
|
-
if (
|
|
3546
|
+
if (usage2.pricing?.status === "unpriced") return "Unpriced";
|
|
3547
|
+
if (usage2.cost_source === "none") return "Unreported";
|
|
3548
3548
|
const measured = [
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3549
|
+
usage2.input_tokens,
|
|
3550
|
+
usage2.output_tokens,
|
|
3551
|
+
usage2.cache_read_tokens,
|
|
3552
|
+
usage2.cache_write_tokens
|
|
3553
3553
|
].some((value) => value !== void 0);
|
|
3554
|
-
const tokens = (
|
|
3554
|
+
const tokens = (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_tokens ?? 0) + (usage2.cache_write_tokens ?? 0);
|
|
3555
3555
|
return tokens > 0 ? `${tokens.toLocaleString()} tok` : measured ? "Unpriced" : null;
|
|
3556
3556
|
}
|
|
3557
3557
|
function utilizationLabel(quota, activeRuns, stateName = (id2) => id2) {
|
|
@@ -3559,13 +3559,13 @@ function utilizationLabel(quota, activeRuns, stateName = (id2) => id2) {
|
|
|
3559
3559
|
return `${activeRuns.length}/${quota.limit} global slot${quota.limit === 1 ? "" : "s"} in use`;
|
|
3560
3560
|
}
|
|
3561
3561
|
const counts = /* @__PURE__ */ new Map();
|
|
3562
|
-
for (const
|
|
3563
|
-
const entry = counts.get(
|
|
3564
|
-
name:
|
|
3562
|
+
for (const run2 of activeRuns) {
|
|
3563
|
+
const entry = counts.get(run2.state_id_at_start) ?? {
|
|
3564
|
+
name: run2.state_at_start_name ?? stateName(run2.state_id_at_start),
|
|
3565
3565
|
n: 0
|
|
3566
3566
|
};
|
|
3567
3567
|
entry.n += 1;
|
|
3568
|
-
counts.set(
|
|
3568
|
+
counts.set(run2.state_id_at_start, entry);
|
|
3569
3569
|
}
|
|
3570
3570
|
for (const stateId of Object.keys(quota.overrides)) {
|
|
3571
3571
|
if (!counts.has(stateId)) counts.set(stateId, { name: stateName(stateId), n: 0 });
|
|
@@ -3587,8 +3587,8 @@ function str(v) {
|
|
|
3587
3587
|
function isoTime(v) {
|
|
3588
3588
|
return typeof v === "number" && Number.isFinite(v) ? new Date(v).toISOString() : "an unknown time";
|
|
3589
3589
|
}
|
|
3590
|
-
function joinChanged(v,
|
|
3591
|
-
return Array.isArray(v) ? v.join(
|
|
3590
|
+
function joinChanged(v, sep4) {
|
|
3591
|
+
return Array.isArray(v) ? v.join(sep4) : "";
|
|
3592
3592
|
}
|
|
3593
3593
|
function action(type) {
|
|
3594
3594
|
return type.split(".")[1] ?? type;
|
|
@@ -5354,30 +5354,30 @@ function structuralDocument(value, allowMissingDigest) {
|
|
|
5354
5354
|
throw new LibraryValidationError([
|
|
5355
5355
|
{ path: "", code: "invalid_document", message: "Library document must be an object" }
|
|
5356
5356
|
]);
|
|
5357
|
-
const
|
|
5358
|
-
if (
|
|
5357
|
+
const object4 = value;
|
|
5358
|
+
if (object4.format !== "tines.library")
|
|
5359
5359
|
throw new LibraryValidationError([
|
|
5360
5360
|
{ path: "/format", code: "invalid_format", message: "Expected tines.library" }
|
|
5361
5361
|
]);
|
|
5362
|
-
if (
|
|
5362
|
+
if (object4.version !== 3)
|
|
5363
5363
|
throw new LibraryValidationError([
|
|
5364
5364
|
{
|
|
5365
5365
|
path: "/version",
|
|
5366
5366
|
code: "unsupported_version",
|
|
5367
|
-
message: `Unsupported library version ${String(
|
|
5367
|
+
message: `Unsupported library version ${String(object4.version)}`
|
|
5368
5368
|
}
|
|
5369
5369
|
]);
|
|
5370
|
-
if (
|
|
5370
|
+
if (object4.profile !== "workflow" && object4.profile !== "library")
|
|
5371
5371
|
throw new LibraryValidationError([
|
|
5372
5372
|
{ path: "/profile", code: "invalid_profile", message: "Expected workflow or library profile" }
|
|
5373
5373
|
]);
|
|
5374
|
-
const allowed = TOP_LEVEL[
|
|
5375
|
-
const unknown = Object.keys(
|
|
5374
|
+
const allowed = TOP_LEVEL[object4.profile];
|
|
5375
|
+
const unknown = Object.keys(object4).find((key) => !allowed.includes(key));
|
|
5376
5376
|
if (unknown)
|
|
5377
5377
|
throw new LibraryValidationError([
|
|
5378
5378
|
{ path: `/${unknown}`, code: "unknown_field", message: `Unknown field ${unknown}` }
|
|
5379
5379
|
]);
|
|
5380
|
-
if (!Number.isSafeInteger(
|
|
5380
|
+
if (!Number.isSafeInteger(object4.exported_at) || object4.exported_at < 0)
|
|
5381
5381
|
throw new LibraryValidationError([
|
|
5382
5382
|
{
|
|
5383
5383
|
path: "/exported_at",
|
|
@@ -5385,7 +5385,7 @@ function structuralDocument(value, allowMissingDigest) {
|
|
|
5385
5385
|
message: "exported_at must be a non-negative safe integer"
|
|
5386
5386
|
}
|
|
5387
5387
|
]);
|
|
5388
|
-
if (!(allowMissingDigest &&
|
|
5388
|
+
if (!(allowMissingDigest && object4.digest === void 0) && (typeof object4.digest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(object4.digest)))
|
|
5389
5389
|
throw new LibraryValidationError([
|
|
5390
5390
|
{
|
|
5391
5391
|
path: "/digest",
|
|
@@ -5395,14 +5395,14 @@ function structuralDocument(value, allowMissingDigest) {
|
|
|
5395
5395
|
]);
|
|
5396
5396
|
for (const key of allowed) {
|
|
5397
5397
|
if (key === "digest" && allowMissingDigest) continue;
|
|
5398
|
-
if (!(key in
|
|
5398
|
+
if (!(key in object4))
|
|
5399
5399
|
throw new LibraryValidationError([
|
|
5400
5400
|
{ path: `/${key}`, code: "missing_field", message: `Missing required field ${key}` }
|
|
5401
5401
|
]);
|
|
5402
5402
|
}
|
|
5403
5403
|
return {
|
|
5404
|
-
...
|
|
5405
|
-
digest: typeof
|
|
5404
|
+
...object4,
|
|
5405
|
+
digest: typeof object4.digest === "string" ? object4.digest : ""
|
|
5406
5406
|
};
|
|
5407
5407
|
}
|
|
5408
5408
|
async function parseLibraryV3Document(input2, options = {}) {
|
|
@@ -5623,7 +5623,7 @@ function roundLines(round, now = Date.now()) {
|
|
|
5623
5623
|
if (latest.artifacts.length > 0) {
|
|
5624
5624
|
lines2.push(` ${latest.artifacts.map(artifactChangeLabel).join(" \xB7 ")}`);
|
|
5625
5625
|
}
|
|
5626
|
-
for (const
|
|
5626
|
+
for (const run2 of earlier) lines2.push(` ${earlierAttemptLine(run2, now)}`);
|
|
5627
5627
|
if (latest.summary_comment) {
|
|
5628
5628
|
lines2.push(` summary (${latest.summary_comment.id}):`);
|
|
5629
5629
|
for (const line of latest.summary_comment.body.split("\n")) lines2.push(` ${line}`);
|
|
@@ -5637,20 +5637,20 @@ function roundLines(round, now = Date.now()) {
|
|
|
5637
5637
|
}
|
|
5638
5638
|
return lines2;
|
|
5639
5639
|
}
|
|
5640
|
-
function runHeadline(
|
|
5641
|
-
const cost = runCostLabel(
|
|
5640
|
+
function runHeadline(run2, now) {
|
|
5641
|
+
const cost = runCostLabel(run2);
|
|
5642
5642
|
return [
|
|
5643
|
-
`${
|
|
5644
|
-
runDurationLabel(
|
|
5645
|
-
|
|
5643
|
+
`${run2.run_id} on ${run2.runner_name}`,
|
|
5644
|
+
runDurationLabel(run2, now),
|
|
5645
|
+
run2.outcome ?? run2.status,
|
|
5646
5646
|
...cost ? [cost] : [],
|
|
5647
|
-
|
|
5647
|
+
run2.transition ? `"${run2.transition.action ?? "moved directly"}" \u2192 ${run2.transition.to_state.name}` : "no transition"
|
|
5648
5648
|
].join(" \xB7 ");
|
|
5649
5649
|
}
|
|
5650
|
-
function earlierAttemptLine(
|
|
5651
|
-
const back =
|
|
5650
|
+
function earlierAttemptLine(run2, now) {
|
|
5651
|
+
const back = run2.returned_via;
|
|
5652
5652
|
const how = back ? back.action ? `sent back by ${back.from_state.name} ("${back.action}")` : "moved directly back" : "never came back";
|
|
5653
|
-
return `${
|
|
5653
|
+
return `${run2.run_id} \xB7 ${runDurationLabel(run2, now)} \xB7 ${run2.outcome ?? run2.status} \xB7 ${how}`;
|
|
5654
5654
|
}
|
|
5655
5655
|
function artifactChangeLabel(a) {
|
|
5656
5656
|
const versions = a.from_version === null ? `v${a.to_version}` : `v${a.from_version} \u2192 v${a.to_version}`;
|
|
@@ -5669,16 +5669,16 @@ function roundSummaryLabel(summary) {
|
|
|
5669
5669
|
];
|
|
5670
5670
|
return parts.length > 0 ? parts.join(" \xB7 ") : "\u2014";
|
|
5671
5671
|
}
|
|
5672
|
-
function runRow(
|
|
5672
|
+
function runRow(run2) {
|
|
5673
5673
|
return [
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
`${
|
|
5678
|
-
`${
|
|
5679
|
-
runDurationLabel(
|
|
5680
|
-
runCostLabel(
|
|
5681
|
-
timestamp(
|
|
5674
|
+
run2.id,
|
|
5675
|
+
run2.issue_ref ? issueRef(run2.issue_ref) : run2.issue_id,
|
|
5676
|
+
run2.runner_name,
|
|
5677
|
+
`${run2.tier}${run2.model ? ` (${run2.model})` : ""}`,
|
|
5678
|
+
`${run2.status}${run2.resumed_from_run_id ? ` \xB7 resumed run ${run2.resumed_from_run_id}` : ""}`,
|
|
5679
|
+
runDurationLabel(run2),
|
|
5680
|
+
runCostLabel(run2) ?? "\u2014",
|
|
5681
|
+
timestamp(run2.created_at)
|
|
5682
5682
|
];
|
|
5683
5683
|
}
|
|
5684
5684
|
function byteSize(bytes2) {
|
|
@@ -5751,11 +5751,11 @@ function assertNewStatesHavePrompts(states, prompts) {
|
|
|
5751
5751
|
}
|
|
5752
5752
|
}
|
|
5753
5753
|
function parseScheduleRef(ref) {
|
|
5754
|
-
const
|
|
5755
|
-
if (
|
|
5754
|
+
const sep4 = ref.indexOf("/");
|
|
5755
|
+
if (sep4 < 1 || sep4 === ref.length - 1) {
|
|
5756
5756
|
throw new CliError(`schedule reference must look like <project>/<name>, got "${ref}"`);
|
|
5757
5757
|
}
|
|
5758
|
-
return { project: ref.slice(0,
|
|
5758
|
+
return { project: ref.slice(0, sep4), name: ref.slice(sep4 + 1) };
|
|
5759
5759
|
}
|
|
5760
5760
|
function parseIssueRef(ref) {
|
|
5761
5761
|
const match = ref.match(/^(.+)\/(\d+)$/);
|
|
@@ -5763,12 +5763,12 @@ function parseIssueRef(ref) {
|
|
|
5763
5763
|
return { project: match[1], number: Number.parseInt(match[2], 10) };
|
|
5764
5764
|
}
|
|
5765
5765
|
function parseFileSpec(spec) {
|
|
5766
|
-
const
|
|
5767
|
-
if (
|
|
5766
|
+
const sep4 = spec.indexOf("=");
|
|
5767
|
+
if (sep4 < 1 || sep4 === spec.length - 1) {
|
|
5768
5768
|
throw new CliError(`--file must look like <path>=@<local-file>, got "${spec}"`);
|
|
5769
5769
|
}
|
|
5770
|
-
const path2 = spec.slice(0,
|
|
5771
|
-
const source = spec.slice(
|
|
5770
|
+
const path2 = spec.slice(0, sep4);
|
|
5771
|
+
const source = spec.slice(sep4 + 1);
|
|
5772
5772
|
if (!source.startsWith("@")) {
|
|
5773
5773
|
throw new CliError(
|
|
5774
5774
|
`skill file content always comes from a local file: --file ${path2}=@<local-file>`
|
|
@@ -5782,10 +5782,10 @@ function parseFileSpec(spec) {
|
|
|
5782
5782
|
}
|
|
5783
5783
|
}
|
|
5784
5784
|
function parseTargetSpec(spec) {
|
|
5785
|
-
const
|
|
5786
|
-
if (
|
|
5787
|
-
const name2 = spec.slice(0,
|
|
5788
|
-
const tier = spec.slice(
|
|
5785
|
+
const sep4 = spec.lastIndexOf(":");
|
|
5786
|
+
if (sep4 === -1) return { name: spec };
|
|
5787
|
+
const name2 = spec.slice(0, sep4);
|
|
5788
|
+
const tier = spec.slice(sep4 + 1);
|
|
5789
5789
|
if (!name2) throw new CliError(`target must look like <runner>[:tier], got "${spec}"`);
|
|
5790
5790
|
if (!MODEL_TIERS.includes(tier)) {
|
|
5791
5791
|
throw new CliError(`unknown tier "${tier}" in "${spec}" (tiers: ${MODEL_TIERS.join(", ")})`);
|
|
@@ -5997,12 +5997,12 @@ async function resolveLabelFlag(api, ref) {
|
|
|
5997
5997
|
return match.id;
|
|
5998
5998
|
}
|
|
5999
5999
|
async function resolveStateFlag(api, ref) {
|
|
6000
|
-
const
|
|
6001
|
-
if (
|
|
6000
|
+
const sep4 = ref.indexOf("/");
|
|
6001
|
+
if (sep4 < 1 || sep4 === ref.length - 1) {
|
|
6002
6002
|
die(`--state must look like <workflow>/<state>, got "${ref}"`);
|
|
6003
6003
|
}
|
|
6004
|
-
const workflow2 = await resolveWorkflow(api, ref.slice(0,
|
|
6005
|
-
const stateRef2 = ref.slice(
|
|
6004
|
+
const workflow2 = await resolveWorkflow(api, ref.slice(0, sep4));
|
|
6005
|
+
const stateRef2 = ref.slice(sep4 + 1);
|
|
6006
6006
|
const state = workflow2.states.find((s) => s.name === stateRef2) ?? workflow2.states.find((s) => s.id === stateRef2);
|
|
6007
6007
|
if (!state) {
|
|
6008
6008
|
die(
|
|
@@ -8489,9 +8489,6 @@ function register7(program3) {
|
|
|
8489
8489
|
});
|
|
8490
8490
|
}
|
|
8491
8491
|
|
|
8492
|
-
// src/commands/runners.ts
|
|
8493
|
-
import { hostname as hostname2 } from "node:os";
|
|
8494
|
-
|
|
8495
8492
|
// src/daemon/daemon.ts
|
|
8496
8493
|
import { spawn as spawn2 } from "node:child_process";
|
|
8497
8494
|
import {
|
|
@@ -8505,10 +8502,9 @@ import {
|
|
|
8505
8502
|
unlinkSync as unlinkSync2,
|
|
8506
8503
|
writeFileSync as writeFileSync3
|
|
8507
8504
|
} from "node:fs";
|
|
8508
|
-
import { hostname, platform, arch } from "node:os";
|
|
8509
8505
|
import { randomUUID } from "node:crypto";
|
|
8510
|
-
import { dirname as dirname4, join as
|
|
8511
|
-
import { StringDecoder } from "node:string_decoder";
|
|
8506
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
8507
|
+
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
8512
8508
|
|
|
8513
8509
|
// src/version.ts
|
|
8514
8510
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
@@ -8535,7 +8531,17 @@ function copySummary(value) {
|
|
|
8535
8531
|
...value.pricingEvidence ? {
|
|
8536
8532
|
pricingEvidence: {
|
|
8537
8533
|
...value.pricingEvidence,
|
|
8538
|
-
...value.pricingEvidence.raw_usage ? { raw_usage: { ...value.pricingEvidence.raw_usage } } : {}
|
|
8534
|
+
...value.pricingEvidence.raw_usage ? { raw_usage: { ...value.pricingEvidence.raw_usage } } : {},
|
|
8535
|
+
...value.pricingEvidence.request_context ? {
|
|
8536
|
+
request_context: {
|
|
8537
|
+
...value.pricingEvidence.request_context,
|
|
8538
|
+
...value.pricingEvidence.request_context.status === "complete" ? {
|
|
8539
|
+
reconciled_usage: {
|
|
8540
|
+
...value.pricingEvidence.request_context.reconciled_usage
|
|
8541
|
+
}
|
|
8542
|
+
} : {}
|
|
8543
|
+
}
|
|
8544
|
+
} : {}
|
|
8539
8545
|
}
|
|
8540
8546
|
} : {}
|
|
8541
8547
|
};
|
|
@@ -8663,10 +8669,10 @@ var ClaudeStreamRenderer = class {
|
|
|
8663
8669
|
}
|
|
8664
8670
|
if (validMetric(event.num_turns)) this.collected.numTurns = event.num_turns;
|
|
8665
8671
|
if (validMetric(event.duration_ms)) this.collected.durationMs = event.duration_ms;
|
|
8666
|
-
const
|
|
8672
|
+
const usage2 = { cost_source: "provider" };
|
|
8667
8673
|
let measured = false;
|
|
8668
8674
|
if (validMetric(event.total_cost_usd)) {
|
|
8669
|
-
|
|
8675
|
+
usage2.cost_usd = event.total_cost_usd;
|
|
8670
8676
|
measured = true;
|
|
8671
8677
|
}
|
|
8672
8678
|
const raw = event.usage;
|
|
@@ -8678,12 +8684,12 @@ var ClaudeStreamRenderer = class {
|
|
|
8678
8684
|
["cache_creation_input_tokens", "cache_write_tokens"]
|
|
8679
8685
|
]) {
|
|
8680
8686
|
if (validMetric(raw[source])) {
|
|
8681
|
-
|
|
8687
|
+
usage2[destination] = raw[source];
|
|
8682
8688
|
measured = true;
|
|
8683
8689
|
}
|
|
8684
8690
|
}
|
|
8685
8691
|
}
|
|
8686
|
-
if (measured) this.collected.usage =
|
|
8692
|
+
if (measured) this.collected.usage = usage2;
|
|
8687
8693
|
}
|
|
8688
8694
|
};
|
|
8689
8695
|
|
|
@@ -8840,12 +8846,12 @@ var CodexStreamRenderer = class {
|
|
|
8840
8846
|
this.stickyStatus = "nonmonotonic";
|
|
8841
8847
|
}
|
|
8842
8848
|
status = this.stickyStatus ?? status;
|
|
8843
|
-
const
|
|
8844
|
-
if (complete)
|
|
8845
|
-
if (rawCached !== void 0)
|
|
8846
|
-
if (rawWrite !== void 0)
|
|
8847
|
-
if (output !== void 0)
|
|
8848
|
-
this.collected.usage =
|
|
8849
|
+
const usage2 = {};
|
|
8850
|
+
if (complete) usage2.input_tokens = input2;
|
|
8851
|
+
if (rawCached !== void 0) usage2.cache_read_tokens = rawCached;
|
|
8852
|
+
if (rawWrite !== void 0) usage2.cache_write_tokens = rawWrite;
|
|
8853
|
+
if (output !== void 0) usage2.output_tokens = output;
|
|
8854
|
+
this.collected.usage = usage2;
|
|
8849
8855
|
this.updateEvidence(status, void 0, rawUsage);
|
|
8850
8856
|
}
|
|
8851
8857
|
updateEvidence(status, rerouted, rawUsage) {
|
|
@@ -8866,6 +8872,261 @@ var CodexStreamRenderer = class {
|
|
|
8866
8872
|
}
|
|
8867
8873
|
};
|
|
8868
8874
|
|
|
8875
|
+
// src/daemon/codex-rollout.ts
|
|
8876
|
+
import { open, opendir, realpath, stat } from "node:fs/promises";
|
|
8877
|
+
import { isAbsolute, join as join3, relative, resolve, sep } from "node:path";
|
|
8878
|
+
import { homedir as homedir2 } from "node:os";
|
|
8879
|
+
import { StringDecoder } from "node:string_decoder";
|
|
8880
|
+
var CODEX_ROLLOUT_MAX_BYTES = 128 * 1024 * 1024;
|
|
8881
|
+
var CODEX_ROLLOUT_MAX_LINE_BYTES = 8 * 1024 * 1024;
|
|
8882
|
+
var CODEX_ROLLOUT_MAX_LINES = 25e4;
|
|
8883
|
+
var CODEX_ROLLOUT_MAX_REQUESTS = 1e4;
|
|
8884
|
+
var CODEX_ROLLOUT_TIMEOUT_MS = 5e3;
|
|
8885
|
+
var SUPPORTED_VERSION = "0.153.4";
|
|
8886
|
+
var FIELDS = [
|
|
8887
|
+
"input_tokens",
|
|
8888
|
+
"cached_input_tokens",
|
|
8889
|
+
"cache_write_input_tokens",
|
|
8890
|
+
"output_tokens"
|
|
8891
|
+
];
|
|
8892
|
+
var unavailable = (reason) => ({
|
|
8893
|
+
version: 1,
|
|
8894
|
+
normalization: "codex-rollout-delta-v1",
|
|
8895
|
+
status: "unavailable",
|
|
8896
|
+
reason
|
|
8897
|
+
});
|
|
8898
|
+
var invalid2 = (reason, harness_version) => ({
|
|
8899
|
+
version: 1,
|
|
8900
|
+
normalization: "codex-rollout-delta-v1",
|
|
8901
|
+
...harness_version ? { harness_version: harness_version.slice(0, 100) } : {},
|
|
8902
|
+
status: "invalid",
|
|
8903
|
+
reason
|
|
8904
|
+
});
|
|
8905
|
+
function object3(value) {
|
|
8906
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
8907
|
+
}
|
|
8908
|
+
function usage(value, validateClasses = true) {
|
|
8909
|
+
const raw = object3(value);
|
|
8910
|
+
if (!raw) return;
|
|
8911
|
+
const out = {};
|
|
8912
|
+
for (const field of FIELDS) {
|
|
8913
|
+
const metric = raw[field];
|
|
8914
|
+
if (!Number.isSafeInteger(metric) || metric < 0) return;
|
|
8915
|
+
out[field] = metric;
|
|
8916
|
+
}
|
|
8917
|
+
if (validateClasses && out.cached_input_tokens + out.cache_write_input_tokens > out.input_tokens)
|
|
8918
|
+
return;
|
|
8919
|
+
return out;
|
|
8920
|
+
}
|
|
8921
|
+
function equal(a, b) {
|
|
8922
|
+
return FIELDS.every((field) => a[field] === b[field]);
|
|
8923
|
+
}
|
|
8924
|
+
function reconcileCodexRollout(records, input2) {
|
|
8925
|
+
let metadata = false;
|
|
8926
|
+
let harnessVersion;
|
|
8927
|
+
let observedModel;
|
|
8928
|
+
let previous = {
|
|
8929
|
+
input_tokens: 0,
|
|
8930
|
+
cached_input_tokens: 0,
|
|
8931
|
+
cache_write_input_tokens: 0,
|
|
8932
|
+
output_tokens: 0
|
|
8933
|
+
};
|
|
8934
|
+
let observed = false;
|
|
8935
|
+
let requestCount = 0;
|
|
8936
|
+
let maxRequestInput = 0;
|
|
8937
|
+
let previousLast = { ...previous };
|
|
8938
|
+
for (const value of records) {
|
|
8939
|
+
const record = object3(value);
|
|
8940
|
+
const payload = object3(record?.payload);
|
|
8941
|
+
if (record?.type === "session_meta") {
|
|
8942
|
+
if (metadata) return invalid2("metadata_mismatch", harnessVersion);
|
|
8943
|
+
harnessVersion = typeof payload?.cli_version === "string" ? payload.cli_version : void 0;
|
|
8944
|
+
if (payload?.id !== input2.threadId || payload?.source !== "exec" || payload?.originator !== "codex_exec")
|
|
8945
|
+
return invalid2("metadata_mismatch", harnessVersion);
|
|
8946
|
+
metadata = true;
|
|
8947
|
+
continue;
|
|
8948
|
+
}
|
|
8949
|
+
if (record?.type === "turn_context" && typeof payload?.model === "string") {
|
|
8950
|
+
observedModel ??= payload.model;
|
|
8951
|
+
if (observedModel !== payload.model || input2.model && payload.model !== input2.model)
|
|
8952
|
+
return invalid2("model_mismatch", harnessVersion);
|
|
8953
|
+
continue;
|
|
8954
|
+
}
|
|
8955
|
+
if (record?.type !== "event_msg" || payload?.type !== "token_count") continue;
|
|
8956
|
+
const info = object3(payload.info);
|
|
8957
|
+
if (!info) continue;
|
|
8958
|
+
const total = usage(info.total_token_usage);
|
|
8959
|
+
const last = usage(info.last_token_usage, false);
|
|
8960
|
+
if (!total || !last) return invalid2("missing_dimension", harnessVersion);
|
|
8961
|
+
observed = true;
|
|
8962
|
+
if (equal(total, previous)) {
|
|
8963
|
+
if (!equal(last, previousLast)) return invalid2("delta_mismatch", harnessVersion);
|
|
8964
|
+
continue;
|
|
8965
|
+
}
|
|
8966
|
+
if (FIELDS.some((field) => total[field] < previous[field]))
|
|
8967
|
+
return invalid2("nonmonotonic", harnessVersion);
|
|
8968
|
+
const delta = {};
|
|
8969
|
+
for (const field of FIELDS) delta[field] = total[field] - previous[field];
|
|
8970
|
+
if (delta.cached_input_tokens + delta.cache_write_input_tokens > delta.input_tokens)
|
|
8971
|
+
return invalid2("nonmonotonic", harnessVersion);
|
|
8972
|
+
if (!equal(delta, last)) return invalid2("delta_mismatch", harnessVersion);
|
|
8973
|
+
requestCount++;
|
|
8974
|
+
if (requestCount > CODEX_ROLLOUT_MAX_REQUESTS) return invalid2("limit_exceeded", harnessVersion);
|
|
8975
|
+
maxRequestInput = Math.max(maxRequestInput, delta.input_tokens);
|
|
8976
|
+
previous = total;
|
|
8977
|
+
previousLast = delta;
|
|
8978
|
+
}
|
|
8979
|
+
if (!metadata) return invalid2("metadata_mismatch", harnessVersion);
|
|
8980
|
+
if (harnessVersion !== SUPPORTED_VERSION) {
|
|
8981
|
+
return {
|
|
8982
|
+
version: 1,
|
|
8983
|
+
normalization: "codex-rollout-delta-v1",
|
|
8984
|
+
...harnessVersion ? { harness_version: harnessVersion.slice(0, 100) } : {},
|
|
8985
|
+
status: "unsupported",
|
|
8986
|
+
reason: "unsupported_version"
|
|
8987
|
+
};
|
|
8988
|
+
}
|
|
8989
|
+
const terminalZero = FIELDS.every((field) => input2.terminalUsage[field] === 0);
|
|
8990
|
+
if (!observed && !terminalZero) return invalid2("missing_dimension", harnessVersion);
|
|
8991
|
+
if (!equal(previous, input2.terminalUsage)) return invalid2("terminal_mismatch", harnessVersion);
|
|
8992
|
+
return {
|
|
8993
|
+
version: 1,
|
|
8994
|
+
normalization: "codex-rollout-delta-v1",
|
|
8995
|
+
harness_version: SUPPORTED_VERSION,
|
|
8996
|
+
status: "complete",
|
|
8997
|
+
request_count: requestCount,
|
|
8998
|
+
max_request_input_tokens: maxRequestInput,
|
|
8999
|
+
reconciled_usage: { ...previous }
|
|
9000
|
+
};
|
|
9001
|
+
}
|
|
9002
|
+
function resolveCodexHome(env, workspace) {
|
|
9003
|
+
const configured = env.CODEX_HOME;
|
|
9004
|
+
return configured ? resolve(workspace, configured) : join3(homedir2(), ".codex");
|
|
9005
|
+
}
|
|
9006
|
+
function dateDirectories(startedAt, endedAt) {
|
|
9007
|
+
const first = new Date(startedAt);
|
|
9008
|
+
first.setUTCDate(first.getUTCDate() - 1);
|
|
9009
|
+
first.setUTCHours(0, 0, 0, 0);
|
|
9010
|
+
const last = new Date(endedAt);
|
|
9011
|
+
last.setUTCDate(last.getUTCDate() + 1);
|
|
9012
|
+
last.setUTCHours(0, 0, 0, 0);
|
|
9013
|
+
const result = [];
|
|
9014
|
+
for (let date = first; date <= last; date = new Date(date.getTime() + 864e5)) {
|
|
9015
|
+
result.push(
|
|
9016
|
+
join3(
|
|
9017
|
+
String(date.getUTCFullYear()),
|
|
9018
|
+
String(date.getUTCMonth() + 1).padStart(2, "0"),
|
|
9019
|
+
String(date.getUTCDate()).padStart(2, "0")
|
|
9020
|
+
)
|
|
9021
|
+
);
|
|
9022
|
+
if (result.length > 10) return null;
|
|
9023
|
+
}
|
|
9024
|
+
return result;
|
|
9025
|
+
}
|
|
9026
|
+
async function readCodexRolloutRecords(stream, timeoutMs) {
|
|
9027
|
+
let timedOut = false;
|
|
9028
|
+
const timer = setTimeout(
|
|
9029
|
+
() => {
|
|
9030
|
+
timedOut = true;
|
|
9031
|
+
stream.destroy(new Error("Codex rollout read deadline exceeded"));
|
|
9032
|
+
},
|
|
9033
|
+
Math.max(0, timeoutMs)
|
|
9034
|
+
);
|
|
9035
|
+
const decoder = new StringDecoder("utf8");
|
|
9036
|
+
let pending = "";
|
|
9037
|
+
let bytes2 = 0;
|
|
9038
|
+
let lines2 = 0;
|
|
9039
|
+
const records = [];
|
|
9040
|
+
const consume = (line) => {
|
|
9041
|
+
if (++lines2 > CODEX_ROLLOUT_MAX_LINES || Buffer.byteLength(line) > CODEX_ROLLOUT_MAX_LINE_BYTES)
|
|
9042
|
+
return { reason: "limit_exceeded" };
|
|
9043
|
+
if (!line.trim()) return;
|
|
9044
|
+
try {
|
|
9045
|
+
records.push(JSON.parse(line));
|
|
9046
|
+
} catch {
|
|
9047
|
+
return { reason: "malformed" };
|
|
9048
|
+
}
|
|
9049
|
+
};
|
|
9050
|
+
try {
|
|
9051
|
+
for await (const value of stream) {
|
|
9052
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
9053
|
+
bytes2 += chunk.length;
|
|
9054
|
+
if (bytes2 > CODEX_ROLLOUT_MAX_BYTES) return { reason: "limit_exceeded" };
|
|
9055
|
+
pending += decoder.write(chunk);
|
|
9056
|
+
let newline;
|
|
9057
|
+
while ((newline = pending.indexOf("\n")) >= 0) {
|
|
9058
|
+
const failure = consume(pending.slice(0, newline));
|
|
9059
|
+
if (failure) return failure;
|
|
9060
|
+
pending = pending.slice(newline + 1);
|
|
9061
|
+
}
|
|
9062
|
+
if (Buffer.byteLength(pending) > CODEX_ROLLOUT_MAX_LINE_BYTES)
|
|
9063
|
+
return { reason: "limit_exceeded" };
|
|
9064
|
+
}
|
|
9065
|
+
pending += decoder.end();
|
|
9066
|
+
if (pending.length) {
|
|
9067
|
+
const failure = consume(pending);
|
|
9068
|
+
if (failure) return failure;
|
|
9069
|
+
}
|
|
9070
|
+
return { records };
|
|
9071
|
+
} catch {
|
|
9072
|
+
return { reason: timedOut ? "limit_exceeded" : "read_failed" };
|
|
9073
|
+
} finally {
|
|
9074
|
+
clearTimeout(timer);
|
|
9075
|
+
stream.destroy();
|
|
9076
|
+
}
|
|
9077
|
+
}
|
|
9078
|
+
async function collectCodexRequestContext(input2) {
|
|
9079
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(input2.threadId))
|
|
9080
|
+
return unavailable("thread_id_missing");
|
|
9081
|
+
const dates = dateDirectories(input2.startedAt, input2.endedAt);
|
|
9082
|
+
if (!dates) return unavailable("limit_exceeded");
|
|
9083
|
+
const sessions = join3(input2.codexHome, "sessions");
|
|
9084
|
+
let root;
|
|
9085
|
+
try {
|
|
9086
|
+
root = await realpath(sessions);
|
|
9087
|
+
} catch {
|
|
9088
|
+
return unavailable("rollout_missing");
|
|
9089
|
+
}
|
|
9090
|
+
const candidates = [];
|
|
9091
|
+
let entries = 0;
|
|
9092
|
+
const deadline = Date.now() + CODEX_ROLLOUT_TIMEOUT_MS;
|
|
9093
|
+
try {
|
|
9094
|
+
for (const date of dates) {
|
|
9095
|
+
const directory = join3(root, date);
|
|
9096
|
+
let stream;
|
|
9097
|
+
try {
|
|
9098
|
+
stream = await opendir(directory);
|
|
9099
|
+
} catch {
|
|
9100
|
+
continue;
|
|
9101
|
+
}
|
|
9102
|
+
for await (const entry of stream) {
|
|
9103
|
+
if (++entries > 2e4 || Date.now() > deadline) return unavailable("limit_exceeded");
|
|
9104
|
+
if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.startsWith("rollout-") && entry.name.endsWith(`-${input2.threadId}.jsonl`))
|
|
9105
|
+
candidates.push(join3(directory, entry.name));
|
|
9106
|
+
}
|
|
9107
|
+
}
|
|
9108
|
+
if (candidates.length === 0) return unavailable("rollout_missing");
|
|
9109
|
+
if (candidates.length !== 1) return unavailable("rollout_ambiguous");
|
|
9110
|
+
const candidate = await realpath(candidates[0]);
|
|
9111
|
+
const inside = relative(root, candidate);
|
|
9112
|
+
if (inside.startsWith(`..${sep}`) || inside === ".." || isAbsolute(inside))
|
|
9113
|
+
return unavailable("unsafe_path");
|
|
9114
|
+
const before = await stat(candidate);
|
|
9115
|
+
if (!before.isFile() || before.size > CODEX_ROLLOUT_MAX_BYTES)
|
|
9116
|
+
return unavailable("limit_exceeded");
|
|
9117
|
+
const handle = await open(candidate, "r");
|
|
9118
|
+
const read = await readCodexRolloutRecords(handle.createReadStream(), deadline - Date.now());
|
|
9119
|
+
if ("reason" in read)
|
|
9120
|
+
return read.reason === "malformed" ? invalid2("malformed") : unavailable(read.reason);
|
|
9121
|
+
const after = await stat(candidate);
|
|
9122
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
9123
|
+
return invalid2("read_failed");
|
|
9124
|
+
return reconcileCodexRollout(read.records, input2);
|
|
9125
|
+
} catch {
|
|
9126
|
+
return unavailable("read_failed");
|
|
9127
|
+
}
|
|
9128
|
+
}
|
|
9129
|
+
|
|
8869
9130
|
// src/daemon/rate-limit.ts
|
|
8870
9131
|
var USAGE_LIMIT_PREFIXES = [
|
|
8871
9132
|
"You've hit your",
|
|
@@ -8969,11 +9230,11 @@ var DURATION = /(\d+)\s*([dhms])/g;
|
|
|
8969
9230
|
function parseResetTime(text4, now) {
|
|
8970
9231
|
const raw = text4.trim();
|
|
8971
9232
|
if (!raw) return null;
|
|
8972
|
-
const
|
|
8973
|
-
if (
|
|
9233
|
+
const relative2 = raw.match(/^in\s+(.+)$/i);
|
|
9234
|
+
if (relative2) {
|
|
8974
9235
|
let ms = 0;
|
|
8975
9236
|
let matched = false;
|
|
8976
|
-
for (const [, amount, unit] of
|
|
9237
|
+
for (const [, amount, unit] of relative2[1].matchAll(DURATION)) {
|
|
8977
9238
|
const n = Number(amount);
|
|
8978
9239
|
ms += n * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit];
|
|
8979
9240
|
matched = true;
|
|
@@ -9056,10 +9317,10 @@ var RateLimitDetector = class {
|
|
|
9056
9317
|
// src/daemon/cli-refresh.ts
|
|
9057
9318
|
import { spawn } from "node:child_process";
|
|
9058
9319
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
9059
|
-
import { dirname as dirname3, join as
|
|
9320
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
9060
9321
|
|
|
9061
9322
|
// src/daemon/support.ts
|
|
9062
|
-
import { delimiter, sep } from "node:path";
|
|
9323
|
+
import { delimiter, sep as sep2 } from "node:path";
|
|
9063
9324
|
var HARNESS_KINDS = ["claude_code", "codex", "custom"];
|
|
9064
9325
|
var KEEP_WORKSPACES_MODES = ["never", "failed", "always"];
|
|
9065
9326
|
function keepWorkspace(mode, outcome) {
|
|
@@ -9138,9 +9399,9 @@ function formatExitLine(exit) {
|
|
|
9138
9399
|
return `# tines runner: exit ${parts.join(" ")} after ${formatDuration(exit.durationMs)}
|
|
9139
9400
|
`;
|
|
9140
9401
|
}
|
|
9141
|
-
function exitLineForRun(
|
|
9142
|
-
if (
|
|
9143
|
-
return formatExitLine({ ...exit, timedOut:
|
|
9402
|
+
function exitLineForRun(run2, exit) {
|
|
9403
|
+
if (run2.settled) return null;
|
|
9404
|
+
return formatExitLine({ ...exit, timedOut: run2.timedOut });
|
|
9144
9405
|
}
|
|
9145
9406
|
var RunTable = class {
|
|
9146
9407
|
constructor(effects, opts = {}) {
|
|
@@ -9163,8 +9424,8 @@ var RunTable = class {
|
|
|
9163
9424
|
has(runId) {
|
|
9164
9425
|
return this.runs.has(runId);
|
|
9165
9426
|
}
|
|
9166
|
-
track(
|
|
9167
|
-
this.runs.set(
|
|
9427
|
+
track(run2) {
|
|
9428
|
+
this.runs.set(run2.runId, run2);
|
|
9168
9429
|
this.effects.persist();
|
|
9169
9430
|
}
|
|
9170
9431
|
/** Call after setting a run's pid so the state file reflects it. */
|
|
@@ -9180,10 +9441,10 @@ var RunTable = class {
|
|
|
9180
9441
|
* already-settled run, a daemon shutdown. Guessing `failed` also errs the
|
|
9181
9442
|
* safe way — it keeps a directory rather than destroying evidence.
|
|
9182
9443
|
*/
|
|
9183
|
-
cleanup(
|
|
9184
|
-
this.runs.delete(
|
|
9444
|
+
cleanup(run2, outcome = "failed") {
|
|
9445
|
+
this.runs.delete(run2.runId);
|
|
9185
9446
|
this.effects.persist();
|
|
9186
|
-
this.effects.release(
|
|
9447
|
+
this.effects.release(run2, { keep: this.keep(outcome, run2), outcome });
|
|
9187
9448
|
}
|
|
9188
9449
|
/**
|
|
9189
9450
|
* Ends a run: flush logs, finish-report, clean up. On a run someone
|
|
@@ -9199,26 +9460,26 @@ var RunTable = class {
|
|
|
9199
9460
|
* `{ judgment: 'rate_limited' }` with its reset time, while transient 5xx
|
|
9200
9461
|
* and transport outages use `interrupted` and the normal short backoff.
|
|
9201
9462
|
*/
|
|
9202
|
-
async finishAndCleanup(
|
|
9203
|
-
if (
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
if (this.keep(status,
|
|
9208
|
-
await
|
|
9209
|
-
const keptBeforeFinish = this.keep(status,
|
|
9463
|
+
async finishAndCleanup(run2, status, error, judgment) {
|
|
9464
|
+
if (run2.settled) return this.cleanup(run2);
|
|
9465
|
+
run2.settled = true;
|
|
9466
|
+
run2.endNote ??= error;
|
|
9467
|
+
run2.drain?.();
|
|
9468
|
+
if (this.keep(status, run2)) this.effects.noteKept?.(run2);
|
|
9469
|
+
await run2.flush?.();
|
|
9470
|
+
const keptBeforeFinish = this.keep(status, run2);
|
|
9210
9471
|
try {
|
|
9211
|
-
await this.effects.finish(
|
|
9212
|
-
this.effects.log(`run ${
|
|
9213
|
-
if (!keptBeforeFinish && this.keep(status,
|
|
9214
|
-
this.effects.log(`run ${
|
|
9472
|
+
await this.effects.finish(run2, status, error, judgment);
|
|
9473
|
+
this.effects.log(`run ${run2.runId} finished: ${status}${error ? ` (${error})` : ""}`);
|
|
9474
|
+
if (!keptBeforeFinish && this.keep(status, run2)) {
|
|
9475
|
+
this.effects.log(`run ${run2.runId} workspace kept for resume at ${run2.workspace}`);
|
|
9215
9476
|
}
|
|
9216
9477
|
} catch (err) {
|
|
9217
9478
|
this.effects.log(
|
|
9218
|
-
`finish report for run ${
|
|
9479
|
+
`finish report for run ${run2.runId} not accepted: ${err instanceof Error ? err.message : String(err)}`
|
|
9219
9480
|
);
|
|
9220
9481
|
}
|
|
9221
|
-
this.cleanup(
|
|
9482
|
+
this.cleanup(run2, status);
|
|
9222
9483
|
}
|
|
9223
9484
|
/**
|
|
9224
9485
|
* Marks a supervisor-settled run (`cancels`): kill, do NOT finish-report.
|
|
@@ -9227,11 +9488,11 @@ var RunTable = class {
|
|
|
9227
9488
|
* is cleaned up by the launch path's next settled check.
|
|
9228
9489
|
*/
|
|
9229
9490
|
markCanceled(runId) {
|
|
9230
|
-
const
|
|
9231
|
-
if (!
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
return
|
|
9491
|
+
const run2 = this.runs.get(runId);
|
|
9492
|
+
if (!run2 || run2.settled) return null;
|
|
9493
|
+
run2.canceled = true;
|
|
9494
|
+
run2.settled = true;
|
|
9495
|
+
return run2;
|
|
9235
9496
|
}
|
|
9236
9497
|
};
|
|
9237
9498
|
var LogBatcher = class {
|
|
@@ -9363,7 +9624,7 @@ function pendingSelfUpdate(cli, running) {
|
|
|
9363
9624
|
return isNewerVersion(cli.version, running) ? cli.version : null;
|
|
9364
9625
|
}
|
|
9365
9626
|
function pathWithin(path2, dir) {
|
|
9366
|
-
const root = dir.endsWith(
|
|
9627
|
+
const root = dir.endsWith(sep2) ? dir : dir + sep2;
|
|
9367
9628
|
return path2.startsWith(root);
|
|
9368
9629
|
}
|
|
9369
9630
|
function buildSpawnEnv(base, opts) {
|
|
@@ -9383,14 +9644,14 @@ function buildSpawnEnv(base, opts) {
|
|
|
9383
9644
|
var PACKAGE = "tines";
|
|
9384
9645
|
var NPM_ARGS = ["--min-release-age=0", "--no-audit", "--no-fund", "--loglevel=error"];
|
|
9385
9646
|
function agentCliPrefix(configDir) {
|
|
9386
|
-
return
|
|
9647
|
+
return join4(configDir, "cli");
|
|
9387
9648
|
}
|
|
9388
9649
|
function binDirOf(prefix) {
|
|
9389
|
-
return
|
|
9650
|
+
return join4(prefix, "node_modules", ".bin");
|
|
9390
9651
|
}
|
|
9391
9652
|
function installedVersion(prefix) {
|
|
9392
9653
|
try {
|
|
9393
|
-
const pkg = readFileSync7(
|
|
9654
|
+
const pkg = readFileSync7(join4(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
|
|
9394
9655
|
const version = JSON.parse(pkg).version;
|
|
9395
9656
|
return typeof version === "string" ? version : null;
|
|
9396
9657
|
} catch {
|
|
@@ -9399,11 +9660,11 @@ function installedVersion(prefix) {
|
|
|
9399
9660
|
}
|
|
9400
9661
|
function lastGood(prefix) {
|
|
9401
9662
|
const binDir = binDirOf(prefix);
|
|
9402
|
-
if (!existsSync4(
|
|
9663
|
+
if (!existsSync4(join4(binDir, PACKAGE))) return AMBIENT_CLI;
|
|
9403
9664
|
return { binDir, version: installedVersion(prefix), source: "stale" };
|
|
9404
9665
|
}
|
|
9405
9666
|
function runNpm(file, args, cwd, timeoutMs) {
|
|
9406
|
-
return new Promise((
|
|
9667
|
+
return new Promise((resolve2) => {
|
|
9407
9668
|
let output = "";
|
|
9408
9669
|
let done = false;
|
|
9409
9670
|
let timedOut = false;
|
|
@@ -9418,7 +9679,7 @@ function runNpm(file, args, cwd, timeoutMs) {
|
|
|
9418
9679
|
if (done) return;
|
|
9419
9680
|
done = true;
|
|
9420
9681
|
clearTimeout(timer);
|
|
9421
|
-
|
|
9682
|
+
resolve2({ ...result, timedOut, output });
|
|
9422
9683
|
};
|
|
9423
9684
|
child.stdout?.on("data", (data) => output += data.toString("utf8"));
|
|
9424
9685
|
child.stderr?.on("data", (data) => output += data.toString("utf8"));
|
|
@@ -9444,7 +9705,7 @@ async function installAgentCli(opts) {
|
|
|
9444
9705
|
const args = ["install", "--prefix", prefix, `${PACKAGE}@latest`, ...NPM_ARGS];
|
|
9445
9706
|
let result = await runNpm("npm", args, prefix, timeoutMs);
|
|
9446
9707
|
if (result.spawnError?.code === "ENOENT") {
|
|
9447
|
-
const sibling =
|
|
9708
|
+
const sibling = join4(dirname3(process.execPath), "npm");
|
|
9448
9709
|
if (existsSync4(sibling)) result = await runNpm(sibling, args, prefix, timeoutMs);
|
|
9449
9710
|
}
|
|
9450
9711
|
if (result.code !== 0) {
|
|
@@ -9466,14 +9727,17 @@ function message(err) {
|
|
|
9466
9727
|
return err instanceof Error ? err.message : String(err);
|
|
9467
9728
|
}
|
|
9468
9729
|
|
|
9730
|
+
// src/daemon/register.ts
|
|
9731
|
+
import { arch, hostname, platform } from "node:os";
|
|
9732
|
+
|
|
9469
9733
|
// src/daemon/store.ts
|
|
9470
9734
|
import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
|
|
9471
|
-
import { join as
|
|
9735
|
+
import { join as join5 } from "node:path";
|
|
9472
9736
|
function credentialsKey(url, name2) {
|
|
9473
9737
|
return `${url.replace(/\/+$/, "")}#${name2}`;
|
|
9474
9738
|
}
|
|
9475
9739
|
function credentialsPath(dir) {
|
|
9476
|
-
return
|
|
9740
|
+
return join5(dir, "runners.json");
|
|
9477
9741
|
}
|
|
9478
9742
|
function loadRunnerCredentials(dir, url, name2) {
|
|
9479
9743
|
const all = readJsonFile(credentialsPath(dir));
|
|
@@ -9496,7 +9760,7 @@ function clearRunnerCredentials(dir, url, name2) {
|
|
|
9496
9760
|
writeJsonFile(path2, all, { secret: true });
|
|
9497
9761
|
}
|
|
9498
9762
|
function daemonStatePath(dir, runnerId) {
|
|
9499
|
-
return
|
|
9763
|
+
return join5(dir, `daemon-state-${runnerId}.json`);
|
|
9500
9764
|
}
|
|
9501
9765
|
function loadDaemonState(path2) {
|
|
9502
9766
|
const state = readJsonFile(path2);
|
|
@@ -9510,8 +9774,8 @@ function saveDaemonState(path2, runs) {
|
|
|
9510
9774
|
}
|
|
9511
9775
|
function processStartTimeMs(pid) {
|
|
9512
9776
|
try {
|
|
9513
|
-
const
|
|
9514
|
-
const afterComm =
|
|
9777
|
+
const stat2 = readFileSync8(`/proc/${pid}/stat`, "utf8");
|
|
9778
|
+
const afterComm = stat2.slice(stat2.lastIndexOf(")") + 2).split(" ");
|
|
9515
9779
|
const startTicks = Number(afterComm[19]);
|
|
9516
9780
|
const btimeLine = readFileSync8("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
|
|
9517
9781
|
const btime = Number(btimeLine?.slice("btime ".length));
|
|
@@ -9522,10 +9786,10 @@ function processStartTimeMs(pid) {
|
|
|
9522
9786
|
}
|
|
9523
9787
|
}
|
|
9524
9788
|
function workspacesDir(configDir) {
|
|
9525
|
-
return
|
|
9789
|
+
return join5(configDir, "workspaces");
|
|
9526
9790
|
}
|
|
9527
9791
|
function keptMarkerPath(workspace) {
|
|
9528
|
-
return
|
|
9792
|
+
return join5(workspace, "kept.json");
|
|
9529
9793
|
}
|
|
9530
9794
|
function writeKeptMarker(workspace, marker) {
|
|
9531
9795
|
writeJsonFile(keptMarkerPath(workspace), marker);
|
|
@@ -9547,7 +9811,7 @@ function listKeptWorkspaces(configDir) {
|
|
|
9547
9811
|
}
|
|
9548
9812
|
const kept = [];
|
|
9549
9813
|
for (const name2 of names) {
|
|
9550
|
-
const path2 =
|
|
9814
|
+
const path2 = join5(root, name2);
|
|
9551
9815
|
const marker = readKeptMarker(path2);
|
|
9552
9816
|
if (marker) kept.push({ ...marker, path: path2 });
|
|
9553
9817
|
}
|
|
@@ -9584,7 +9848,7 @@ function directorySizeBytes(path2) {
|
|
|
9584
9848
|
return 0;
|
|
9585
9849
|
}
|
|
9586
9850
|
for (const entry of entries) {
|
|
9587
|
-
const child =
|
|
9851
|
+
const child = join5(path2, entry.name);
|
|
9588
9852
|
if (entry.isDirectory()) total += directorySizeBytes(child);
|
|
9589
9853
|
else if (entry.isFile()) {
|
|
9590
9854
|
try {
|
|
@@ -9596,15 +9860,48 @@ function directorySizeBytes(path2) {
|
|
|
9596
9860
|
return total;
|
|
9597
9861
|
}
|
|
9598
9862
|
|
|
9863
|
+
// src/daemon/register.ts
|
|
9864
|
+
function missingApiKeyMessage(name2, baseUrl) {
|
|
9865
|
+
return `no stored runner token for "${name2}" at ${baseUrl} \u2014 set TINES_API_KEY (a user API key) to register`;
|
|
9866
|
+
}
|
|
9867
|
+
async function ensureRunnerCredentials(opts) {
|
|
9868
|
+
const stored = loadRunnerCredentials(opts.configDir, opts.baseUrl, opts.name);
|
|
9869
|
+
if (stored) {
|
|
9870
|
+
opts.log(
|
|
9871
|
+
`reconnecting as runner "${opts.name}" (${stored.runner_id}) \u2014 token from ${opts.configDir}`
|
|
9872
|
+
);
|
|
9873
|
+
return { creds: stored, registered: false };
|
|
9874
|
+
}
|
|
9875
|
+
if (!opts.apiKey) throw new Error(missingApiKeyMessage(opts.name, opts.baseUrl));
|
|
9876
|
+
const userClient = createApiClient({ baseUrl: opts.baseUrl, apiKey: opts.apiKey });
|
|
9877
|
+
const registered = await userClient.registerRunner({
|
|
9878
|
+
name: opts.name,
|
|
9879
|
+
harness: opts.harness,
|
|
9880
|
+
...opts.command !== void 0 ? { command: opts.command } : {},
|
|
9881
|
+
max_concurrent: opts.maxConcurrent,
|
|
9882
|
+
hostname: hostname(),
|
|
9883
|
+
platform: `${platform()} ${arch()}`
|
|
9884
|
+
});
|
|
9885
|
+
const creds = { runner_id: registered.runner.id, token: registered.runner_token };
|
|
9886
|
+
saveRunnerCredentials(opts.configDir, opts.baseUrl, opts.name, creds);
|
|
9887
|
+
opts.log(
|
|
9888
|
+
`registered runner "${opts.name}" (${creds.runner_id}); token stored in ${opts.configDir}`
|
|
9889
|
+
);
|
|
9890
|
+
return { creds, registered: true };
|
|
9891
|
+
}
|
|
9892
|
+
function nextStepsMessage(name2, baseUrl) {
|
|
9893
|
+
return `next: route work to "${name2}" \u2014 ${baseUrl}/agents (or: tines routing set ${name2}). Eligible work starts when this runner is available and routing matches. If automation is stopped, resume it in Agents or with tines supervisor enable.`;
|
|
9894
|
+
}
|
|
9895
|
+
|
|
9599
9896
|
// src/daemon/daemon.ts
|
|
9600
9897
|
var CLI_REFRESH_TTL_MS = 10 * 6e4;
|
|
9601
9898
|
var DAEMON_VERSION = cliVersion();
|
|
9602
|
-
async function uploadRawLog(
|
|
9603
|
-
const path2 =
|
|
9604
|
-
if (!path2 || !
|
|
9605
|
-
|
|
9899
|
+
async function uploadRawLog(run2) {
|
|
9900
|
+
const path2 = run2.rawSpoolPath;
|
|
9901
|
+
if (!path2 || !run2.rawUpload) return;
|
|
9902
|
+
run2.rawSpoolPath = void 0;
|
|
9606
9903
|
try {
|
|
9607
|
-
await new Promise((
|
|
9904
|
+
await new Promise((resolve2) => run2.rawSpool?.end(resolve2) ?? resolve2());
|
|
9608
9905
|
const size = statSync4(path2).size;
|
|
9609
9906
|
if (size > 0) {
|
|
9610
9907
|
let body = readFileSync9(path2);
|
|
@@ -9618,7 +9915,7 @@ async function uploadRawLog(run) {
|
|
|
9618
9915
|
body.subarray(body.byteLength - RUN_LOG_RAW_MAX_BYTES + marker.byteLength)
|
|
9619
9916
|
]);
|
|
9620
9917
|
}
|
|
9621
|
-
await
|
|
9918
|
+
await run2.rawUpload(body);
|
|
9622
9919
|
}
|
|
9623
9920
|
} catch {
|
|
9624
9921
|
} finally {
|
|
@@ -9629,7 +9926,7 @@ async function uploadRawLog(run) {
|
|
|
9629
9926
|
}
|
|
9630
9927
|
}
|
|
9631
9928
|
var log = (message3) => console.log(`[${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)}] ${message3}`);
|
|
9632
|
-
var sleep = (ms) => new Promise((
|
|
9929
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
9633
9930
|
function killTree(pid, signal) {
|
|
9634
9931
|
try {
|
|
9635
9932
|
process.kill(-pid, signal);
|
|
@@ -9680,31 +9977,21 @@ async function runDaemon(opts) {
|
|
|
9680
9977
|
);
|
|
9681
9978
|
}
|
|
9682
9979
|
};
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
name: opts.name,
|
|
9697
|
-
harness: opts.harness,
|
|
9698
|
-
...opts.command !== void 0 ? { command: opts.command } : {},
|
|
9699
|
-
max_concurrent: opts.maxConcurrent,
|
|
9700
|
-
hostname: hostname(),
|
|
9701
|
-
platform: `${platform()} ${arch()}`
|
|
9702
|
-
});
|
|
9703
|
-
creds = { runner_id: registered.runner.id, token: registered.runner_token };
|
|
9704
|
-
saveRunnerCredentials(opts.configDir, baseUrl, opts.name, creds);
|
|
9705
|
-
log(`registered runner "${opts.name}" (${creds.runner_id}); token stored in ${opts.configDir}`);
|
|
9980
|
+
const ensured = await ensureRunnerCredentials({
|
|
9981
|
+
configDir: opts.configDir,
|
|
9982
|
+
baseUrl,
|
|
9983
|
+
name: opts.name,
|
|
9984
|
+
harness: opts.harness,
|
|
9985
|
+
...opts.command !== void 0 ? { command: opts.command } : {},
|
|
9986
|
+
maxConcurrent: opts.maxConcurrent,
|
|
9987
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
9988
|
+
log
|
|
9989
|
+
});
|
|
9990
|
+
const creds = ensured.creds;
|
|
9991
|
+
if (ensured.registered) {
|
|
9992
|
+
log(nextStepsMessage(opts.name, baseUrl));
|
|
9706
9993
|
log(
|
|
9707
|
-
`
|
|
9994
|
+
`keep it running: \`tines runner install --name ${opts.name} --harness ${opts.harness.replaceAll("_", "-")}\` installs this runner as a launchd/systemd service that keeps itself updated (docs/runner-daemon.md)`
|
|
9708
9995
|
);
|
|
9709
9996
|
}
|
|
9710
9997
|
const client2 = createApiClient({ baseUrl, apiKey: creds.token });
|
|
@@ -9719,7 +10006,7 @@ async function runDaemon(opts) {
|
|
|
9719
10006
|
const selfUpdate = opts.selfUpdate && opts.cliRefresh && managedInstall;
|
|
9720
10007
|
if (opts.selfUpdate && opts.cliRefresh) {
|
|
9721
10008
|
log(
|
|
9722
|
-
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 ${
|
|
10009
|
+
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 ${join6(prefix, "node_modules", ".bin", "tines")} to have refreshes apply on restart \u2014 \`tines runner install --name ${opts.name} --harness ${opts.harness.replaceAll("_", "-")}\` sets that up as a service (docs/runner-daemon.md)`
|
|
9723
10010
|
);
|
|
9724
10011
|
}
|
|
9725
10012
|
let pendingUpdate = null;
|
|
@@ -9739,18 +10026,56 @@ async function runDaemon(opts) {
|
|
|
9739
10026
|
log(`agent CLI: ${cliLabel(await ensureCli())}`);
|
|
9740
10027
|
const table2 = new RunTable(
|
|
9741
10028
|
{
|
|
9742
|
-
finish: async (
|
|
9743
|
-
const summary =
|
|
9744
|
-
|
|
10029
|
+
finish: async (run2, status, error, judgment) => {
|
|
10030
|
+
const summary = run2.renderer?.summary();
|
|
10031
|
+
if (summary?.pricingEvidence && opts.harness === "codex") {
|
|
10032
|
+
const raw = summary.pricingEvidence.raw_usage;
|
|
10033
|
+
const completeRaw = raw && [
|
|
10034
|
+
"input_tokens",
|
|
10035
|
+
"cached_input_tokens",
|
|
10036
|
+
"cache_write_input_tokens",
|
|
10037
|
+
"output_tokens"
|
|
10038
|
+
].every((field) => raw[field] !== void 0);
|
|
10039
|
+
if (status === "completed" && run2.pricingSessionMode === "cold" && summary.pricingEvidence.measurement_status === "complete" && completeRaw && summary.providerSessionId && run2.codexHome && run2.spawnedAt) {
|
|
10040
|
+
summary.pricingEvidence.request_context = await collectCodexRequestContext({
|
|
10041
|
+
codexHome: run2.codexHome,
|
|
10042
|
+
threadId: summary.providerSessionId,
|
|
10043
|
+
model: run2.pricingModel ?? null,
|
|
10044
|
+
startedAt: run2.spawnedAt,
|
|
10045
|
+
endedAt: Date.now(),
|
|
10046
|
+
terminalUsage: raw
|
|
10047
|
+
});
|
|
10048
|
+
if ("reason" in summary.pricingEvidence.request_context && summary.pricingEvidence.request_context.reason === "model_mismatch")
|
|
10049
|
+
summary.pricingEvidence.model_rerouted = true;
|
|
10050
|
+
} else {
|
|
10051
|
+
summary.pricingEvidence.request_context = {
|
|
10052
|
+
version: 1,
|
|
10053
|
+
normalization: "codex-rollout-delta-v1",
|
|
10054
|
+
status: "unavailable",
|
|
10055
|
+
reason: "not_applicable"
|
|
10056
|
+
};
|
|
10057
|
+
}
|
|
10058
|
+
}
|
|
10059
|
+
const accounting = {
|
|
10060
|
+
usage: summary?.usage ?? { cost_source: "none" },
|
|
10061
|
+
...summary?.pricingEvidence ? { pricing_evidence: summary.pricingEvidence } : {},
|
|
10062
|
+
...summary?.providerSessionId ? { provider_session_id: summary.providerSessionId } : {},
|
|
10063
|
+
daemon_version: DAEMON_VERSION,
|
|
10064
|
+
status
|
|
10065
|
+
};
|
|
10066
|
+
run2.batcher.append(`[usage] ${JSON.stringify(accounting)}
|
|
10067
|
+
`);
|
|
10068
|
+
await run2.batcher.flush();
|
|
10069
|
+
const ended = await client2.finishRun(run2.runId, {
|
|
9745
10070
|
status,
|
|
9746
10071
|
...error ? { error } : {},
|
|
9747
10072
|
...judgment,
|
|
9748
|
-
usage:
|
|
10073
|
+
usage: accounting.usage,
|
|
9749
10074
|
...summary?.pricingEvidence ? {
|
|
9750
10075
|
pricing_evidence: {
|
|
9751
10076
|
...summary.pricingEvidence,
|
|
9752
|
-
model:
|
|
9753
|
-
session_mode:
|
|
10077
|
+
model: run2.pricingModel ?? null,
|
|
10078
|
+
session_mode: run2.pricingSessionMode ?? "cold",
|
|
9754
10079
|
daemon_version: DAEMON_VERSION
|
|
9755
10080
|
}
|
|
9756
10081
|
} : {},
|
|
@@ -9758,33 +10083,33 @@ async function runDaemon(opts) {
|
|
|
9758
10083
|
...summary?.numTurns !== void 0 ? { turn_count: summary.numTurns } : {},
|
|
9759
10084
|
// The whole conversation's turns, which for a resumed run is
|
|
9760
10085
|
// more than this run's own: the size guard reads this one.
|
|
9761
|
-
...summary?.numTurns !== void 0 ? { conversation_turn_count:
|
|
9762
|
-
workspace_path:
|
|
10086
|
+
...summary?.numTurns !== void 0 ? { conversation_turn_count: run2.priorTurns + summary.numTurns } : {},
|
|
10087
|
+
workspace_path: run2.workspace
|
|
9763
10088
|
});
|
|
9764
|
-
if (ended?.resume_expires_at)
|
|
10089
|
+
if (ended?.resume_expires_at) run2.keepForResume = true;
|
|
9765
10090
|
},
|
|
9766
|
-
release: (
|
|
9767
|
-
if (
|
|
9768
|
-
|
|
9769
|
-
settleWorkspace(
|
|
9770
|
-
run_id:
|
|
9771
|
-
...
|
|
10091
|
+
release: (run2, { keep, outcome }) => {
|
|
10092
|
+
if (run2.timeout) clearTimeout(run2.timeout);
|
|
10093
|
+
run2.renderer?.finish();
|
|
10094
|
+
settleWorkspace(run2.workspace, keep, {
|
|
10095
|
+
run_id: run2.runId,
|
|
10096
|
+
...run2.issueLabel ? { issue_ref: run2.issueLabel } : {},
|
|
9772
10097
|
status: outcome,
|
|
9773
|
-
...
|
|
10098
|
+
...run2.endNote ? { error: run2.endNote } : {}
|
|
9774
10099
|
});
|
|
9775
|
-
void uploadRawLog(
|
|
10100
|
+
void uploadRawLog(run2);
|
|
9776
10101
|
sweepKeptWorkspaces();
|
|
9777
10102
|
},
|
|
9778
|
-
noteKept: (
|
|
10103
|
+
noteKept: (run2) => run2.batcher.append(`workspace kept at ${run2.workspace}
|
|
9779
10104
|
`),
|
|
9780
10105
|
persist: () => {
|
|
9781
|
-
const entries = table2.values().filter((
|
|
9782
|
-
run_id:
|
|
9783
|
-
pid:
|
|
9784
|
-
workspace:
|
|
9785
|
-
key_fingerprint:
|
|
9786
|
-
started_at:
|
|
9787
|
-
...
|
|
10106
|
+
const entries = table2.values().filter((run2) => run2.child?.pid !== void 0).map((run2) => ({
|
|
10107
|
+
run_id: run2.runId,
|
|
10108
|
+
pid: run2.child.pid,
|
|
10109
|
+
workspace: run2.workspace,
|
|
10110
|
+
key_fingerprint: run2.keyFingerprint,
|
|
10111
|
+
started_at: run2.spawnedAt,
|
|
10112
|
+
...run2.issueLabel ? { issue_ref: run2.issueLabel } : {}
|
|
9788
10113
|
}));
|
|
9789
10114
|
saveDaemonState(statePath, entries);
|
|
9790
10115
|
},
|
|
@@ -9793,7 +10118,7 @@ async function runDaemon(opts) {
|
|
|
9793
10118
|
{
|
|
9794
10119
|
// A workspace the server retained for a resume is kept whatever the
|
|
9795
10120
|
// --keep-workspaces mode says; pruning still bounds it by age/count.
|
|
9796
|
-
keep: (outcome,
|
|
10121
|
+
keep: (outcome, run2) => run2.keepForResume || keepWorkspace(opts.keepWorkspaces, outcome)
|
|
9797
10122
|
}
|
|
9798
10123
|
);
|
|
9799
10124
|
for (const orphan of loadDaemonState(statePath)) {
|
|
@@ -9827,12 +10152,12 @@ async function runDaemon(opts) {
|
|
|
9827
10152
|
}
|
|
9828
10153
|
saveDaemonState(statePath, []);
|
|
9829
10154
|
const killWithoutFinish = (runId) => {
|
|
9830
|
-
const
|
|
9831
|
-
if (!
|
|
9832
|
-
|
|
10155
|
+
const run2 = table2.markCanceled(runId);
|
|
10156
|
+
if (!run2) return;
|
|
10157
|
+
run2.endNote ??= "canceled by supervisor";
|
|
9833
10158
|
log(`supervisor canceled run ${runId}; killing without finish-reporting`);
|
|
9834
|
-
if (
|
|
9835
|
-
const pid =
|
|
10159
|
+
if (run2.child?.pid) {
|
|
10160
|
+
const pid = run2.child.pid;
|
|
9836
10161
|
killTree(pid, "SIGTERM");
|
|
9837
10162
|
setTimeout(() => killTree(pid, "SIGKILL"), 5e3).unref?.();
|
|
9838
10163
|
}
|
|
@@ -9846,9 +10171,9 @@ async function runDaemon(opts) {
|
|
|
9846
10171
|
`run ${runId}: resume workspace ${assignment.resume.workspace_path} is gone; launching fresh`
|
|
9847
10172
|
);
|
|
9848
10173
|
}
|
|
9849
|
-
const workspace = resume ? resume.workspace_path :
|
|
10174
|
+
const workspace = resume ? resume.workspace_path : join6(workspacesDir(opts.configDir), runId);
|
|
9850
10175
|
const issueLabel = assignment.run.issue_ref ? `${assignment.run.issue_ref.project_name}/${assignment.run.issue_ref.number}` : void 0;
|
|
9851
|
-
const
|
|
10176
|
+
const run2 = {
|
|
9852
10177
|
runId,
|
|
9853
10178
|
workspace,
|
|
9854
10179
|
...issueLabel ? { issueLabel } : {},
|
|
@@ -9864,8 +10189,8 @@ async function runDaemon(opts) {
|
|
|
9864
10189
|
{ onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`) }
|
|
9865
10190
|
)
|
|
9866
10191
|
};
|
|
9867
|
-
|
|
9868
|
-
table2.track(
|
|
10192
|
+
run2.flush = () => run2.batcher.flush();
|
|
10193
|
+
table2.track(run2);
|
|
9869
10194
|
log(
|
|
9870
10195
|
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`
|
|
9871
10196
|
);
|
|
@@ -9874,47 +10199,47 @@ async function runDaemon(opts) {
|
|
|
9874
10199
|
rmSync3(workspace, { recursive: true, force: true });
|
|
9875
10200
|
mkdirSync4(workspace, { recursive: true });
|
|
9876
10201
|
} else {
|
|
9877
|
-
rmSync3(
|
|
10202
|
+
rmSync3(join6(workspace, "kept.json"), { force: true });
|
|
9878
10203
|
}
|
|
9879
|
-
writeFileSync3(
|
|
10204
|
+
writeFileSync3(join6(workspace, "prompt.md"), `${assignment.prompt}
|
|
9880
10205
|
`);
|
|
9881
10206
|
if (!resume)
|
|
9882
10207
|
for (const skill of assignment.bundle.skills) {
|
|
9883
10208
|
for (const file of skill.files) {
|
|
9884
|
-
const target =
|
|
10209
|
+
const target = join6(workspace, "skills", skill.name, file.path);
|
|
9885
10210
|
mkdirSync4(dirname4(target), { recursive: true });
|
|
9886
10211
|
writeFileSync3(target, file.content);
|
|
9887
10212
|
}
|
|
9888
10213
|
}
|
|
9889
10214
|
if (!resume)
|
|
9890
10215
|
writeFileSync3(
|
|
9891
|
-
|
|
10216
|
+
join6(workspace, "repos.json"),
|
|
9892
10217
|
`${JSON.stringify(assignment.bundle.repos, null, 2)}
|
|
9893
10218
|
`
|
|
9894
10219
|
);
|
|
9895
10220
|
for (const repo of resume ? [] : assignment.bundle.repos) {
|
|
9896
|
-
if (
|
|
10221
|
+
if (run2.settled) return table2.cleanup(run2);
|
|
9897
10222
|
const args = [
|
|
9898
10223
|
"clone",
|
|
9899
10224
|
...repo.branch ? ["--branch", repo.branch] : [],
|
|
9900
10225
|
repo.url,
|
|
9901
10226
|
repo.dir
|
|
9902
10227
|
];
|
|
9903
|
-
|
|
10228
|
+
run2.batcher.append(`$ git ${args.join(" ")}
|
|
9904
10229
|
`);
|
|
9905
|
-
const result = await runGit(args, workspace,
|
|
10230
|
+
const result = await runGit(args, workspace, run2.batcher);
|
|
9906
10231
|
if (result !== 0) {
|
|
9907
10232
|
return table2.finishAndCleanup(
|
|
9908
|
-
|
|
10233
|
+
run2,
|
|
9909
10234
|
"failed",
|
|
9910
10235
|
`git clone failed for ${repo.url} (exit ${result})`
|
|
9911
10236
|
);
|
|
9912
10237
|
}
|
|
9913
10238
|
}
|
|
9914
|
-
if (
|
|
10239
|
+
if (run2.settled) return table2.cleanup(run2);
|
|
9915
10240
|
const cli = await ensureCli();
|
|
9916
|
-
if (
|
|
9917
|
-
|
|
10241
|
+
if (run2.settled) return table2.cleanup(run2);
|
|
10242
|
+
run2.batcher.append(
|
|
9918
10243
|
cli.source === "fresh" ? `tines CLI: ${cli.version ?? "unknown"} (daemon-managed)
|
|
9919
10244
|
` : cli.source === "stale" ? `warning: agent CLI refresh failed; using last-good tines ${cli.version ?? "unknown"} from ${opts.configDir}/cli
|
|
9920
10245
|
` : `warning: no daemon-managed tines CLI; using whatever \`tines\` is on this machine's PATH
|
|
@@ -9922,7 +10247,7 @@ async function runDaemon(opts) {
|
|
|
9922
10247
|
);
|
|
9923
10248
|
const harnessInput = {
|
|
9924
10249
|
workspace,
|
|
9925
|
-
promptFile:
|
|
10250
|
+
promptFile: join6(workspace, "prompt.md"),
|
|
9926
10251
|
prompt: assignment.prompt,
|
|
9927
10252
|
model: assignment.run.model,
|
|
9928
10253
|
...resume ? { resumeSessionId: resume.provider_session_id } : {}
|
|
@@ -9932,10 +10257,10 @@ async function runDaemon(opts) {
|
|
|
9932
10257
|
harnessInput
|
|
9933
10258
|
);
|
|
9934
10259
|
if (opts.harness === "codex") {
|
|
9935
|
-
|
|
9936
|
-
|
|
10260
|
+
run2.pricingModel = harnessInput.model;
|
|
10261
|
+
run2.pricingSessionMode = resume ? "resumed" : "cold";
|
|
9937
10262
|
}
|
|
9938
|
-
|
|
10263
|
+
run2.batcher.append(
|
|
9939
10264
|
formatLaunchBanner(invocation, harnessInput, {
|
|
9940
10265
|
harness: opts.harness,
|
|
9941
10266
|
timeoutMinutes: assignment.timeout_minutes,
|
|
@@ -9943,96 +10268,98 @@ async function runDaemon(opts) {
|
|
|
9943
10268
|
...resume ? { resumedFromRunId: resume.previous_run_id } : {}
|
|
9944
10269
|
})
|
|
9945
10270
|
);
|
|
10271
|
+
const spawnEnv = buildSpawnEnv(process.env, {
|
|
10272
|
+
binDir: cli.binDir,
|
|
10273
|
+
apiKey: assignment.run_key,
|
|
10274
|
+
apiUrl: baseUrl
|
|
10275
|
+
});
|
|
10276
|
+
if (opts.harness === "codex") run2.codexHome = resolveCodexHome(spawnEnv, workspace);
|
|
9946
10277
|
const child = spawn2(invocation.file, invocation.args, {
|
|
9947
10278
|
cwd: workspace,
|
|
9948
|
-
env:
|
|
9949
|
-
binDir: cli.binDir,
|
|
9950
|
-
apiKey: assignment.run_key,
|
|
9951
|
-
apiUrl: baseUrl
|
|
9952
|
-
}),
|
|
10279
|
+
env: spawnEnv,
|
|
9953
10280
|
stdio: ["ignore", "pipe", "pipe"],
|
|
9954
10281
|
detached: true
|
|
9955
10282
|
});
|
|
9956
|
-
|
|
9957
|
-
|
|
10283
|
+
run2.child = child;
|
|
10284
|
+
run2.spawnedAt = Date.now();
|
|
9958
10285
|
table2.persist();
|
|
9959
10286
|
log(`run ${runId}: launched ${invocation.file} (pid ${child.pid})`);
|
|
9960
10287
|
if (opts.harness === "claude_code") {
|
|
9961
10288
|
const limiter = new RateLimitDetector();
|
|
9962
|
-
|
|
10289
|
+
run2.limiter = limiter;
|
|
9963
10290
|
const renderer = new ClaudeStreamRenderer(
|
|
9964
|
-
(line) =>
|
|
10291
|
+
(line) => run2.batcher.append(line),
|
|
9965
10292
|
(event) => limiter.noteStreamEvent(event)
|
|
9966
10293
|
);
|
|
9967
|
-
|
|
9968
|
-
const spoolPath =
|
|
10294
|
+
run2.renderer = renderer;
|
|
10295
|
+
const spoolPath = join6(opts.configDir, "rawlogs", `${runId}.ndjson`);
|
|
9969
10296
|
mkdirSync4(dirname4(spoolPath), { recursive: true });
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
const decoder = new
|
|
9974
|
-
|
|
10297
|
+
run2.rawSpoolPath = spoolPath;
|
|
10298
|
+
run2.rawSpool = createWriteStream(spoolPath);
|
|
10299
|
+
run2.rawUpload = (body) => client2.putRunLogRaw(runId, body);
|
|
10300
|
+
const decoder = new StringDecoder2("utf8");
|
|
10301
|
+
run2.drain = () => {
|
|
9975
10302
|
const trailing = decoder.end();
|
|
9976
10303
|
if (trailing) renderer.write(trailing);
|
|
9977
10304
|
renderer.finish();
|
|
9978
10305
|
};
|
|
9979
10306
|
child.stdout?.on("data", (data) => {
|
|
9980
|
-
|
|
10307
|
+
run2.rawSpool?.write(data);
|
|
9981
10308
|
renderer.write(decoder.write(data));
|
|
9982
10309
|
});
|
|
9983
10310
|
} else if (opts.harness === "codex") {
|
|
9984
|
-
const renderer = new CodexStreamRenderer((line) =>
|
|
9985
|
-
const decoder = new
|
|
9986
|
-
|
|
9987
|
-
|
|
10311
|
+
const renderer = new CodexStreamRenderer((line) => run2.batcher.append(line));
|
|
10312
|
+
const decoder = new StringDecoder2("utf8");
|
|
10313
|
+
run2.renderer = renderer;
|
|
10314
|
+
run2.drain = () => {
|
|
9988
10315
|
const trailing = decoder.end();
|
|
9989
10316
|
if (trailing) renderer.write(trailing);
|
|
9990
10317
|
renderer.finish();
|
|
9991
10318
|
};
|
|
9992
10319
|
child.stdout?.on("data", (data) => renderer.write(decoder.write(data)));
|
|
9993
10320
|
} else {
|
|
9994
|
-
child.stdout?.on("data", (data) =>
|
|
10321
|
+
child.stdout?.on("data", (data) => run2.batcher.append(data.toString("utf8")));
|
|
9995
10322
|
}
|
|
9996
10323
|
child.stderr?.on("data", (data) => {
|
|
9997
10324
|
const text4 = data.toString("utf8");
|
|
9998
|
-
|
|
9999
|
-
|
|
10325
|
+
run2.batcher.append(text4);
|
|
10326
|
+
run2.limiter?.noteStderr(text4);
|
|
10000
10327
|
});
|
|
10001
|
-
|
|
10002
|
-
if (
|
|
10328
|
+
run2.timeout = setTimeout(() => {
|
|
10329
|
+
if (run2.settled) return;
|
|
10003
10330
|
log(`run ${runId} hit its ${assignment.timeout_minutes}m timeout; killing`);
|
|
10004
|
-
|
|
10331
|
+
run2.timedOut = true;
|
|
10005
10332
|
if (child.pid) killTree(child.pid, "SIGTERM");
|
|
10006
10333
|
if (child.pid) setTimeout(() => killTree(child.pid, "SIGKILL"), 5e3).unref?.();
|
|
10007
10334
|
}, assignment.timeout_minutes * 6e4);
|
|
10008
10335
|
child.on("error", (err) => {
|
|
10009
|
-
void table2.finishAndCleanup(
|
|
10336
|
+
void table2.finishAndCleanup(run2, "failed", `failed to launch harness: ${message2(err)}`);
|
|
10010
10337
|
});
|
|
10011
10338
|
child.on("close", (code, signal) => {
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
const closing = exitLineForRun(
|
|
10339
|
+
run2.drain?.();
|
|
10340
|
+
run2.limiter?.finish();
|
|
10341
|
+
const closing = exitLineForRun(run2, {
|
|
10015
10342
|
code,
|
|
10016
10343
|
signal,
|
|
10017
|
-
durationMs: Date.now() - (
|
|
10344
|
+
durationMs: Date.now() - (run2.spawnedAt ?? Date.now())
|
|
10018
10345
|
});
|
|
10019
|
-
if (closing)
|
|
10020
|
-
if (
|
|
10346
|
+
if (closing) run2.batcher.append(closing);
|
|
10347
|
+
if (run2.timedOut) {
|
|
10021
10348
|
void table2.finishAndCleanup(
|
|
10022
|
-
|
|
10349
|
+
run2,
|
|
10023
10350
|
"failed",
|
|
10024
10351
|
`run exceeded the ${assignment.timeout_minutes}m timeout; harness killed`
|
|
10025
10352
|
);
|
|
10026
10353
|
} else if (code === 0) {
|
|
10027
|
-
void table2.finishAndCleanup(
|
|
10354
|
+
void table2.finishAndCleanup(run2, "completed");
|
|
10028
10355
|
} else {
|
|
10029
|
-
const limited = signal ? null :
|
|
10030
|
-
const providerError = signal ? null :
|
|
10356
|
+
const limited = signal ? null : run2.limiter?.signal() ?? null;
|
|
10357
|
+
const providerError = signal ? null : run2.limiter?.providerError() ?? null;
|
|
10031
10358
|
if (limited) {
|
|
10032
10359
|
log(
|
|
10033
10360
|
`run ${runId}: harness rate limited (${limited.detail}); reporting without a strike`
|
|
10034
10361
|
);
|
|
10035
|
-
void table2.finishAndCleanup(
|
|
10362
|
+
void table2.finishAndCleanup(run2, "failed", `rate limited: ${limited.detail}`, {
|
|
10036
10363
|
judgment: "rate_limited",
|
|
10037
10364
|
...limited.resumeAt !== null ? { resume_at: limited.resumeAt } : {}
|
|
10038
10365
|
});
|
|
@@ -10040,12 +10367,12 @@ async function runDaemon(opts) {
|
|
|
10040
10367
|
log(
|
|
10041
10368
|
`run ${runId}: transient provider error (${providerError.detail}); reporting without a strike`
|
|
10042
10369
|
);
|
|
10043
|
-
void table2.finishAndCleanup(
|
|
10370
|
+
void table2.finishAndCleanup(run2, "failed", `provider error: ${providerError.detail}`, {
|
|
10044
10371
|
judgment: "interrupted"
|
|
10045
10372
|
});
|
|
10046
10373
|
} else {
|
|
10047
10374
|
void table2.finishAndCleanup(
|
|
10048
|
-
|
|
10375
|
+
run2,
|
|
10049
10376
|
"failed",
|
|
10050
10377
|
signal ? `harness killed by ${signal}` : `harness exited with code ${code}`
|
|
10051
10378
|
);
|
|
@@ -10053,7 +10380,7 @@ async function runDaemon(opts) {
|
|
|
10053
10380
|
}
|
|
10054
10381
|
});
|
|
10055
10382
|
} catch (err) {
|
|
10056
|
-
void table2.finishAndCleanup(
|
|
10383
|
+
void table2.finishAndCleanup(run2, "failed", `workspace setup failed: ${message2(err)}`);
|
|
10057
10384
|
}
|
|
10058
10385
|
};
|
|
10059
10386
|
const shutdown = async () => {
|
|
@@ -10061,9 +10388,9 @@ async function runDaemon(opts) {
|
|
|
10061
10388
|
shuttingDown = true;
|
|
10062
10389
|
log("shutting down; failing in-flight runs");
|
|
10063
10390
|
await Promise.all(
|
|
10064
|
-
table2.values().map(async (
|
|
10065
|
-
if (
|
|
10066
|
-
await table2.finishAndCleanup(
|
|
10391
|
+
table2.values().map(async (run2) => {
|
|
10392
|
+
if (run2.child?.pid) killTree(run2.child.pid, "SIGTERM");
|
|
10393
|
+
await table2.finishAndCleanup(run2, "failed", "daemon shut down", {
|
|
10067
10394
|
judgment: "interrupted"
|
|
10068
10395
|
});
|
|
10069
10396
|
})
|
|
@@ -10114,11 +10441,11 @@ async function runDaemon(opts) {
|
|
|
10114
10441
|
return;
|
|
10115
10442
|
}
|
|
10116
10443
|
if (err instanceof ApiError && err.status === 401) {
|
|
10117
|
-
for (const
|
|
10118
|
-
if (
|
|
10119
|
-
settleWorkspace(
|
|
10120
|
-
run_id:
|
|
10121
|
-
...
|
|
10444
|
+
for (const run2 of table2.values()) {
|
|
10445
|
+
if (run2.child?.pid) killTree(run2.child.pid, "SIGKILL");
|
|
10446
|
+
settleWorkspace(run2.workspace, keepWorkspace(opts.keepWorkspaces, "failed"), {
|
|
10447
|
+
run_id: run2.runId,
|
|
10448
|
+
...run2.issueLabel ? { issue_ref: run2.issueLabel } : {},
|
|
10122
10449
|
status: "failed",
|
|
10123
10450
|
error: "daemon token rejected"
|
|
10124
10451
|
});
|
|
@@ -10151,12 +10478,486 @@ function realpathOrNull(path2) {
|
|
|
10151
10478
|
}
|
|
10152
10479
|
}
|
|
10153
10480
|
function runGit(args, cwd, batcher) {
|
|
10154
|
-
return new Promise((
|
|
10481
|
+
return new Promise((resolve2) => {
|
|
10155
10482
|
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
10156
10483
|
child.stdout?.on("data", (data) => batcher.append(data.toString("utf8")));
|
|
10157
10484
|
child.stderr?.on("data", (data) => batcher.append(data.toString("utf8")));
|
|
10158
|
-
child.on("error", () =>
|
|
10159
|
-
child.on("exit", (code) =>
|
|
10485
|
+
child.on("error", () => resolve2(127));
|
|
10486
|
+
child.on("exit", (code) => resolve2(code ?? 1));
|
|
10487
|
+
});
|
|
10488
|
+
}
|
|
10489
|
+
|
|
10490
|
+
// src/commands/daemon-flags.ts
|
|
10491
|
+
import { hostname as hostname2 } from "node:os";
|
|
10492
|
+
function withDaemonFlags(cmd) {
|
|
10493
|
+
return cmd.option(
|
|
10494
|
+
"--name <name>",
|
|
10495
|
+
"runner name, unique per user; name it machine-plus-harness, e.g. macbook-claude (default: this hostname)"
|
|
10496
|
+
).option("--harness <harness>", "claude-code | codex | custom", "claude-code").option(
|
|
10497
|
+
"--command <template>",
|
|
10498
|
+
"custom harness command template ({prompt_file}, {workspace}, {model})"
|
|
10499
|
+
).option("--max-concurrent <n>", "maximum simultaneous runs", (v) => Number.parseInt(v, 10), 1).option("--poll-interval <seconds>", "seconds between polls", (v) => Number.parseInt(v, 10), 15).option(
|
|
10500
|
+
"--keep-workspaces <mode>",
|
|
10501
|
+
"keep settled runs' workspaces for debugging: never | failed | always",
|
|
10502
|
+
"never"
|
|
10503
|
+
).option(
|
|
10504
|
+
"--keep-workspaces-for <hours>",
|
|
10505
|
+
"delete kept workspaces older than this",
|
|
10506
|
+
(v) => Number(v),
|
|
10507
|
+
72
|
|
10508
|
+
).option(
|
|
10509
|
+
"--keep-workspaces-max <n>",
|
|
10510
|
+
"keep at most this many workspaces (oldest removed first)",
|
|
10511
|
+
(v) => Number.parseInt(v, 10),
|
|
10512
|
+
20
|
|
10513
|
+
);
|
|
10514
|
+
}
|
|
10515
|
+
function parseDaemonFlags(opts) {
|
|
10516
|
+
const harness = opts.harness.replaceAll("-", "_");
|
|
10517
|
+
if (!HARNESS_KINDS.includes(harness)) {
|
|
10518
|
+
die(`--harness must be claude-code, codex, or custom, got "${opts.harness}"`);
|
|
10519
|
+
}
|
|
10520
|
+
if (harness === "custom" && !opts.command) {
|
|
10521
|
+
die('the custom harness needs --command "<template>" ({prompt_file}, {workspace}, {model})');
|
|
10522
|
+
}
|
|
10523
|
+
if (harness !== "custom" && opts.command) die("--command only applies to --harness custom");
|
|
10524
|
+
if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent < 1 || opts.maxConcurrent > 100) {
|
|
10525
|
+
die("--max-concurrent must be an integer between 1 and 100");
|
|
10526
|
+
}
|
|
10527
|
+
if (!Number.isInteger(opts.pollInterval) || opts.pollInterval < 1) {
|
|
10528
|
+
die("--poll-interval must be a positive number of seconds");
|
|
10529
|
+
}
|
|
10530
|
+
const keepWorkspaces = opts.keepWorkspaces;
|
|
10531
|
+
if (!KEEP_WORKSPACES_MODES.includes(keepWorkspaces)) {
|
|
10532
|
+
die(
|
|
10533
|
+
`--keep-workspaces must be ${KEEP_WORKSPACES_MODES.join(", ")}, got "${opts.keepWorkspaces}"`
|
|
10534
|
+
);
|
|
10535
|
+
}
|
|
10536
|
+
if (!Number.isFinite(opts.keepWorkspacesFor) || opts.keepWorkspacesFor <= 0) {
|
|
10537
|
+
die("--keep-workspaces-for must be a positive number of hours");
|
|
10538
|
+
}
|
|
10539
|
+
if (!Number.isInteger(opts.keepWorkspacesMax) || opts.keepWorkspacesMax < 1) {
|
|
10540
|
+
die("--keep-workspaces-max must be a positive integer");
|
|
10541
|
+
}
|
|
10542
|
+
return {
|
|
10543
|
+
name: opts.name ?? hostname2(),
|
|
10544
|
+
harness,
|
|
10545
|
+
...opts.command !== void 0 ? { command: opts.command } : {},
|
|
10546
|
+
maxConcurrent: opts.maxConcurrent,
|
|
10547
|
+
pollIntervalSeconds: opts.pollInterval,
|
|
10548
|
+
keepWorkspaces,
|
|
10549
|
+
keepWorkspacesForHours: opts.keepWorkspacesFor,
|
|
10550
|
+
keepWorkspacesMax: opts.keepWorkspacesMax
|
|
10551
|
+
};
|
|
10552
|
+
}
|
|
10553
|
+
function harnessFlag(harness) {
|
|
10554
|
+
return harness.replaceAll("_", "-");
|
|
10555
|
+
}
|
|
10556
|
+
|
|
10557
|
+
// src/commands/runner-service.ts
|
|
10558
|
+
import { execFile } from "node:child_process";
|
|
10559
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
10560
|
+
import { homedir as homedir3, hostname as hostname3, userInfo } from "node:os";
|
|
10561
|
+
import { dirname as dirname6 } from "node:path";
|
|
10562
|
+
import { promisify } from "node:util";
|
|
10563
|
+
|
|
10564
|
+
// src/daemon/service.ts
|
|
10565
|
+
import { existsSync as existsSync6, statSync as statSync5 } from "node:fs";
|
|
10566
|
+
import { delimiter as delimiter2, dirname as dirname5, join as join7, sep as sep3 } from "node:path";
|
|
10567
|
+
function defaultServiceManager(platform2 = process.platform) {
|
|
10568
|
+
if (platform2 === "darwin") return "launchd";
|
|
10569
|
+
if (platform2 === "linux") return "systemd";
|
|
10570
|
+
return null;
|
|
10571
|
+
}
|
|
10572
|
+
function serviceLabel(name2, kind) {
|
|
10573
|
+
return kind === "launchd" ? `dev.tines.runner.${name2}` : `tines-runner-${name2}`;
|
|
10574
|
+
}
|
|
10575
|
+
function unitPath(name2, kind, home) {
|
|
10576
|
+
const label = serviceLabel(name2, kind);
|
|
10577
|
+
return kind === "launchd" ? join7(home, "Library", "LaunchAgents", `${label}.plist`) : join7(home, ".config", "systemd", "user", `${label}.service`);
|
|
10578
|
+
}
|
|
10579
|
+
function serviceLogPath(configDir, name2) {
|
|
10580
|
+
return join7(configDir, "logs", `runner-${name2}.log`);
|
|
10581
|
+
}
|
|
10582
|
+
function managedBinary(prefix) {
|
|
10583
|
+
return join7(prefix, "node_modules", ".bin", "tines");
|
|
10584
|
+
}
|
|
10585
|
+
function daemonArgs(input2) {
|
|
10586
|
+
const args = [
|
|
10587
|
+
"runner",
|
|
10588
|
+
"daemon",
|
|
10589
|
+
"--url",
|
|
10590
|
+
input2.url,
|
|
10591
|
+
"--name",
|
|
10592
|
+
input2.name,
|
|
10593
|
+
"--harness",
|
|
10594
|
+
input2.harness
|
|
10595
|
+
];
|
|
10596
|
+
if (input2.command !== void 0) args.push("--command", input2.command);
|
|
10597
|
+
if (input2.maxConcurrent !== 1) args.push("--max-concurrent", String(input2.maxConcurrent));
|
|
10598
|
+
if (input2.pollIntervalSeconds !== void 0 && input2.pollIntervalSeconds !== 15)
|
|
10599
|
+
args.push("--poll-interval", String(input2.pollIntervalSeconds));
|
|
10600
|
+
if (input2.keepWorkspaces !== void 0 && input2.keepWorkspaces !== "never")
|
|
10601
|
+
args.push("--keep-workspaces", input2.keepWorkspaces);
|
|
10602
|
+
if (input2.keepWorkspacesForHours !== void 0 && input2.keepWorkspacesForHours !== 72)
|
|
10603
|
+
args.push("--keep-workspaces-for", String(input2.keepWorkspacesForHours));
|
|
10604
|
+
if (input2.keepWorkspacesMax !== void 0 && input2.keepWorkspacesMax !== 20)
|
|
10605
|
+
args.push("--keep-workspaces-max", String(input2.keepWorkspacesMax));
|
|
10606
|
+
return args;
|
|
10607
|
+
}
|
|
10608
|
+
function findOnPath(name2, envPath) {
|
|
10609
|
+
for (const dir of (envPath ?? "").split(delimiter2)) {
|
|
10610
|
+
if (!dir) continue;
|
|
10611
|
+
const candidate = join7(dir, name2);
|
|
10612
|
+
try {
|
|
10613
|
+
if (statSync5(candidate).isFile()) return candidate;
|
|
10614
|
+
} catch {
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
return null;
|
|
10618
|
+
}
|
|
10619
|
+
function servicePath(input2) {
|
|
10620
|
+
const dirs = [dirname5(input2.execPath)];
|
|
10621
|
+
const missing = [];
|
|
10622
|
+
for (const name2 of input2.required) {
|
|
10623
|
+
const found = findOnPath(name2, input2.envPath);
|
|
10624
|
+
if (found) dirs.push(dirname5(found));
|
|
10625
|
+
else missing.push(name2);
|
|
10626
|
+
}
|
|
10627
|
+
dirs.push(
|
|
10628
|
+
...input2.platform === "darwin" ? ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"] : ["/usr/local/bin", "/usr/bin", "/bin"]
|
|
10629
|
+
);
|
|
10630
|
+
return { path: [...new Set(dirs)].join(delimiter2), missing };
|
|
10631
|
+
}
|
|
10632
|
+
function xml(value) {
|
|
10633
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
10634
|
+
}
|
|
10635
|
+
function renderLaunchdPlist(spec) {
|
|
10636
|
+
const label = serviceLabel(spec.name, "launchd");
|
|
10637
|
+
const env = { PATH: spec.path, ...spec.env };
|
|
10638
|
+
const lines2 = [
|
|
10639
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
10640
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
10641
|
+
'<plist version="1.0">',
|
|
10642
|
+
"<dict>",
|
|
10643
|
+
` <key>Label</key><string>${xml(label)}</string>`,
|
|
10644
|
+
" <key>ProgramArguments</key>",
|
|
10645
|
+
" <array>",
|
|
10646
|
+
...[spec.binary, ...spec.args].map((arg) => ` <string>${xml(arg)}</string>`),
|
|
10647
|
+
" </array>",
|
|
10648
|
+
" <key>EnvironmentVariables</key>",
|
|
10649
|
+
" <dict>",
|
|
10650
|
+
...Object.entries(env).map(
|
|
10651
|
+
([key, value]) => ` <key>${xml(key)}</key><string>${xml(value)}</string>`
|
|
10652
|
+
),
|
|
10653
|
+
" </dict>",
|
|
10654
|
+
" <key>RunAtLoad</key><true/>",
|
|
10655
|
+
" <!-- Relaunches after any exit: a crash, and the daemon's own exit for a self-update. -->",
|
|
10656
|
+
" <key>KeepAlive</key><true/>",
|
|
10657
|
+
` <key>StandardOutPath</key><string>${xml(spec.logPath)}</string>`,
|
|
10658
|
+
` <key>StandardErrorPath</key><string>${xml(spec.logPath)}</string>`,
|
|
10659
|
+
"</dict>",
|
|
10660
|
+
"</plist>",
|
|
10661
|
+
""
|
|
10662
|
+
];
|
|
10663
|
+
return lines2.join("\n");
|
|
10664
|
+
}
|
|
10665
|
+
function unitQuote(value) {
|
|
10666
|
+
return /^[A-Za-z0-9_./:=@%+-]+$/.test(value) ? value : `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
10667
|
+
}
|
|
10668
|
+
function renderSystemdUnit(spec) {
|
|
10669
|
+
const env = { PATH: spec.path, ...spec.env };
|
|
10670
|
+
const lines2 = [
|
|
10671
|
+
"[Unit]",
|
|
10672
|
+
`Description=Tines local runner daemon (${spec.name})`,
|
|
10673
|
+
"After=network-online.target",
|
|
10674
|
+
"",
|
|
10675
|
+
"[Service]",
|
|
10676
|
+
`ExecStart=${[spec.binary, ...spec.args].map(unitQuote).join(" ")}`,
|
|
10677
|
+
...Object.entries(env).map(([key, value]) => `Environment=${unitQuote(`${key}=${value}`)}`),
|
|
10678
|
+
`StandardOutput=append:${spec.logPath}`,
|
|
10679
|
+
`StandardError=append:${spec.logPath}`,
|
|
10680
|
+
"# always, not on-failure: the daemon exits 0 on purpose to pick up a self-update.",
|
|
10681
|
+
"Restart=always",
|
|
10682
|
+
"RestartSec=10",
|
|
10683
|
+
"",
|
|
10684
|
+
"[Install]",
|
|
10685
|
+
"WantedBy=default.target",
|
|
10686
|
+
""
|
|
10687
|
+
];
|
|
10688
|
+
return lines2.join("\n");
|
|
10689
|
+
}
|
|
10690
|
+
function renderUnit(spec) {
|
|
10691
|
+
return spec.kind === "launchd" ? renderLaunchdPlist(spec) : renderSystemdUnit(spec);
|
|
10692
|
+
}
|
|
10693
|
+
function serviceCommands(target) {
|
|
10694
|
+
const label = serviceLabel(target.name, target.kind);
|
|
10695
|
+
if (target.kind === "launchd") {
|
|
10696
|
+
const domain = `gui/${target.uid}`;
|
|
10697
|
+
return {
|
|
10698
|
+
// bootout first so a re-install replaces the loaded definition
|
|
10699
|
+
// instead of failing with "service already loaded".
|
|
10700
|
+
load: [
|
|
10701
|
+
{ argv: ["launchctl", "bootout", `${domain}/${label}`], optional: true },
|
|
10702
|
+
{ argv: ["launchctl", "bootstrap", domain, target.unitPath] }
|
|
10703
|
+
],
|
|
10704
|
+
restart: [{ argv: ["launchctl", "kickstart", "-k", `${domain}/${label}`] }],
|
|
10705
|
+
unload: [{ argv: ["launchctl", "bootout", `${domain}/${label}`], optional: true }]
|
|
10706
|
+
};
|
|
10707
|
+
}
|
|
10708
|
+
return {
|
|
10709
|
+
load: [
|
|
10710
|
+
{ argv: ["systemctl", "--user", "daemon-reload"] },
|
|
10711
|
+
{ argv: ["systemctl", "--user", "enable", "--now", label] },
|
|
10712
|
+
// So the runner keeps working while nobody is logged in.
|
|
10713
|
+
{ argv: ["loginctl", "enable-linger"], optional: true }
|
|
10714
|
+
],
|
|
10715
|
+
restart: [{ argv: ["systemctl", "--user", "restart", label] }],
|
|
10716
|
+
unload: [
|
|
10717
|
+
{ argv: ["systemctl", "--user", "disable", "--now", label], optional: true },
|
|
10718
|
+
{ argv: ["systemctl", "--user", "daemon-reload"], optional: true }
|
|
10719
|
+
]
|
|
10720
|
+
};
|
|
10721
|
+
}
|
|
10722
|
+
function matchDaemonProcesses(psOutput, query2) {
|
|
10723
|
+
const matches = [];
|
|
10724
|
+
for (const raw of psOutput.split("\n")) {
|
|
10725
|
+
const line = raw.trim();
|
|
10726
|
+
const m = /^(\d+)\s+(.*)$/.exec(line);
|
|
10727
|
+
if (!m) continue;
|
|
10728
|
+
const pid = Number(m[1]);
|
|
10729
|
+
const args = m[2];
|
|
10730
|
+
if (pid === query2.selfPid) continue;
|
|
10731
|
+
const words = args.split(/\s+/);
|
|
10732
|
+
const daemonAt = words.findIndex((w, i) => w === "runner" && words[i + 1] === "daemon");
|
|
10733
|
+
if (daemonAt < 0) continue;
|
|
10734
|
+
const binary = words[daemonAt - 1] ?? "";
|
|
10735
|
+
if (!/(^|\/)tines(\.js|\.cjs|\.mjs)?$/.test(binary) && !/(^|\/)tines$/.test(binary)) continue;
|
|
10736
|
+
let name2 = null;
|
|
10737
|
+
for (let i = daemonAt + 2; i < words.length; i++) {
|
|
10738
|
+
const w = words[i];
|
|
10739
|
+
if (w === "--name") name2 = words[i + 1] ?? null;
|
|
10740
|
+
else if (w.startsWith("--name=")) name2 = w.slice("--name=".length);
|
|
10741
|
+
}
|
|
10742
|
+
if ((name2 ?? query2.hostname) !== query2.name) continue;
|
|
10743
|
+
const root = query2.prefix.endsWith(sep3) ? query2.prefix : query2.prefix + sep3;
|
|
10744
|
+
matches.push({ pid, args, managed: args.includes(root) });
|
|
10745
|
+
}
|
|
10746
|
+
return matches;
|
|
10747
|
+
}
|
|
10748
|
+
function daemonStartedLine(name2) {
|
|
10749
|
+
const escaped = name2.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10750
|
+
return new RegExp(`(reconnecting as runner|registered runner) "${escaped}"`);
|
|
10751
|
+
}
|
|
10752
|
+
function managedBinaryPresent(prefix) {
|
|
10753
|
+
return existsSync6(managedBinary(prefix));
|
|
10754
|
+
}
|
|
10755
|
+
|
|
10756
|
+
// src/commands/runner-service.ts
|
|
10757
|
+
var run = promisify(execFile);
|
|
10758
|
+
var STARTUP_TIMEOUT_MS = 3e4;
|
|
10759
|
+
var log2 = (message3) => console.log(message3);
|
|
10760
|
+
function resolveServiceManager(flag) {
|
|
10761
|
+
if (flag !== void 0) {
|
|
10762
|
+
if (flag !== "launchd" && flag !== "systemd")
|
|
10763
|
+
die(`--service-manager must be launchd or systemd, got "${flag}"`);
|
|
10764
|
+
return flag;
|
|
10765
|
+
}
|
|
10766
|
+
const kind = defaultServiceManager();
|
|
10767
|
+
if (!kind) {
|
|
10768
|
+
die(
|
|
10769
|
+
`no service manager is known for ${process.platform}; run \`tines runner daemon\` under your own supervisor instead`
|
|
10770
|
+
);
|
|
10771
|
+
}
|
|
10772
|
+
return kind;
|
|
10773
|
+
}
|
|
10774
|
+
async function runServiceCommands(steps) {
|
|
10775
|
+
for (const step of steps) {
|
|
10776
|
+
try {
|
|
10777
|
+
await run(step.argv[0], step.argv.slice(1), { timeout: 3e4 });
|
|
10778
|
+
} catch (err) {
|
|
10779
|
+
if (step.optional) continue;
|
|
10780
|
+
const detail = err && typeof err === "object" && "stderr" in err && String(err.stderr).trim() ? `: ${String(err.stderr).trim()}` : err instanceof Error ? `: ${err.message}` : "";
|
|
10781
|
+
die(`\`${step.argv.join(" ")}\` failed${detail}`);
|
|
10782
|
+
}
|
|
10783
|
+
}
|
|
10784
|
+
}
|
|
10785
|
+
async function listProcesses() {
|
|
10786
|
+
try {
|
|
10787
|
+
const { stdout } = await run("ps", ["-axo", "pid=,args="], { maxBuffer: 16 * 1024 * 1024 });
|
|
10788
|
+
return stdout;
|
|
10789
|
+
} catch {
|
|
10790
|
+
return null;
|
|
10791
|
+
}
|
|
10792
|
+
}
|
|
10793
|
+
function fileSize(path2) {
|
|
10794
|
+
try {
|
|
10795
|
+
return statSync6(path2).size;
|
|
10796
|
+
} catch {
|
|
10797
|
+
return 0;
|
|
10798
|
+
}
|
|
10799
|
+
}
|
|
10800
|
+
async function waitForStart(logPath, offset, name2) {
|
|
10801
|
+
const pattern = daemonStartedLine(name2);
|
|
10802
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
10803
|
+
while (Date.now() < deadline) {
|
|
10804
|
+
if (existsSync7(logPath)) {
|
|
10805
|
+
const content = readFileSync10(logPath, "utf8").slice(offset);
|
|
10806
|
+
if (pattern.test(content)) return true;
|
|
10807
|
+
}
|
|
10808
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
10809
|
+
}
|
|
10810
|
+
return false;
|
|
10811
|
+
}
|
|
10812
|
+
function serviceTarget(name2, kind) {
|
|
10813
|
+
return { kind, name: name2, unitPath: unitPath(name2, kind, homedir3()), uid: userInfo().uid };
|
|
10814
|
+
}
|
|
10815
|
+
function registerServiceCommands(runnerCmd) {
|
|
10816
|
+
withCommon(
|
|
10817
|
+
withDaemonFlags(
|
|
10818
|
+
runnerCmd.command("install").description(
|
|
10819
|
+
"Register this machine as a runner and install the daemon as a launchd/systemd service that keeps itself updated"
|
|
10820
|
+
)
|
|
10821
|
+
).option("--service-manager <kind>", "launchd | systemd (default: the one this OS ships)")
|
|
10822
|
+
).action(async (opts) => {
|
|
10823
|
+
const settings = parseDaemonFlags(opts);
|
|
10824
|
+
const kind = resolveServiceManager(opts.serviceManager);
|
|
10825
|
+
const baseUrl = resolveUrl(opts).replace(/\/+$/, "");
|
|
10826
|
+
const configDir = defaultConfigDir();
|
|
10827
|
+
mkdirSync5(configDir, { recursive: true });
|
|
10828
|
+
const prefix = agentCliPrefix(configDir);
|
|
10829
|
+
const ps = await listProcesses();
|
|
10830
|
+
const running = ps ? matchDaemonProcesses(ps, {
|
|
10831
|
+
name: settings.name,
|
|
10832
|
+
hostname: hostname3(),
|
|
10833
|
+
prefix,
|
|
10834
|
+
selfPid: process.pid
|
|
10835
|
+
}) : [];
|
|
10836
|
+
if (running.length > 0) {
|
|
10837
|
+
const label = serviceLabel(settings.name, kind);
|
|
10838
|
+
const lines2 = running.map((p) => ` pid ${p.pid}: ${p.args}`);
|
|
10839
|
+
die(
|
|
10840
|
+
running.every((p) => p.managed) ? `"${settings.name}" is already running under its service (${label}):
|
|
10841
|
+
${lines2.join("\n")}
|
|
10842
|
+
to change its flags: \`tines runners pause ${settings.name}\`, wait for 0 active runs, \`tines runner uninstall ${settings.name}\`, then install again \u2014 or \`tines runner restart ${settings.name}\` to relaunch it as is` : `a daemon for "${settings.name}" is already running outside the service:
|
|
10843
|
+
${lines2.join("\n")}
|
|
10844
|
+
stop it first \u2014 \`tines runners pause ${settings.name}\`, wait for 0 active runs, kill that process \u2014 then run install again; it reconnects with the stored token, so no key is needed`
|
|
10845
|
+
);
|
|
10846
|
+
}
|
|
10847
|
+
if (!managedBinaryPresent(prefix)) {
|
|
10848
|
+
log2(`installing the daemon-managed tines into ${prefix} \u2026`);
|
|
10849
|
+
await installAgentCli({ configDir, log: log2 });
|
|
10850
|
+
if (!managedBinaryPresent(prefix)) {
|
|
10851
|
+
die(
|
|
10852
|
+
`could not install tines into ${prefix}; the service needs that copy (npm and network access required)`
|
|
10853
|
+
);
|
|
10854
|
+
}
|
|
10855
|
+
}
|
|
10856
|
+
let ensured;
|
|
10857
|
+
try {
|
|
10858
|
+
ensured = await ensureRunnerCredentials({
|
|
10859
|
+
configDir,
|
|
10860
|
+
baseUrl,
|
|
10861
|
+
name: settings.name,
|
|
10862
|
+
harness: settings.harness,
|
|
10863
|
+
...settings.command !== void 0 ? { command: settings.command } : {},
|
|
10864
|
+
maxConcurrent: settings.maxConcurrent,
|
|
10865
|
+
...resolveApiKey(opts) !== void 0 ? { apiKey: resolveApiKey(opts) } : {},
|
|
10866
|
+
log: log2
|
|
10867
|
+
});
|
|
10868
|
+
} catch (err) {
|
|
10869
|
+
die(err instanceof Error ? err.message : String(err));
|
|
10870
|
+
}
|
|
10871
|
+
const harnessBinary = settings.harness === "claude_code" ? "claude" : settings.harness === "codex" ? "codex" : null;
|
|
10872
|
+
const { path: path2, missing } = servicePath({
|
|
10873
|
+
execPath: process.execPath,
|
|
10874
|
+
required: [...harnessBinary ? [harnessBinary] : [], "git"],
|
|
10875
|
+
envPath: process.env.PATH,
|
|
10876
|
+
platform: process.platform
|
|
10877
|
+
});
|
|
10878
|
+
for (const name2 of missing) {
|
|
10879
|
+
log2(
|
|
10880
|
+
`warning: \`${name2}\` is not on this shell's PATH, so it is not on the service's either \u2014 install it, then \`tines runner install\` again`
|
|
10881
|
+
);
|
|
10882
|
+
}
|
|
10883
|
+
const target = serviceTarget(settings.name, kind);
|
|
10884
|
+
const logPath = serviceLogPath(configDir, settings.name);
|
|
10885
|
+
const spec = {
|
|
10886
|
+
name: settings.name,
|
|
10887
|
+
kind,
|
|
10888
|
+
binary: managedBinary(prefix),
|
|
10889
|
+
args: daemonArgs({
|
|
10890
|
+
url: baseUrl,
|
|
10891
|
+
name: settings.name,
|
|
10892
|
+
harness: harnessFlag(settings.harness),
|
|
10893
|
+
...settings.command !== void 0 ? { command: settings.command } : {},
|
|
10894
|
+
maxConcurrent: settings.maxConcurrent,
|
|
10895
|
+
pollIntervalSeconds: settings.pollIntervalSeconds,
|
|
10896
|
+
keepWorkspaces: settings.keepWorkspaces,
|
|
10897
|
+
keepWorkspacesForHours: settings.keepWorkspacesForHours,
|
|
10898
|
+
keepWorkspacesMax: settings.keepWorkspacesMax
|
|
10899
|
+
}),
|
|
10900
|
+
path: path2,
|
|
10901
|
+
logPath,
|
|
10902
|
+
env: process.env.TINES_CONFIG_DIR ? { TINES_CONFIG_DIR: process.env.TINES_CONFIG_DIR } : {}
|
|
10903
|
+
};
|
|
10904
|
+
mkdirSync5(dirname6(target.unitPath), { recursive: true });
|
|
10905
|
+
mkdirSync5(dirname6(logPath), { recursive: true });
|
|
10906
|
+
writeFileSync4(target.unitPath, renderUnit(spec), { mode: 420 });
|
|
10907
|
+
log2(`wrote ${target.unitPath}`);
|
|
10908
|
+
const offset = fileSize(logPath);
|
|
10909
|
+
await runServiceCommands(serviceCommands(target).load);
|
|
10910
|
+
log2(`loaded ${serviceLabel(settings.name, kind)}; waiting for the daemon to report in \u2026`);
|
|
10911
|
+
const started = await waitForStart(logPath, offset, settings.name);
|
|
10912
|
+
const summary = {
|
|
10913
|
+
name: settings.name,
|
|
10914
|
+
runner_id: ensured.creds.runner_id,
|
|
10915
|
+
registered: ensured.registered,
|
|
10916
|
+
service_manager: kind,
|
|
10917
|
+
unit_path: target.unitPath,
|
|
10918
|
+
log_path: logPath,
|
|
10919
|
+
started
|
|
10920
|
+
};
|
|
10921
|
+
if (!started) {
|
|
10922
|
+
if (opts.json) printJson(summary);
|
|
10923
|
+
die(
|
|
10924
|
+
`the service is loaded but the daemon has not reported in after ${STARTUP_TIMEOUT_MS / 1e3}s \u2014 read ${logPath} (a missing \`node\` on the service PATH, or a rejected token, shows there)`
|
|
10925
|
+
);
|
|
10926
|
+
}
|
|
10927
|
+
if (opts.json) return printJson(summary);
|
|
10928
|
+
log2(
|
|
10929
|
+
`runner "${settings.name}" is up under ${kind} and updates itself: log at ${logPath}; \`tines runner restart ${settings.name}\` relaunches it, \`tines runner uninstall ${settings.name}\` removes the service`
|
|
10930
|
+
);
|
|
10931
|
+
if (ensured.registered) log2(nextStepsMessage(settings.name, baseUrl));
|
|
10932
|
+
});
|
|
10933
|
+
runnerCmd.command("restart [name]").description(
|
|
10934
|
+
"Relaunch an installed runner service (interrupts runs in flight: pause the runner and wait for 0 active runs first if that matters)"
|
|
10935
|
+
).option("--service-manager <kind>", "launchd | systemd (default: the one this OS ships)").option("--json", "print JSON instead of a message").action(async (name2, opts) => {
|
|
10936
|
+
const kind = resolveServiceManager(opts.serviceManager);
|
|
10937
|
+
const target = serviceTarget(name2 ?? hostname3(), kind);
|
|
10938
|
+
if (!existsSync7(target.unitPath)) {
|
|
10939
|
+
die(
|
|
10940
|
+
`"${target.name}" is not installed as a service (no ${target.unitPath}); run \`tines runner install --name ${target.name} \u2026\` first`
|
|
10941
|
+
);
|
|
10942
|
+
}
|
|
10943
|
+
await runServiceCommands(serviceCommands(target).restart);
|
|
10944
|
+
if (opts.json)
|
|
10945
|
+
return printJson({ name: target.name, service_manager: kind, restarted: true });
|
|
10946
|
+
log2(`restarted ${serviceLabel(target.name, kind)}`);
|
|
10947
|
+
});
|
|
10948
|
+
runnerCmd.command("uninstall [name]").description(
|
|
10949
|
+
"Stop and remove a runner's service unit (the stored runner token is kept, so a later install reconnects)"
|
|
10950
|
+
).option("--service-manager <kind>", "launchd | systemd (default: the one this OS ships)").option("--json", "print JSON instead of a message").action(async (name2, opts) => {
|
|
10951
|
+
const kind = resolveServiceManager(opts.serviceManager);
|
|
10952
|
+
const target = serviceTarget(name2 ?? hostname3(), kind);
|
|
10953
|
+
const existed = existsSync7(target.unitPath);
|
|
10954
|
+
await runServiceCommands(serviceCommands(target).unload);
|
|
10955
|
+
rmSync4(target.unitPath, { force: true });
|
|
10956
|
+
if (opts.json)
|
|
10957
|
+
return printJson({ name: target.name, service_manager: kind, removed: existed });
|
|
10958
|
+
log2(
|
|
10959
|
+
existed ? `removed ${serviceLabel(target.name, kind)} (${target.unitPath}); the runner token is still stored, so \`tines runner install\` reconnects without a key` : `no service unit for "${target.name}" at ${target.unitPath}; nothing to remove`
|
|
10960
|
+
);
|
|
10160
10961
|
});
|
|
10161
10962
|
}
|
|
10162
10963
|
|
|
@@ -10455,87 +11256,38 @@ function register8(program3) {
|
|
|
10455
11256
|
});
|
|
10456
11257
|
const runnerCmd = program3.command("runner").description("The local runner daemon");
|
|
10457
11258
|
withCommon(
|
|
10458
|
-
|
|
10459
|
-
"
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
"runner name, unique per user; name it machine-plus-harness, e.g. macbook-claude (default: this hostname)"
|
|
10463
|
-
).option("--harness <harness>", "claude-code | codex | custom", "claude-code").option(
|
|
10464
|
-
"--command <template>",
|
|
10465
|
-
"custom harness command template ({prompt_file}, {workspace}, {model})"
|
|
10466
|
-
).option("--max-concurrent <n>", "maximum simultaneous runs", (v) => Number.parseInt(v, 10), 1).option(
|
|
10467
|
-
"--poll-interval <seconds>",
|
|
10468
|
-
"seconds between polls",
|
|
10469
|
-
(v) => Number.parseInt(v, 10),
|
|
10470
|
-
15
|
|
11259
|
+
withDaemonFlags(
|
|
11260
|
+
runnerCmd.command("daemon").description(
|
|
11261
|
+
"Run the local runner daemon in the foreground: register/reconnect, poll for assigned runs, execute them (`tines runner install` runs it as a service instead)"
|
|
11262
|
+
)
|
|
10471
11263
|
).option(
|
|
10472
11264
|
"--no-cli-refresh",
|
|
10473
11265
|
"do not install/refresh the agent-facing tines CLI from npm (harnesses use the ambient PATH)"
|
|
10474
11266
|
).option(
|
|
10475
11267
|
"--no-self-update",
|
|
10476
11268
|
"do not exit for the service manager to relaunch a newer daemon (only applies when launched from the daemon-managed prefix)"
|
|
10477
|
-
).option(
|
|
10478
|
-
"--keep-workspaces <mode>",
|
|
10479
|
-
"keep settled runs' workspaces for debugging: never | failed | always",
|
|
10480
|
-
"never"
|
|
10481
|
-
).option(
|
|
10482
|
-
"--keep-workspaces-for <hours>",
|
|
10483
|
-
"delete kept workspaces older than this",
|
|
10484
|
-
(v) => Number(v),
|
|
10485
|
-
72
|
|
10486
|
-
).option(
|
|
10487
|
-
"--keep-workspaces-max <n>",
|
|
10488
|
-
"keep at most this many workspaces (oldest removed first)",
|
|
10489
|
-
(v) => Number.parseInt(v, 10),
|
|
10490
|
-
20
|
|
10491
11269
|
)
|
|
10492
11270
|
).action(
|
|
10493
11271
|
async (opts) => {
|
|
10494
|
-
const
|
|
10495
|
-
if (!HARNESS_KINDS.includes(harness)) {
|
|
10496
|
-
die(`--harness must be claude-code, codex, or custom, got "${opts.harness}"`);
|
|
10497
|
-
}
|
|
10498
|
-
if (harness === "custom" && !opts.command) {
|
|
10499
|
-
die(
|
|
10500
|
-
'the custom harness needs --command "<template>" ({prompt_file}, {workspace}, {model})'
|
|
10501
|
-
);
|
|
10502
|
-
}
|
|
10503
|
-
if (harness !== "custom" && opts.command) die("--command only applies to --harness custom");
|
|
10504
|
-
if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent < 1 || opts.maxConcurrent > 100) {
|
|
10505
|
-
die("--max-concurrent must be an integer between 1 and 100");
|
|
10506
|
-
}
|
|
10507
|
-
if (!Number.isInteger(opts.pollInterval) || opts.pollInterval < 1) {
|
|
10508
|
-
die("--poll-interval must be a positive number of seconds");
|
|
10509
|
-
}
|
|
10510
|
-
const keepWorkspaces = opts.keepWorkspaces;
|
|
10511
|
-
if (!KEEP_WORKSPACES_MODES.includes(keepWorkspaces)) {
|
|
10512
|
-
die(
|
|
10513
|
-
`--keep-workspaces must be ${KEEP_WORKSPACES_MODES.join(", ")}, got "${opts.keepWorkspaces}"`
|
|
10514
|
-
);
|
|
10515
|
-
}
|
|
10516
|
-
if (!Number.isFinite(opts.keepWorkspacesFor) || opts.keepWorkspacesFor <= 0) {
|
|
10517
|
-
die("--keep-workspaces-for must be a positive number of hours");
|
|
10518
|
-
}
|
|
10519
|
-
if (!Number.isInteger(opts.keepWorkspacesMax) || opts.keepWorkspacesMax < 1) {
|
|
10520
|
-
die("--keep-workspaces-max must be a positive integer");
|
|
10521
|
-
}
|
|
11272
|
+
const settings = parseDaemonFlags(opts);
|
|
10522
11273
|
await runDaemon({
|
|
10523
11274
|
url: resolveUrl(opts).replace(/\/+$/, ""),
|
|
10524
11275
|
apiKey: resolveApiKey(opts),
|
|
10525
|
-
name:
|
|
10526
|
-
harness,
|
|
10527
|
-
command:
|
|
10528
|
-
maxConcurrent:
|
|
10529
|
-
pollIntervalMs:
|
|
11276
|
+
name: settings.name,
|
|
11277
|
+
harness: settings.harness,
|
|
11278
|
+
command: settings.command,
|
|
11279
|
+
maxConcurrent: settings.maxConcurrent,
|
|
11280
|
+
pollIntervalMs: settings.pollIntervalSeconds * 1e3,
|
|
10530
11281
|
configDir: defaultConfigDir(),
|
|
10531
11282
|
cliRefresh: opts.cliRefresh,
|
|
10532
11283
|
selfUpdate: opts.selfUpdate,
|
|
10533
|
-
keepWorkspaces,
|
|
10534
|
-
keepWorkspacesForHours:
|
|
10535
|
-
keepWorkspacesMax:
|
|
11284
|
+
keepWorkspaces: settings.keepWorkspaces,
|
|
11285
|
+
keepWorkspacesForHours: settings.keepWorkspacesForHours,
|
|
11286
|
+
keepWorkspacesMax: settings.keepWorkspacesMax
|
|
10536
11287
|
});
|
|
10537
11288
|
}
|
|
10538
11289
|
);
|
|
11290
|
+
registerServiceCommands(runnerCmd);
|
|
10539
11291
|
const workspacesCmd = runnerCmd.command("workspaces").description("List run workspaces the daemon kept for debugging (--keep-workspaces)").option("--json", "print JSON instead of a table").action((opts) => {
|
|
10540
11292
|
const configDir = defaultConfigDir();
|
|
10541
11293
|
const kept = listKeptWorkspaces(configDir);
|
|
@@ -10622,14 +11374,14 @@ function register8(program3) {
|
|
|
10622
11374
|
if (evidence.population === "pending") {
|
|
10623
11375
|
table([
|
|
10624
11376
|
["ID", "ISSUE", "RUNNER", "TIER", "STATUS", "COST", "CREATED"],
|
|
10625
|
-
...items.map((
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
11377
|
+
...items.map((run2) => [
|
|
11378
|
+
run2.id,
|
|
11379
|
+
run2.issue_ref ? `${run2.issue_ref.project_name}/${run2.issue_ref.number}` : run2.issue_id,
|
|
11380
|
+
run2.runner_name,
|
|
11381
|
+
run2.tier,
|
|
10630
11382
|
"Pending at cutoff",
|
|
10631
11383
|
"\u2014",
|
|
10632
|
-
timestamp(
|
|
11384
|
+
timestamp(run2.created_at)
|
|
10633
11385
|
])
|
|
10634
11386
|
]);
|
|
10635
11387
|
return;
|
|
@@ -10645,29 +11397,29 @@ function register8(program3) {
|
|
|
10645
11397
|
).action(
|
|
10646
11398
|
async (id2, opts) => {
|
|
10647
11399
|
const api = client(opts);
|
|
10648
|
-
const
|
|
10649
|
-
if (opts.json) return printJson(
|
|
11400
|
+
const run2 = await api.getRun(id2);
|
|
11401
|
+
if (opts.json) return printJson(run2);
|
|
10650
11402
|
console.log(
|
|
10651
|
-
`${
|
|
11403
|
+
`${run2.id} ${run2.status} on ${run2.runner_name}${run2.resumed_from_run_id ? ` \xB7 resumed run ${run2.resumed_from_run_id}` : ""}`
|
|
10652
11404
|
);
|
|
10653
|
-
if (
|
|
10654
|
-
console.log(`tier: ${
|
|
11405
|
+
if (run2.issue_ref) console.log(`issue: ${issueRef(run2.issue_ref)} \u2014 ${run2.issue_ref.title}`);
|
|
11406
|
+
console.log(`tier: ${run2.tier} model: ${run2.model ?? "(n/a)"}`);
|
|
10655
11407
|
console.log(
|
|
10656
|
-
`states: ${
|
|
11408
|
+
`states: ${run2.state_at_start_name ?? run2.state_id_at_start} \u2192 ${run2.state_at_end_name ?? run2.state_id_at_end ?? "\u2026"}`
|
|
10657
11409
|
);
|
|
10658
11410
|
console.log(
|
|
10659
|
-
`created: ${timestamp(
|
|
11411
|
+
`created: ${timestamp(run2.created_at)} started: ${run2.started_at ? timestamp(run2.started_at) : "\u2014"} ended: ${run2.ended_at ? timestamp(run2.ended_at) : "\u2014"} duration: ${runDurationLabel(run2)}`
|
|
10660
11412
|
);
|
|
10661
|
-
if (
|
|
10662
|
-
const u =
|
|
10663
|
-
const costLabel = runCostLabel(
|
|
11413
|
+
if (run2.usage) {
|
|
11414
|
+
const u = run2.usage;
|
|
11415
|
+
const costLabel = runCostLabel(run2);
|
|
10664
11416
|
const metric = (value) => value === void 0 ? "unknown" : value.toLocaleString();
|
|
10665
11417
|
console.log(
|
|
10666
11418
|
`usage: input ${metric(u.input_tokens)} cache-read ${metric(u.cache_read_tokens)} cache-write ${metric(u.cache_write_tokens)} output ${metric(u.output_tokens)}`
|
|
10667
11419
|
);
|
|
10668
11420
|
if (u.cost_usd !== void 0)
|
|
10669
11421
|
console.log(
|
|
10670
|
-
`cost: ${runCostLabel(
|
|
11422
|
+
`cost: ${runCostLabel(run2)}${u.cost_source === void 0 ? " \xB7 source unavailable" : ""}`
|
|
10671
11423
|
);
|
|
10672
11424
|
const pricing = u.pricing;
|
|
10673
11425
|
if (pricing?.status === "provider_authoritative")
|
|
@@ -10691,10 +11443,20 @@ function register8(program3) {
|
|
|
10691
11443
|
console.log(` exact estimated USD: ${b.cost_usd_exact}`);
|
|
10692
11444
|
console.log(" Standard API list-price estimate; not an invoice or subscription usage.");
|
|
10693
11445
|
}
|
|
11446
|
+
const proof = pricing?.evidence?.request_context;
|
|
11447
|
+
if (proof?.status === "complete") {
|
|
11448
|
+
console.log(
|
|
11449
|
+
`request context: ${proof.request_count.toLocaleString()} verified \xB7 largest input ${proof.max_request_input_tokens.toLocaleString()} \xB7 Codex ${proof.harness_version} \xB7 ${proof.normalization}`
|
|
11450
|
+
);
|
|
11451
|
+
} else if (proof) {
|
|
11452
|
+
console.log(
|
|
11453
|
+
`request context: ${proof.status} (${proof.reason})${proof.harness_version ? ` \xB7 Codex ${proof.harness_version}` : ""} \xB7 ${proof.normalization}`
|
|
11454
|
+
);
|
|
11455
|
+
}
|
|
10694
11456
|
}
|
|
10695
|
-
if (
|
|
10696
|
-
if (
|
|
10697
|
-
if (
|
|
11457
|
+
if (run2.provider_session_id) console.log(`provider session: ${run2.provider_session_id}`);
|
|
11458
|
+
if (run2.provider_url) console.log(`provider console: ${run2.provider_url}`);
|
|
11459
|
+
if (run2.error) console.log(`error: ${run2.error}`);
|
|
10698
11460
|
if (opts.logs) {
|
|
10699
11461
|
console.log("");
|
|
10700
11462
|
if (opts.full || opts.raw) {
|
|
@@ -10711,12 +11473,12 @@ function register8(program3) {
|
|
|
10711
11473
|
process.stdout.write(decoder.decode());
|
|
10712
11474
|
return;
|
|
10713
11475
|
}
|
|
10714
|
-
if (
|
|
11476
|
+
if (run2.log_bytes_dropped > 0) {
|
|
10715
11477
|
console.log(
|
|
10716
|
-
`[${Math.round(
|
|
11478
|
+
`[${Math.round(run2.log_bytes_dropped / 1024)} KB truncated from the head \u2014 ` + (run2.log_expired ? "past its retention window; only this tail remains]" : `run \`tines runs show ${run2.id} --logs --full\` for the complete ${Math.round(run2.log_full_bytes / 1024)} KB log]`)
|
|
10717
11479
|
);
|
|
10718
11480
|
}
|
|
10719
|
-
console.log(
|
|
11481
|
+
console.log(run2.log || "(no log output captured)");
|
|
10720
11482
|
}
|
|
10721
11483
|
}
|
|
10722
11484
|
);
|
|
@@ -10724,10 +11486,10 @@ function register8(program3) {
|
|
|
10724
11486
|
runsCmd.command("cancel <id>").description("Cancel a run (judged like any other end: usually a strike)")
|
|
10725
11487
|
).action(async (id2, opts) => {
|
|
10726
11488
|
const api = client(opts);
|
|
10727
|
-
const
|
|
10728
|
-
if (opts.json) return printJson(
|
|
11489
|
+
const run2 = await api.cancelRun(id2);
|
|
11490
|
+
if (opts.json) return printJson(run2);
|
|
10729
11491
|
console.log(
|
|
10730
|
-
`canceled ${
|
|
11492
|
+
`canceled ${run2.id}${run2.issue_ref ? ` on ${issueRef(run2.issue_ref)}` : ""} (was on ${run2.runner_name})`
|
|
10731
11493
|
);
|
|
10732
11494
|
});
|
|
10733
11495
|
}
|
|
@@ -11104,12 +11866,12 @@ function register10(program3) {
|
|
|
11104
11866
|
const api = client(opts);
|
|
11105
11867
|
const overrides = {};
|
|
11106
11868
|
for (const spec of opts.state) {
|
|
11107
|
-
const
|
|
11108
|
-
if (
|
|
11869
|
+
const sep4 = spec.lastIndexOf("=");
|
|
11870
|
+
if (sep4 < 1 || sep4 === spec.length - 1) {
|
|
11109
11871
|
die(`--state must look like <workflow>/<state>=<n>, got "${spec}"`);
|
|
11110
11872
|
}
|
|
11111
|
-
const limit = Number.parseInt(spec.slice(
|
|
11112
|
-
const { state } = await resolveStateFlag(api, spec.slice(0,
|
|
11873
|
+
const limit = Number.parseInt(spec.slice(sep4 + 1), 10);
|
|
11874
|
+
const { state } = await resolveStateFlag(api, spec.slice(0, sep4));
|
|
11113
11875
|
overrides[state.id] = limit;
|
|
11114
11876
|
}
|
|
11115
11877
|
const settings = await api.updateSupervisorSettings({
|
|
@@ -11126,17 +11888,17 @@ function register10(program3) {
|
|
|
11126
11888
|
}
|
|
11127
11889
|
|
|
11128
11890
|
// src/commands/workflows.ts
|
|
11129
|
-
import { readFileSync as
|
|
11891
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
11130
11892
|
|
|
11131
11893
|
// src/workflow-packages.ts
|
|
11132
11894
|
import { createInterface as createInterface4 } from "node:readline/promises";
|
|
11133
11895
|
import { createHash } from "node:crypto";
|
|
11134
|
-
import { readFileSync as
|
|
11896
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
11135
11897
|
var PACKAGE_STDIN = "-";
|
|
11136
11898
|
var MAX_LOCAL_JSON_BYTES = 32 * 1024 * 1024;
|
|
11137
11899
|
function readPackageSource(path2) {
|
|
11138
11900
|
try {
|
|
11139
|
-
const bytes2 =
|
|
11901
|
+
const bytes2 = readFileSync11(path2 === PACKAGE_STDIN ? 0 : path2);
|
|
11140
11902
|
if (bytes2.byteLength > MAX_LOCAL_JSON_BYTES)
|
|
11141
11903
|
throw new Error(`input exceeds ${MAX_LOCAL_JSON_BYTES} bytes`);
|
|
11142
11904
|
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2);
|
|
@@ -11480,14 +12242,14 @@ function readJsonBody(inline, file) {
|
|
|
11480
12242
|
if (file !== void 0 && file !== "-") {
|
|
11481
12243
|
let raw;
|
|
11482
12244
|
try {
|
|
11483
|
-
raw =
|
|
12245
|
+
raw = readFileSync12(file, "utf8");
|
|
11484
12246
|
} catch (err) {
|
|
11485
12247
|
die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
11486
12248
|
}
|
|
11487
12249
|
return parseJsonObject(raw, file);
|
|
11488
12250
|
}
|
|
11489
12251
|
if (file === "-" || !process.stdin.isTTY) {
|
|
11490
|
-
const raw =
|
|
12252
|
+
const raw = readFileSync12(0, "utf8");
|
|
11491
12253
|
if (raw.trim() === "") {
|
|
11492
12254
|
if (file === "-") die("no JSON on stdin");
|
|
11493
12255
|
return void 0;
|
|
@@ -11596,9 +12358,9 @@ async function resolveStateBases(states, library) {
|
|
|
11596
12358
|
}
|
|
11597
12359
|
}
|
|
11598
12360
|
function resolveBasePair(lib, pair, stateName) {
|
|
11599
|
-
const
|
|
11600
|
-
const workflowRef = pair.slice(0,
|
|
11601
|
-
const stateRef2 = pair.slice(
|
|
12361
|
+
const sep4 = pair.indexOf("/");
|
|
12362
|
+
const workflowRef = pair.slice(0, sep4).trim();
|
|
12363
|
+
const stateRef2 = pair.slice(sep4 + 1).trim();
|
|
11602
12364
|
const where = `state "${stateName}" inherits from "${pair}"`;
|
|
11603
12365
|
if (!workflowRef || !stateRef2) {
|
|
11604
12366
|
die(`${where}, which is not a <workflow>/<state> pair`);
|
|
@@ -11892,8 +12654,8 @@ function registerPackageCommands(workflows) {
|
|
|
11892
12654
|
else {
|
|
11893
12655
|
console.log(`installed workflow package (${receipt.id})`);
|
|
11894
12656
|
console.log(`plan: ${receipt.plan_digest}`);
|
|
11895
|
-
for (const
|
|
11896
|
-
console.log(` ${
|
|
12657
|
+
for (const object4 of receipt.objects)
|
|
12658
|
+
console.log(` ${object4.kind}: ${object4.name} \u2014 ${object4.href}`);
|
|
11897
12659
|
}
|
|
11898
12660
|
}
|
|
11899
12661
|
);
|