@mestreyoda/fabrica 0.2.3 → 0.2.6

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/dist/index.js CHANGED
@@ -110821,7 +110821,7 @@ var init_registry = __esm({
110821
110821
  });
110822
110822
 
110823
110823
  // lib/config/merge.ts
110824
- function mergeConfig(base, overlay) {
110824
+ function mergeConfig(base, overlay, traceOpts) {
110825
110825
  const merged = {};
110826
110826
  if (base.roles || overlay.roles) {
110827
110827
  merged.roles = { ...base.roles };
@@ -110872,6 +110872,44 @@ function mergeConfig(base, overlay) {
110872
110872
  } : void 0
110873
110873
  };
110874
110874
  }
110875
+ if (traceOpts) {
110876
+ const { baseLabel, overlayLabel } = traceOpts;
110877
+ const trace2 = {};
110878
+ if (merged.workflow) {
110879
+ for (const key of ["initial", "reviewPolicy", "testPolicy", "roleExecution", "maxWorkersPerLevel"]) {
110880
+ if (merged.workflow[key] !== void 0) {
110881
+ const fromOverlay = overlay.workflow?.[key] !== void 0;
110882
+ trace2[`workflow.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110883
+ }
110884
+ }
110885
+ }
110886
+ if (merged.timeouts) {
110887
+ for (const [key, value] of Object.entries(merged.timeouts)) {
110888
+ if (value !== void 0) {
110889
+ const fromOverlay = overlay.timeouts?.[key] !== void 0;
110890
+ trace2[`timeouts.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110891
+ }
110892
+ }
110893
+ }
110894
+ if (merged.roles) {
110895
+ for (const [roleId, roleValue] of Object.entries(merged.roles)) {
110896
+ if (roleValue === false) {
110897
+ trace2[`roles.${roleId}`] = overlay.roles?.[roleId] === false ? overlayLabel : baseLabel;
110898
+ continue;
110899
+ }
110900
+ if (typeof roleValue === "object") {
110901
+ for (const key of ["defaultLevel", "levels", "completionResults"]) {
110902
+ if (roleValue[key] !== void 0) {
110903
+ const overlayRole = overlay.roles?.[roleId];
110904
+ const fromOverlay = typeof overlayRole === "object" && overlayRole?.[key] !== void 0;
110905
+ trace2[`roles.${roleId}.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110906
+ }
110907
+ }
110908
+ }
110909
+ }
110910
+ }
110911
+ return Object.assign(merged, { _trace: trace2 });
110912
+ }
110875
110913
  return merged;
110876
110914
  }
110877
110915
  function mergeWorkflowStates(base, overlay) {
@@ -110917,42 +110955,10 @@ function mergeRoleOverride(base, overlay) {
110917
110955
  };
110918
110956
  }
110919
110957
  function mergeConfigWithTrace(base, overlay, baseLabel, overlayLabel) {
110920
- const merged = mergeConfig(base, overlay);
110921
- const trace2 = {};
110922
- if (merged.workflow) {
110923
- for (const key of ["initial", "reviewPolicy", "testPolicy", "roleExecution", "maxWorkersPerLevel"]) {
110924
- if (merged.workflow[key] !== void 0) {
110925
- const fromOverlay = overlay.workflow?.[key] !== void 0;
110926
- trace2[`workflow.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110927
- }
110928
- }
110929
- }
110930
- if (merged.timeouts) {
110931
- for (const [key, value] of Object.entries(merged.timeouts)) {
110932
- if (value !== void 0) {
110933
- const fromOverlay = overlay.timeouts?.[key] !== void 0;
110934
- trace2[`timeouts.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110935
- }
110936
- }
110937
- }
110938
- if (merged.roles) {
110939
- for (const [roleId, roleValue] of Object.entries(merged.roles)) {
110940
- if (roleValue === false) {
110941
- trace2[`roles.${roleId}`] = overlay.roles?.[roleId] === false ? overlayLabel : baseLabel;
110942
- continue;
110943
- }
110944
- if (typeof roleValue === "object") {
110945
- for (const key of ["defaultLevel", "levels", "completionResults"]) {
110946
- if (roleValue[key] !== void 0) {
110947
- const overlayRole = overlay.roles?.[roleId];
110948
- const fromOverlay = typeof overlayRole === "object" && overlayRole?.[key] !== void 0;
110949
- trace2[`roles.${roleId}.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110950
- }
110951
- }
110952
- }
110953
- }
110954
- }
110955
- return { merged, trace: trace2 };
110958
+ const result = mergeConfig(base, overlay, { baseLabel, overlayLabel });
110959
+ const trace2 = result._trace ?? {};
110960
+ delete result._trace;
110961
+ return { merged: result, trace: trace2 };
110956
110962
  }
110957
110963
  var init_merge = __esm({
110958
110964
  "lib/config/merge.ts"() {
@@ -111324,8 +111330,8 @@ import fsSync from "node:fs";
111324
111330
  import path5 from "node:path";
111325
111331
  import { fileURLToPath as fileURLToPath3 } from "node:url";
111326
111332
  function getCurrentVersion() {
111327
- if ("0.2.3") {
111328
- return "0.2.3";
111333
+ if ("0.2.6") {
111334
+ return "0.2.6";
111329
111335
  }
111330
111336
  try {
111331
111337
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -117132,6 +117138,8 @@ var GitHubRateLimitError = class extends Error {
117132
117138
  this.name = "GitHubRateLimitError";
117133
117139
  }
117134
117140
  };
117141
+ var MAX_RATE_LIMIT_RETRIES = 2;
117142
+ var JITTER_MAX_MS = 5e3;
117135
117143
  var MAX_ENTRIES = 50;
117136
117144
  var policyCache = /* @__PURE__ */ new Map();
117137
117145
  var accessOrder = [];
@@ -117167,9 +117175,27 @@ function getProviderPolicy(providerKey) {
117167
117175
  accessOrder.push(providerKey);
117168
117176
  return policy;
117169
117177
  }
117170
- function withResilience(fn, providerKey) {
117178
+ async function withResilience(fn, providerKey, opts) {
117171
117179
  const policy = providerKey ? getProviderPolicy(providerKey) : getProviderPolicy("__global__");
117172
- return policy.execute(() => fn());
117180
+ const jitterMax = opts?.jitterMaxMs ?? JITTER_MAX_MS;
117181
+ const withRateLimitRetry = async () => {
117182
+ let lastError;
117183
+ for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {
117184
+ try {
117185
+ return await fn();
117186
+ } catch (err) {
117187
+ if (err instanceof GitHubRateLimitError && attempt < MAX_RATE_LIMIT_RETRIES) {
117188
+ lastError = err;
117189
+ const jitter = jitterMax > 0 ? Math.floor(Math.random() * jitterMax) : 0;
117190
+ await new Promise((resolve3) => setTimeout(resolve3, err.retryAfterMs + jitter));
117191
+ continue;
117192
+ }
117193
+ throw err;
117194
+ }
117195
+ }
117196
+ throw lastError;
117197
+ };
117198
+ return policy.execute(withRateLimitRetry);
117173
117199
  }
117174
117200
  var providerPolicy = getProviderPolicy("__legacy_global__");
117175
117201
 
@@ -118078,7 +118104,9 @@ var GitHubProvider = class {
118078
118104
  if (result.code != null && result.code !== 0) {
118079
118105
  const errText = result.stderr?.trim() ?? "";
118080
118106
  if (errText.includes("rate limit") || errText.includes("429")) {
118081
- throw new GitHubRateLimitError(6e4);
118107
+ const retryMatch = errText.match(/retry after (\d+)/i);
118108
+ const retryMs = retryMatch ? parseInt(retryMatch[1], 10) * 1e3 : 6e4;
118109
+ throw new GitHubRateLimitError(retryMs);
118082
118110
  }
118083
118111
  throw new Error(errText || `gh api ${method} ${endpoint} failed with exit code ${result.code}`);
118084
118112
  }
@@ -122967,7 +122995,8 @@ ${roleInstructions}`;
122967
122995
  return effortPrefix ?? roleInstructions ?? "";
122968
122996
  }
122969
122997
  function sendToAgent(sessionKey, taskMessage, opts) {
122970
- const idempotencyKey = `fabrica-${opts.projectName}-${opts.issueId}-${opts.role}-${opts.level ?? "unknown"}-${opts.slotIndex ?? 0}-${opts.fromLabel ?? "unknown"}-${sessionKey}`;
122998
+ const epoch = opts.dispatchEpoch ?? (/* @__PURE__ */ new Date()).toISOString();
122999
+ const idempotencyKey = `fabrica-${opts.projectName}-${opts.issueId}-${opts.role}-${opts.level ?? "unknown"}-${opts.slotIndex ?? 0}-${opts.fromLabel ?? "unknown"}-${sessionKey}-${epoch}`;
122971
123000
  if (opts.runtime?.subagent?.run) {
122972
123001
  opts.runtime.subagent.run({
122973
123002
  sessionKey,
@@ -123388,6 +123417,7 @@ async function dispatchTask(opts) {
123388
123417
  error: err.message ?? String(err)
123389
123418
  }).catch((auditErr) => console.error("[fabrica] silent-catch:", auditErr.message));
123390
123419
  });
123420
+ const dispatchEpoch = (/* @__PURE__ */ new Date()).toISOString();
123391
123421
  sendToAgent(sessionKey, taskMessage, {
123392
123422
  agentId,
123393
123423
  projectName: project.name,
@@ -123405,7 +123435,8 @@ async function dispatchTask(opts) {
123405
123435
  roleInstructions.trim() || void 0
123406
123436
  ) || void 0,
123407
123437
  runCommand: rc,
123408
- runtime
123438
+ runtime,
123439
+ dispatchEpoch
123409
123440
  });
123410
123441
  await recordIssueLifecycle({
123411
123442
  workspaceDir,
@@ -130457,7 +130488,10 @@ async function cleanupExpired(workspaceDir, ttlMs = DEFAULT_TTL_MS2) {
130457
130488
  const cutoff = Date.now() - ttlMs;
130458
130489
  const kept = entries.filter((e2) => e2.ts >= cutoff);
130459
130490
  if (kept.length < entries.length) {
130460
- await fs28.writeFile(filePath, kept.map((e2) => JSON.stringify(e2)).join("\n") + (kept.length > 0 ? "\n" : ""), "utf-8");
130491
+ const content = kept.map((e2) => JSON.stringify(e2)).join("\n") + (kept.length > 0 ? "\n" : "");
130492
+ const tmpPath = filePath + ".tmp";
130493
+ await fs28.writeFile(tmpPath, content, "utf-8");
130494
+ await fs28.rename(tmpPath, filePath);
130461
130495
  }
130462
130496
  }
130463
130497
 
@@ -132206,23 +132240,46 @@ async function checkProjectActive(workspaceDir, slug) {
132206
132240
  );
132207
132241
  }
132208
132242
 
132243
+ // lib/services/heartbeat/wake-bridge.ts
132244
+ var _wakeCallback = null;
132245
+ function setPluginWakeHandler(cb) {
132246
+ _wakeCallback = cb;
132247
+ }
132248
+ async function wakeHeartbeat(reason) {
132249
+ await _wakeCallback?.(reason);
132250
+ }
132251
+
132209
132252
  // lib/services/heartbeat/index.ts
132210
132253
  function registerHeartbeatService(api, pluginCtx) {
132211
132254
  let sharedIntervalId = null;
132212
- let tickCount = 0;
132255
+ let _pendingWake = false;
132213
132256
  api.registerService({
132214
132257
  id: "fabrica-heartbeat",
132215
132258
  start: async (svcCtx) => {
132216
132259
  const { intervalSeconds } = resolveHeartbeatConfig(pluginCtx.pluginConfig);
132217
132260
  const intervalMs = intervalSeconds * 1e3;
132218
- setTimeout(() => runHeartbeatTick(pluginCtx, svcCtx.logger, "repair"), 2e3);
132261
+ setTimeout(() => runHeartbeatTick(pluginCtx, svcCtx.logger, "full"), 2e3);
132219
132262
  sharedIntervalId = setInterval(() => {
132220
- const mode = tickCount % 2 === 0 ? "repair" : "triage";
132221
- tickCount++;
132222
- runHeartbeatTick(pluginCtx, svcCtx.logger, mode);
132263
+ runHeartbeatTick(pluginCtx, svcCtx.logger, "full").finally(() => {
132264
+ if (_pendingWake) {
132265
+ _pendingWake = false;
132266
+ svcCtx.logger.info("heartbeat_wake: running deferred full tick");
132267
+ runHeartbeatTick(pluginCtx, svcCtx.logger, "full");
132268
+ }
132269
+ });
132223
132270
  }, intervalMs);
132271
+ setPluginWakeHandler(async (reason) => {
132272
+ if (_anyTickRunning) {
132273
+ _pendingWake = true;
132274
+ svcCtx.logger.info(`heartbeat_wake: deferred (tick-in-progress), reason=${reason}`);
132275
+ return;
132276
+ }
132277
+ svcCtx.logger.info(`heartbeat_wake: running full tick, reason=${reason}`);
132278
+ await runHeartbeatTick(pluginCtx, svcCtx.logger, "full");
132279
+ });
132224
132280
  },
132225
132281
  stop: async (svcCtx) => {
132282
+ setPluginWakeHandler(null);
132226
132283
  if (sharedIntervalId) {
132227
132284
  clearInterval(sharedIntervalId);
132228
132285
  sharedIntervalId = null;
@@ -139108,7 +139165,7 @@ function registerTelegramBootstrapHook(api, ctx) {
139108
139165
  ctx.logger.info(`[telegram-bootstrap] stale received session (expired) \u2014 restarting pipeline for conversation ${conversationId}`);
139109
139166
  }
139110
139167
  }
139111
- if (!parsed.projectName && ctx.runtime?.subagent?.run) {
139168
+ if (!parsed.projectName && ctx.runtime?.subagent?.run != null) {
139112
139169
  await upsertTelegramBootstrapSession(workspaceDir, {
139113
139170
  conversationId,
139114
139171
  rawIdea: parsed.rawIdea,
@@ -139484,6 +139541,8 @@ function registerReactiveDispatchHooks(api, ctx) {
139484
139541
  api.on("after_tool_call", async (event, _eventCtx) => {
139485
139542
  if (!COMPLETION_TOOLS.has(event.toolName)) return;
139486
139543
  ctx.runtime?.system.requestHeartbeatNow({ reason: "work_finish", coalesceMs: 2e3 });
139544
+ wakeHeartbeat("work_finish").catch(() => {
139545
+ });
139487
139546
  });
139488
139547
  api.on("agent_end", async (_event, eventCtx) => {
139489
139548
  const sessionKey = eventCtx.sessionKey;
@@ -139491,6 +139550,8 @@ function registerReactiveDispatchHooks(api, ctx) {
139491
139550
  const parsed = parseFabricaSessionKey(sessionKey);
139492
139551
  if (!parsed) return;
139493
139552
  ctx.runtime?.system.requestHeartbeatNow({ reason: "agent_end", coalesceMs: 2e3 });
139553
+ wakeHeartbeat("agent_end").catch(() => {
139554
+ });
139494
139555
  });
139495
139556
  api.on("subagent_spawned", async (event, _eventCtx) => {
139496
139557
  const sessionKey = event.childSessionKey;
@@ -139500,6 +139561,7 @@ function registerReactiveDispatchHooks(api, ctx) {
139500
139561
  }
139501
139562
 
139502
139563
  // lib/dispatch/subagent-lifecycle-hook.ts
139564
+ init_workflow();
139503
139565
  function registerSubagentLifecycleHook(api, ctx) {
139504
139566
  const workspaceDir = resolveWorkspaceDir(ctx.config);
139505
139567
  if (!workspaceDir) return;
@@ -139523,6 +139585,67 @@ function registerSubagentLifecycleHook(api, ctx) {
139523
139585
  ctx.logger.info(
139524
139586
  `subagent_ended: worker ${role} in "${projectName}" ended with outcome=${event.outcome ?? "unknown"} (session=${sessionKey})`
139525
139587
  );
139588
+ try {
139589
+ const projects = await readProjects(workspaceDir);
139590
+ const projectEntry = Object.entries(projects.projects).find(
139591
+ ([, p]) => p.name === projectName
139592
+ );
139593
+ if (!projectEntry) return;
139594
+ const [projectSlug, project] = projectEntry;
139595
+ const roleWorker = project.workers[role];
139596
+ if (!roleWorker) return;
139597
+ let foundLevel;
139598
+ let foundSlotIndex;
139599
+ let foundSlot;
139600
+ for (const [level, slots] of Object.entries(roleWorker.levels)) {
139601
+ for (let i2 = 0; i2 < slots.length; i2++) {
139602
+ if (slots[i2].sessionKey === sessionKey && slots[i2].active) {
139603
+ foundLevel = level;
139604
+ foundSlotIndex = i2;
139605
+ foundSlot = slots[i2];
139606
+ break;
139607
+ }
139608
+ }
139609
+ if (foundSlot) break;
139610
+ }
139611
+ if (!foundSlot || foundLevel == null || foundSlotIndex == null) return;
139612
+ const issueId = foundSlot.issueId;
139613
+ await deactivateWorker(workspaceDir, projectSlug, role, {
139614
+ level: foundLevel,
139615
+ slotIndex: foundSlotIndex
139616
+ });
139617
+ if (issueId) {
139618
+ try {
139619
+ const config2 = await loadConfig(workspaceDir, projectSlug);
139620
+ const activeLabel = getActiveLabel(config2.workflow, role);
139621
+ const revertLabel = getRevertLabel(config2.workflow, role);
139622
+ const { provider } = await createProvider({
139623
+ repo: project.repo,
139624
+ provider: project.provider,
139625
+ runCommand: ctx.runCommand
139626
+ });
139627
+ const issue2 = await provider.getIssue(Number(issueId));
139628
+ if (issue2.labels.includes(activeLabel)) {
139629
+ await provider.transitionLabel(Number(issueId), activeLabel, revertLabel);
139630
+ }
139631
+ } catch {
139632
+ }
139633
+ }
139634
+ await log(workspaceDir, "subagent_ended_slot_cleanup", {
139635
+ sessionKey,
139636
+ project: projectName,
139637
+ role,
139638
+ level: foundLevel,
139639
+ slotIndex: foundSlotIndex,
139640
+ issueId,
139641
+ outcome: event.outcome ?? "unknown"
139642
+ }).catch(() => {
139643
+ });
139644
+ wakeHeartbeat("subagent_ended").catch(() => {
139645
+ });
139646
+ } catch (err) {
139647
+ ctx.logger.warn(`subagent_ended_slot_cleanup failed: ${err.message}`);
139648
+ }
139526
139649
  });
139527
139650
  }
139528
139651