@whittlelabs/sifter 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +3 -1
  2. package/bin.js +352 -97
  3. package/bin.js.map +4 -4
  4. package/package.json +1 -1
package/bin.js CHANGED
@@ -9961,6 +9961,8 @@ var require_progress = __commonJS({
9961
9961
  seq = 0;
9962
9962
  pending = [];
9963
9963
  textBuf = "";
9964
+ /** The last two characters of reasoning emitted since the last tool; null when none. */
9965
+ reasoningTail = null;
9964
9966
  timer = null;
9965
9967
  closed = false;
9966
9968
  flushMs;
@@ -9970,20 +9972,39 @@ var require_progress = __commonJS({
9970
9972
  this.flushMs = options.flushMs ?? 400;
9971
9973
  this.maxChars = options.maxChars ?? 1200;
9972
9974
  }
9975
+ /** A fragment of reasoning, continuing whatever came before it. */
9973
9976
  reasoning(text) {
9974
9977
  if (this.closed || !text)
9975
9978
  return;
9976
9979
  this.textBuf += text;
9980
+ this.reasoningTail = ((this.reasoningTail ?? "") + text).slice(-2);
9977
9981
  if (this.textBuf.length >= this.maxChars) {
9978
9982
  this.flush();
9979
9983
  } else {
9980
9984
  this.arm();
9981
9985
  }
9982
9986
  }
9987
+ /**
9988
+ * A whole block of reasoning (a thinking summary, a message between tool
9989
+ * calls), trimmed and set off from the reasoning before it by one blank
9990
+ * line. A consumer flows consecutive reasoning frames into one passage, so
9991
+ * two blocks reported as fragments run together mid-sentence, and a
9992
+ * block's own trailing blank lines (thinking summaries end in them) would
9993
+ * show as empty lines before the next act.
9994
+ */
9995
+ reasoningBlock(text) {
9996
+ const block = text.trim();
9997
+ if (this.closed || !block)
9998
+ return;
9999
+ const tail = this.reasoningTail;
10000
+ const gap = tail === null || tail.endsWith("\n\n") ? "" : tail.endsWith("\n") ? "\n" : "\n\n";
10001
+ this.reasoning(gap + block);
10002
+ }
9983
10003
  tool(name, target) {
9984
10004
  if (this.closed || !name)
9985
10005
  return;
9986
10006
  this.drainText();
10007
+ this.reasoningTail = null;
9987
10008
  this.push({ kind: "tool", tool: target ? { name, target } : { name } });
9988
10009
  this.flush();
9989
10010
  }
@@ -10611,6 +10632,87 @@ var require_workspace_telemetry = __commonJS({
10611
10632
  }
10612
10633
  });
10613
10634
 
10635
+ // ../../packages/shuttle/dist/executors/cli-version.js
10636
+ var require_cli_version = __commonJS({
10637
+ "../../packages/shuttle/dist/executors/cli-version.js"(exports2) {
10638
+ "use strict";
10639
+ Object.defineProperty(exports2, "__esModule", { value: true });
10640
+ exports2.cliVersion = cliVersion;
10641
+ var child_process_1 = require("child_process");
10642
+ var VERSION_TIMEOUT_MS = 5e3;
10643
+ function cliVersion(bin, env) {
10644
+ return new Promise((resolve) => {
10645
+ try {
10646
+ (0, child_process_1.execFile)(bin, ["--version"], { timeout: VERSION_TIMEOUT_MS, ...env ? { env } : {} }, (err, stdout) => {
10647
+ resolve(err ? null : String(stdout).match(/\d+\.\d+\.\d+/)?.[0] ?? null);
10648
+ });
10649
+ } catch {
10650
+ resolve(null);
10651
+ }
10652
+ });
10653
+ }
10654
+ }
10655
+ });
10656
+
10657
+ // ../../packages/shuttle/dist/version-check.js
10658
+ var require_version_check = __commonJS({
10659
+ "../../packages/shuttle/dist/version-check.js"(exports2) {
10660
+ "use strict";
10661
+ Object.defineProperty(exports2, "__esModule", { value: true });
10662
+ exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = void 0;
10663
+ exports2.compareSemver = compareSemver;
10664
+ exports2.checkResponseMinVersion = checkResponseMinVersion;
10665
+ exports2.readResponseLatestVersion = readResponseLatestVersion;
10666
+ exports2.MIN_VERSION_HEADER = "x-sifter-min-version";
10667
+ exports2.LATEST_VERSION_HEADER = "x-sifter-latest-version";
10668
+ var UpgradeRequiredError2 = class extends Error {
10669
+ currentVersion;
10670
+ minVersion;
10671
+ packageName;
10672
+ constructor(opts) {
10673
+ super(`The backend requires ${opts.packageName} ${opts.minVersion} or newer. You're running ${opts.currentVersion}. Upgrade: npm install -g ${opts.packageName}@latest`);
10674
+ this.name = "UpgradeRequiredError";
10675
+ this.currentVersion = opts.currentVersion;
10676
+ this.minVersion = opts.minVersion;
10677
+ this.packageName = opts.packageName;
10678
+ }
10679
+ };
10680
+ exports2.UpgradeRequiredError = UpgradeRequiredError2;
10681
+ function compareSemver(a, b) {
10682
+ const parse = (v) => {
10683
+ const core = v.replace(/^v/, "").split(/[-+]/, 1)[0];
10684
+ const parts = core.split(".").map((p) => Number.parseInt(p, 10));
10685
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
10686
+ };
10687
+ const [aMaj, aMin, aPat] = parse(a);
10688
+ const [bMaj, bMin, bPat] = parse(b);
10689
+ if (aMaj !== bMaj)
10690
+ return aMaj < bMaj ? -1 : 1;
10691
+ if (aMin !== bMin)
10692
+ return aMin < bMin ? -1 : 1;
10693
+ if (aPat !== bPat)
10694
+ return aPat < bPat ? -1 : 1;
10695
+ return 0;
10696
+ }
10697
+ function checkResponseMinVersion(opts) {
10698
+ const minVersion = opts.response.headers.get(exports2.MIN_VERSION_HEADER);
10699
+ if (!minVersion)
10700
+ return;
10701
+ if (compareSemver(opts.currentVersion, minVersion) >= 0)
10702
+ return;
10703
+ throw new UpgradeRequiredError2({
10704
+ currentVersion: opts.currentVersion,
10705
+ minVersion,
10706
+ packageName: opts.packageName
10707
+ });
10708
+ }
10709
+ function readResponseLatestVersion(response) {
10710
+ const latest = response.headers.get(exports2.LATEST_VERSION_HEADER);
10711
+ return latest && latest.trim().length > 0 ? latest.trim() : null;
10712
+ }
10713
+ }
10714
+ });
10715
+
10614
10716
  // ../../packages/shuttle/dist/executors/claude-code.js
10615
10717
  var require_claude_code = __commonJS({
10616
10718
  "../../packages/shuttle/dist/executors/claude-code.js"(exports2) {
@@ -10634,9 +10736,12 @@ var require_claude_code = __commonJS({
10634
10736
  var json_answer_1 = require_json_answer();
10635
10737
  var models_1 = require_models();
10636
10738
  var workspace_telemetry_1 = require_workspace_telemetry();
10739
+ var cli_version_1 = require_cli_version();
10637
10740
  var apply_1 = require_apply();
10741
+ var version_check_1 = require_version_check();
10638
10742
  var DEFAULT_TIMEOUT = 6e5;
10639
10743
  var REVIEW_TOOLS = ["Read", "Grep", "Glob"];
10744
+ var THINKING_DISPLAY_FLOOR = "2.1.94";
10640
10745
  function reviewRunArgs(settingSources) {
10641
10746
  return [
10642
10747
  "--setting-sources",
@@ -10891,13 +10996,17 @@ var require_claude_code = __commonJS({
10891
10996
  args.push("--model", model);
10892
10997
  if (effort)
10893
10998
  args.push("--effort", effort);
10999
+ const version = await (0, cli_version_1.cliVersion)("claude", childEnv);
11000
+ if (version !== null && (0, version_check_1.compareSemver)(version, THINKING_DISPLAY_FLOOR) >= 0) {
11001
+ args.push("--thinking-display", "summarized");
11002
+ }
10894
11003
  if (this.config.maxTurns !== void 0)
10895
11004
  args.push("--max-turns", String(this.config.maxTurns));
10896
11005
  if (allowedTools && allowedTools.length > 0) {
10897
11006
  args.push("--allowedTools", allowedTools.join(","));
10898
11007
  }
10899
11008
  const tap = reporter ? new progress_1.StreamJsonTap({
10900
- onReasoning: (t) => reporter.reasoning(t),
11009
+ onReasoning: (t) => reporter.reasoningBlock(t),
10901
11010
  onTool: (name, target) => reporter.tool(name, target),
10902
11011
  onResult: () => reporter.flush()
10903
11012
  }) : null;
@@ -11395,6 +11504,11 @@ var require_codex = __commonJS({
11395
11504
  'approval_policy="never"',
11396
11505
  "-c",
11397
11506
  'web_search="disabled"',
11507
+ // Reasoning summaries are the live view's narration. With the user config
11508
+ // ignored, codex 0.162.1 emits no reasoning items at all; `detailed`
11509
+ // brings them back without touching the answer.
11510
+ "-c",
11511
+ 'model_reasoning_summary="detailed"',
11398
11512
  ...DISABLED_FEATURES.flatMap((f) => ["--disable", f])
11399
11513
  ];
11400
11514
  }
@@ -11649,12 +11763,17 @@ var require_codex = __commonJS({
11649
11763
  if (event.type === "item.started" && kind === "command_execution" && typeof item?.command === "string") {
11650
11764
  reporter.tool("shell", item.command);
11651
11765
  } else if (event.type === "item.completed" && kind === "reasoning" && typeof item?.text === "string") {
11652
- reporter.reasoning(item.text);
11766
+ reporter.reasoningBlock(plainHeadings(item.text));
11767
+ } else if (event.type === "item.completed" && kind === "agent_message" && typeof item?.text === "string" && (0, json_answer_1.extractJsonObject)(item.text) === void 0) {
11768
+ reporter.reasoningBlock(item.text);
11653
11769
  } else if (event.type === "turn.completed") {
11654
11770
  reporter.flush();
11655
11771
  }
11656
11772
  });
11657
11773
  }
11774
+ function plainHeadings(text) {
11775
+ return text.replace(/^\*\*([^*\n]+)\*\*[ \t]*$/gm, "$1");
11776
+ }
11658
11777
  function itemKind(item) {
11659
11778
  const kind = typeof item.type === "string" ? item.type : typeof item.item_type === "string" ? item.item_type : null;
11660
11779
  return kind === "assistant_message" ? "agent_message" : kind;
@@ -12182,7 +12301,7 @@ ${JSON.stringify(spec.outputSchema)}
12182
12301
  const target = [args.path, args.pattern].find((v) => typeof v === "string");
12183
12302
  reporter.tool(data.toolName, target);
12184
12303
  } else if (event.type === "assistant.reasoning" && typeof data.content === "string") {
12185
- reporter.reasoning(data.content);
12304
+ reporter.reasoningBlock(data.content);
12186
12305
  } else if (event.type === "assistant.turn_end") {
12187
12306
  reporter.flush();
12188
12307
  }
@@ -13730,65 +13849,6 @@ var require_custom_script = __commonJS({
13730
13849
  }
13731
13850
  });
13732
13851
 
13733
- // ../../packages/shuttle/dist/version-check.js
13734
- var require_version_check = __commonJS({
13735
- "../../packages/shuttle/dist/version-check.js"(exports2) {
13736
- "use strict";
13737
- Object.defineProperty(exports2, "__esModule", { value: true });
13738
- exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = void 0;
13739
- exports2.compareSemver = compareSemver;
13740
- exports2.checkResponseMinVersion = checkResponseMinVersion;
13741
- exports2.readResponseLatestVersion = readResponseLatestVersion;
13742
- exports2.MIN_VERSION_HEADER = "x-sifter-min-version";
13743
- exports2.LATEST_VERSION_HEADER = "x-sifter-latest-version";
13744
- var UpgradeRequiredError2 = class extends Error {
13745
- currentVersion;
13746
- minVersion;
13747
- packageName;
13748
- constructor(opts) {
13749
- super(`The backend requires ${opts.packageName} ${opts.minVersion} or newer. You're running ${opts.currentVersion}. Upgrade: npm install -g ${opts.packageName}@latest`);
13750
- this.name = "UpgradeRequiredError";
13751
- this.currentVersion = opts.currentVersion;
13752
- this.minVersion = opts.minVersion;
13753
- this.packageName = opts.packageName;
13754
- }
13755
- };
13756
- exports2.UpgradeRequiredError = UpgradeRequiredError2;
13757
- function compareSemver(a, b) {
13758
- const parse = (v) => {
13759
- const core = v.replace(/^v/, "").split(/[-+]/, 1)[0];
13760
- const parts = core.split(".").map((p) => Number.parseInt(p, 10));
13761
- return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
13762
- };
13763
- const [aMaj, aMin, aPat] = parse(a);
13764
- const [bMaj, bMin, bPat] = parse(b);
13765
- if (aMaj !== bMaj)
13766
- return aMaj < bMaj ? -1 : 1;
13767
- if (aMin !== bMin)
13768
- return aMin < bMin ? -1 : 1;
13769
- if (aPat !== bPat)
13770
- return aPat < bPat ? -1 : 1;
13771
- return 0;
13772
- }
13773
- function checkResponseMinVersion(opts) {
13774
- const minVersion = opts.response.headers.get(exports2.MIN_VERSION_HEADER);
13775
- if (!minVersion)
13776
- return;
13777
- if (compareSemver(opts.currentVersion, minVersion) >= 0)
13778
- return;
13779
- throw new UpgradeRequiredError2({
13780
- currentVersion: opts.currentVersion,
13781
- minVersion,
13782
- packageName: opts.packageName
13783
- });
13784
- }
13785
- function readResponseLatestVersion(response) {
13786
- const latest = response.headers.get(exports2.LATEST_VERSION_HEADER);
13787
- return latest && latest.trim().length > 0 ? latest.trim() : null;
13788
- }
13789
- }
13790
- });
13791
-
13792
13852
  // ../../packages/shuttle/dist/branding/environment.js
13793
13853
  var require_environment = __commonJS({
13794
13854
  "../../packages/shuttle/dist/branding/environment.js"(exports2) {
@@ -22203,8 +22263,20 @@ var require_client2 = __commonJS({
22203
22263
  * callers should be rare. Returns the dispatched-job context the
22204
22264
  * subscriber needs to execute and submit.
22205
22265
  */
22206
- async acceptDispatch(jobId) {
22207
- return this.request("POST", `/api/jobs/${jobId}/attendance`);
22266
+ async acceptDispatch(jobId, options) {
22267
+ return this.request("POST", `/api/jobs/${jobId}/attendance`, options?.shiftId ? { shiftId: options.shiftId } : void 0);
22268
+ }
22269
+ // ── Shifts (whittlelabs ADR 0129) ────────────────────────────────
22270
+ /**
22271
+ * Start a shift for this process to claim on. A Jobs that predates shifts
22272
+ * answers 404; the subscribe loop then claims without one.
22273
+ */
22274
+ async startShift() {
22275
+ return this.request("POST", "/api/my/shifts");
22276
+ }
22277
+ /** End a shift, releasing every claim still taken on it back to the queue. */
22278
+ async endShift(shiftId) {
22279
+ await this.request("DELETE", `/api/my/shifts/${shiftId}`);
22208
22280
  }
22209
22281
  async submit(jobId, options) {
22210
22282
  return this.request("PUT", `/api/jobs/${jobId}/attendance`, options);
@@ -22226,12 +22298,15 @@ var require_client2 = __commonJS({
22226
22298
  params.set("offset", String(options.offset));
22227
22299
  if (options?.sort)
22228
22300
  params.set("sort", options.sort);
22301
+ if (options?.shift)
22302
+ params.set("shift", options.shift);
22229
22303
  const qs = params.toString();
22230
- const { jobs, total, directives } = await this.request("GET", `/api/my/jobs${qs ? `?${qs}` : ""}`);
22304
+ const { jobs, total, directives, shift } = await this.request("GET", `/api/my/jobs${qs ? `?${qs}` : ""}`);
22231
22305
  return {
22232
22306
  data: jobs,
22233
22307
  pagination: { total, limit: options?.limit ?? 25, offset: options?.offset ?? 0 },
22234
- directives: directives ?? null
22308
+ directives: directives ?? null,
22309
+ shift: shift ?? null
22235
22310
  };
22236
22311
  }
22237
22312
  // ── HTTP plumbing ────────────────────────────────────────────────
@@ -22322,6 +22397,17 @@ var require_subscribe = __commonJS({
22322
22397
  const rest = seconds % 60;
22323
22398
  return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
22324
22399
  }
22400
+ function statusOf(err) {
22401
+ const code = err?.statusCode;
22402
+ return typeof code === "number" ? code : void 0;
22403
+ }
22404
+ var HandBack = class {
22405
+ reason;
22406
+ constructor(reason) {
22407
+ this.reason = reason;
22408
+ }
22409
+ };
22410
+ var RELEASED = Symbol("released");
22325
22411
  function newOutcome() {
22326
22412
  let resolve;
22327
22413
  let reject;
@@ -22362,7 +22448,10 @@ var require_subscribe = __commonJS({
22362
22448
  let pollTimer = null;
22363
22449
  let inflight = 0;
22364
22450
  let resolvedPoolIds = [];
22365
- const controllers = /* @__PURE__ */ new Set();
22451
+ const controllers = /* @__PURE__ */ new Map();
22452
+ let shift = null;
22453
+ let shiftsSupported = true;
22454
+ let draining = null;
22366
22455
  const holds = /* @__PURE__ */ new Set();
22367
22456
  let fatalError = null;
22368
22457
  let outcome = newOutcome();
@@ -22491,13 +22580,74 @@ var require_subscribe = __commonJS({
22491
22580
  }
22492
22581
  if (stopped || mySession !== session)
22493
22582
  return;
22494
- logger.info("subscribe loop started", { pools: resolvedPoolIds, maxConcurrency });
22583
+ try {
22584
+ const opened = await untilAvailable(() => startShift(), mySession);
22585
+ if (opened === STOPPED)
22586
+ return;
22587
+ } catch (err) {
22588
+ if (mySession === session) {
22589
+ stopped = true;
22590
+ outcome.reject(err);
22591
+ }
22592
+ throw err;
22593
+ }
22594
+ if (stopped || mySession !== session)
22595
+ return;
22596
+ logger.info("subscribe loop started", {
22597
+ pools: resolvedPoolIds,
22598
+ maxConcurrency,
22599
+ ...shift ? { shiftId: shift.id } : {}
22600
+ });
22495
22601
  schedulePoll(pollIntervalMs);
22496
22602
  }
22603
+ async function startShift() {
22604
+ if (!shiftsSupported)
22605
+ return;
22606
+ try {
22607
+ shift = await opts.jobsClient.startShift();
22608
+ } catch (err) {
22609
+ if (isFatal(err) || isUnavailable(err) || isRateLimited(err))
22610
+ throw err;
22611
+ if (statusOf(err) === 404) {
22612
+ shiftsSupported = false;
22613
+ logger.debug?.("Jobs predates shifts; claims are held until each job's deadline");
22614
+ return;
22615
+ }
22616
+ logger.warn(`Could not start a shift (${describeOutage(err)}). Claims taken meanwhile are held until each job's deadline; the next poll tries again.`);
22617
+ }
22618
+ }
22619
+ async function endShift() {
22620
+ const ending = shift;
22621
+ shift = null;
22622
+ if (!ending)
22623
+ return;
22624
+ try {
22625
+ await opts.jobsClient.endShift(ending.id);
22626
+ } catch (err) {
22627
+ logger.debug?.("could not end the shift; it will lapse", {
22628
+ shiftId: ending.id,
22629
+ error: err instanceof Error ? err.message : String(err)
22630
+ });
22631
+ }
22632
+ }
22633
+ async function shiftReleased(ended) {
22634
+ const mine = [...controllers].filter(([, held]) => held.shiftId === ended.id);
22635
+ logger.warn(`Jobs ended this Sifter's shift (${ended.endReason ?? "ended"}) and released its claims` + (mine.length > 0 ? `; ${mine.length} job(s) running on it were handed back to the queue and stop here` : "") + ". Starting a new shift.", { shiftId: ended.id });
22636
+ for (const [controller] of mine)
22637
+ controller.abort(RELEASED);
22638
+ shift = null;
22639
+ try {
22640
+ await startShift();
22641
+ } catch (err) {
22642
+ if (isFatal(err))
22643
+ throw err;
22644
+ }
22645
+ }
22497
22646
  async function finish() {
22498
22647
  while (inflight > 0) {
22499
22648
  await sleep(100);
22500
22649
  }
22650
+ await endShift();
22501
22651
  logger.info("subscribe loop stopped");
22502
22652
  if (fatalError !== null)
22503
22653
  outcome.reject(fatalError.error);
@@ -22512,45 +22662,61 @@ var require_subscribe = __commonJS({
22512
22662
  }
22513
22663
  wakeStartup?.();
22514
22664
  }
22515
- async function stop() {
22665
+ async function stop(options = {}) {
22666
+ if (options.handBack !== void 0) {
22667
+ for (const [controller] of controllers)
22668
+ controller.abort(new HandBack(options.handBack));
22669
+ }
22670
+ if (draining)
22671
+ return draining;
22516
22672
  if (stopped)
22517
22673
  return;
22518
22674
  halt();
22519
- await finish();
22675
+ draining = finish().finally(() => {
22676
+ draining = null;
22677
+ });
22678
+ return draining;
22520
22679
  }
22521
22680
  async function fail(err) {
22522
22681
  if (fatalError === null)
22523
22682
  fatalError = { error: err };
22524
- for (const controller of controllers)
22683
+ for (const [controller] of controllers)
22525
22684
  controller.abort(err);
22685
+ if (draining)
22686
+ return draining;
22526
22687
  if (stopped)
22527
22688
  return;
22528
22689
  halt();
22529
- await finish();
22690
+ draining = finish().finally(() => {
22691
+ draining = null;
22692
+ });
22693
+ return draining;
22530
22694
  }
22531
22695
  function schedulePoll(delayMs) {
22532
22696
  const mySession = session;
22697
+ const checkInMs = shift ? shift.lapseAfterSeconds * 1e3 / 4 : Infinity;
22533
22698
  pollTimer = setTimeout(() => {
22534
22699
  void poll(mySession);
22535
- }, delayMs);
22700
+ }, Math.min(delayMs, checkInMs));
22536
22701
  }
22537
22702
  async function poll(mySession) {
22538
22703
  if (stopped || mySession !== session)
22539
22704
  return;
22540
22705
  let nextPollMs = pollIntervalMs;
22541
- if (inflight >= maxConcurrency) {
22542
- schedulePoll(pollIntervalMs);
22543
- return;
22544
- }
22545
22706
  try {
22546
22707
  const paused = holds.size > 0;
22547
22708
  const free = maxConcurrency - inflight;
22548
- const { data, directives } = await opts.jobsClient.listDispatchableJobs({
22709
+ if (shift === null && shiftsSupported)
22710
+ await startShift();
22711
+ const { data, directives, shift: standing } = await opts.jobsClient.listDispatchableJobs({
22549
22712
  pools: resolvedPoolIds,
22550
- limit: paused ? 1 : accepts ? Math.max(free, DECLINE_WINDOW) : free,
22551
- sort: "priority:desc,queued_at:asc"
22713
+ limit: paused || free <= 0 ? 1 : accepts ? Math.max(free, DECLINE_WINDOW) : free,
22714
+ sort: "priority:desc,queued_at:asc",
22715
+ ...shift ? { shift: shift.id } : {}
22552
22716
  });
22553
22717
  noteReachable();
22718
+ if (shift && standing?.id === shift.id && standing.endedAt !== null)
22719
+ await shiftReleased(standing);
22554
22720
  if (directives && Object.keys(directives).length > 0 && opts.onDirectives) {
22555
22721
  try {
22556
22722
  await opts.onDirectives(directives);
@@ -22598,8 +22764,9 @@ var require_subscribe = __commonJS({
22598
22764
  }
22599
22765
  async function runOnce(available) {
22600
22766
  let dispatch;
22767
+ const shiftId = shift?.id ?? null;
22601
22768
  try {
22602
- dispatch = await opts.jobsClient.acceptDispatch(available.id);
22769
+ dispatch = await opts.jobsClient.acceptDispatch(available.id, shiftId ? { shiftId } : void 0);
22603
22770
  } catch (err) {
22604
22771
  if (isFatal(err)) {
22605
22772
  void fail(err);
@@ -22616,8 +22783,13 @@ var require_subscribe = __commonJS({
22616
22783
  });
22617
22784
  return;
22618
22785
  }
22786
+ logger.info(`Claimed job ${dispatch.job.id} (attempt ${dispatch.job.attempt})`, {
22787
+ jobId: dispatch.job.id,
22788
+ pool: dispatch.pool.name,
22789
+ ...shiftId ? { shiftId } : {}
22790
+ });
22619
22791
  const controller = new AbortController();
22620
- controllers.add(controller);
22792
+ controllers.set(controller, { shiftId });
22621
22793
  if (fatalError !== null)
22622
22794
  controller.abort(fatalError.error);
22623
22795
  let quietTimer = null;
@@ -22657,9 +22829,20 @@ var require_subscribe = __commonJS({
22657
22829
  controllers.delete(controller);
22658
22830
  }
22659
22831
  if (fatalError !== null) {
22660
- logger.debug?.("not submitting a result after a fatal error", { jobId: dispatch.job.id });
22832
+ logger.error(shiftId ? `Dropping job ${dispatch.job.id}'s result after a fatal error. Its claim goes back to the queue when this Sifter's shift lapses.` : `Dropping job ${dispatch.job.id}'s result after a fatal error. Its claim is held until the job's deadline.`, { jobId: dispatch.job.id, status: result.status });
22833
+ return;
22834
+ }
22835
+ if (controller.signal.reason === RELEASED) {
22836
+ logger.warn(`Job ${dispatch.job.id} was handed back to the queue while it ran; its result is not submitted.`, {
22837
+ jobId: dispatch.job.id
22838
+ });
22661
22839
  return;
22662
22840
  }
22841
+ const handBack = controller.signal.reason instanceof HandBack ? controller.signal.reason.reason : null;
22842
+ if (handBack !== null && result.status !== "completed") {
22843
+ result = { status: "abandoned", reason: handBack };
22844
+ logger.warn(`Handing job ${dispatch.job.id} back to the queue: ${handBack}`, { jobId: dispatch.job.id });
22845
+ }
22663
22846
  try {
22664
22847
  for (const output of result.outputs ?? []) {
22665
22848
  await opts.jobsClient.createOutput(dispatch.job.id, {
@@ -22681,7 +22864,12 @@ var require_subscribe = __commonJS({
22681
22864
  jobId: dispatch.job.id,
22682
22865
  error: err instanceof Error ? err.message : String(err)
22683
22866
  });
22867
+ return;
22684
22868
  }
22869
+ logger.info(`Submitted job ${dispatch.job.id} as ${result.status}`, {
22870
+ jobId: dispatch.job.id,
22871
+ ...result.reason !== void 0 ? { reason: result.reason } : {}
22872
+ });
22685
22873
  }
22686
22874
  return {
22687
22875
  start,
@@ -26322,9 +26510,12 @@ var require_logger = __commonJS({
26322
26510
  "../../packages/shuttle/dist/logging/logger.js"(exports2) {
26323
26511
  "use strict";
26324
26512
  Object.defineProperty(exports2, "__esModule", { value: true });
26513
+ exports2.LOG_FILE_MAX_BYTES = void 0;
26325
26514
  exports2.createLogger = createLogger;
26326
26515
  exports2.setGlobalLogger = setGlobalLogger;
26327
26516
  exports2.getLogger = getLogger;
26517
+ var node_fs_1 = require("node:fs");
26518
+ var node_path_1 = require("node:path");
26328
26519
  var LEVEL_ORDER = {
26329
26520
  debug: 0,
26330
26521
  info: 1,
@@ -26352,13 +26543,42 @@ var require_logger = __commonJS({
26352
26543
  };
26353
26544
  return JSON.stringify(entry);
26354
26545
  }
26546
+ exports2.LOG_FILE_MAX_BYTES = 5 * 1024 * 1024;
26547
+ function fileSink(path) {
26548
+ let size = 0;
26549
+ let broken = false;
26550
+ try {
26551
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
26552
+ size = (0, node_fs_1.statSync)(path).size;
26553
+ } catch {
26554
+ size = 0;
26555
+ }
26556
+ return (line) => {
26557
+ if (broken)
26558
+ return;
26559
+ try {
26560
+ if (size > exports2.LOG_FILE_MAX_BYTES) {
26561
+ (0, node_fs_1.renameSync)(path, `${path}.1`);
26562
+ size = 0;
26563
+ }
26564
+ const entry = `${line}
26565
+ `;
26566
+ (0, node_fs_1.appendFileSync)(path, entry, { mode: 384 });
26567
+ size += Buffer.byteLength(entry);
26568
+ } catch {
26569
+ broken = true;
26570
+ }
26571
+ };
26572
+ }
26355
26573
  function createLogger(options) {
26356
26574
  const threshold = LEVEL_ORDER[options.level];
26357
26575
  const format = options.format === "json" ? formatJson : formatPretty;
26576
+ const toFile = options.file ? fileSink(options.file) : null;
26358
26577
  function emit(level, message, context) {
26359
26578
  if (LEVEL_ORDER[level] < threshold)
26360
26579
  return;
26361
26580
  const line = format(level, message, context);
26581
+ toFile?.(line);
26362
26582
  if (level === "error") {
26363
26583
  console.error(line);
26364
26584
  } else if (level === "warn") {
@@ -27654,6 +27874,7 @@ var require_shuttle = __commonJS({
27654
27874
  exports2.mergeExecutorDefaults = mergeExecutorDefaults;
27655
27875
  exports2.resolveAuth = resolveAuth;
27656
27876
  var os_1 = require("os");
27877
+ var path_1 = require("path");
27657
27878
  var jobs_1 = require_dist4();
27658
27879
  var self_update_1 = require_self_update();
27659
27880
  var decommission_1 = require_decommission();
@@ -27794,13 +28015,18 @@ var require_shuttle = __commonJS({
27794
28015
  return this.spendTracker;
27795
28016
  }
27796
28017
  async start() {
28018
+ const paths = (0, apply_1.resolvePaths)(this.brand, this.profile);
28019
+ const logFile = this.config.credentials === void 0 ? (0, path_1.join)(paths.configDir, `${this.brand.product.id}.log`) : void 0;
27797
28020
  (0, logger_1.setGlobalLogger)((0, logger_1.createLogger)({
27798
28021
  level: this.config.logging.level,
27799
- format: this.config.logging.format
28022
+ format: this.config.logging.format,
28023
+ ...logFile ? { file: logFile } : {}
27800
28024
  }));
27801
28025
  const logger = (0, logger_1.getLogger)();
27802
- logger.info(`${this.brand.product.title} starting...`);
27803
- const paths = (0, apply_1.resolvePaths)(this.brand, this.profile);
28026
+ logger.info(`${this.brand.product.title} starting...`, {
28027
+ version: this.brand.product.version,
28028
+ ...logFile ? { log: logFile } : {}
28029
+ });
27804
28030
  this.broker = (0, credentials_1.createCredentialBroker)(this.config.credentials, this.credentialDeps);
27805
28031
  const subscribeLogger = {
27806
28032
  debug: (msg, meta) => logger.debug(msg, meta),
@@ -27938,7 +28164,7 @@ var require_shuttle = __commonJS({
27938
28164
  const runOn = async (executor) => {
27939
28165
  await observerChain.notify({ kind: "executor.dispatch.started", jobId, attendanceId, executor: executor.capability.id, strategy }, onObserverError);
27940
28166
  const dispatchStartedAt = Date.now();
27941
- const outcome = await (0, run_dispatch_1.runDispatch)(dispatch, executor, {
28167
+ const ran = await (0, run_dispatch_1.runDispatch)(dispatch, executor, {
27942
28168
  credentials: this.broker,
27943
28169
  signal,
27944
28170
  reportProgress: (channel, frames) => {
@@ -27946,6 +28172,8 @@ var require_shuttle = __commonJS({
27946
28172
  });
27947
28173
  }
27948
28174
  });
28175
+ subscribeLogger.info(`Job ${jobId} ran on ${executor.capability.id} for ${formatDuration(Date.now() - dispatchStartedAt)}: ` + (ran.kind === "completed" ? "finished" : ran.kind === "gate-failed" ? "its output failed a gate" : "the executor failed"), { jobId, ...signal.aborted ? { aborted: true } : {} });
28176
+ const outcome = ran;
27949
28177
  if (outcome.kind === "executor-failed") {
27950
28178
  const err = outcome.error;
27951
28179
  const reason = `executor failed: ${err instanceof Error ? err.message : String(err)}`;
@@ -28105,14 +28333,21 @@ var require_shuttle = __commonJS({
28105
28333
  logger.info(`${this.brand.product.title} stopping...`);
28106
28334
  this.stopping.abort();
28107
28335
  if (this.subscription) {
28108
- await this.subscription.stop();
28336
+ await this.subscription.stop({
28337
+ handBack: `The ${this.brand.product.title} was stopped before it finished this job`
28338
+ });
28109
28339
  }
28110
28340
  logger.info(`${this.brand.product.title} stopped`);
28111
28341
  this.lifetime.resolve();
28112
28342
  }
28113
28343
  /** Forward the current subscribe session's fatal, if one ends it, to the run's outcome. */
28114
28344
  watchSession() {
28115
- this.subscription?.done().catch((err) => this.lifetime.reject(err));
28345
+ this.subscription?.done().catch((err) => {
28346
+ (0, logger_1.getLogger)().error(`${this.brand.product.title} stopped on a fatal error`, {
28347
+ error: err instanceof Error ? `${err.name}: ${err.message}` : String(err)
28348
+ });
28349
+ this.lifetime.reject(err);
28350
+ });
28116
28351
  }
28117
28352
  /**
28118
28353
  * Note that an executor cannot run jobs on this machine, and keep checking
@@ -28276,29 +28511,49 @@ var require_shutdown = __commonJS({
28276
28511
  "../../packages/shuttle/dist/lifecycle/shutdown.js"(exports2) {
28277
28512
  "use strict";
28278
28513
  Object.defineProperty(exports2, "__esModule", { value: true });
28514
+ exports2.SHUTDOWN_DEADLINE_MS = void 0;
28279
28515
  exports2.setupShutdownHandler = setupShutdownHandler;
28516
+ exports2.createShutdownHandler = createShutdownHandler;
28280
28517
  var logger_1 = require_logger();
28518
+ exports2.SHUTDOWN_DEADLINE_MS = 2e4;
28281
28519
  function setupShutdownHandler(onShutdown) {
28520
+ const handler = createShutdownHandler(onShutdown);
28521
+ process.on("SIGINT", () => handler("SIGINT"));
28522
+ process.on("SIGTERM", () => handler("SIGTERM"));
28523
+ process.on("SIGHUP", () => handler("SIGHUP"));
28524
+ }
28525
+ function createShutdownHandler(onShutdown, options = {}) {
28526
+ const deadlineMs = options.deadlineMs ?? exports2.SHUTDOWN_DEADLINE_MS;
28527
+ const exit = options.exit ?? ((code) => process.exit(code));
28282
28528
  let shuttingDown = false;
28283
28529
  const handler = (signal) => {
28284
28530
  const logger = (0, logger_1.getLogger)();
28285
28531
  if (shuttingDown) {
28286
- logger.warn("Forced shutdown", { signal });
28287
- process.exit(1);
28532
+ logger.warn("Forced shutdown: exiting now. Jobs releases any claim still held once this shift lapses.", {
28533
+ signal
28534
+ });
28535
+ exit(1);
28536
+ return;
28288
28537
  }
28289
28538
  shuttingDown = true;
28290
- logger.info("Shutting down...", { signal });
28539
+ logger.info("Shutting down: handing any job in progress back to the queue...", { signal });
28540
+ const deadline = setTimeout(() => {
28541
+ logger.warn(`Shutdown did not finish within ${Math.round(deadlineMs / 1e3)}s; exiting. Jobs releases any claim still held once this shift lapses.`);
28542
+ exit(1);
28543
+ }, deadlineMs);
28544
+ deadline.unref?.();
28291
28545
  onShutdown().then(() => {
28292
- process.exit(0);
28546
+ clearTimeout(deadline);
28547
+ exit(0);
28293
28548
  }).catch((err) => {
28549
+ clearTimeout(deadline);
28294
28550
  logger.error("Shutdown error", {
28295
28551
  error: err instanceof Error ? err.message : String(err)
28296
28552
  });
28297
- process.exit(1);
28553
+ exit(1);
28298
28554
  });
28299
28555
  };
28300
- process.on("SIGINT", () => handler("SIGINT"));
28301
- process.on("SIGTERM", () => handler("SIGTERM"));
28556
+ return handler;
28302
28557
  }
28303
28558
  }
28304
28559
  });
@@ -29132,7 +29387,7 @@ var import_path = require("path");
29132
29387
  var import_promises = require("fs/promises");
29133
29388
  var import_yaml = __toESM(require_dist3());
29134
29389
  var import_shuttle = __toESM(require_dist5());
29135
- var buildVersion = true ? "0.35.0" : pkg.version;
29390
+ var buildVersion = true ? "0.37.0" : pkg.version;
29136
29391
  var AGENT_EXECUTORS = ["claude-code", "codex", "copilot"];
29137
29392
  var sifterBrand = {
29138
29393
  product: {