@mestreyoda/fabrica 0.2.0 → 0.2.2

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
@@ -111324,8 +111324,8 @@ import fsSync from "node:fs";
111324
111324
  import path5 from "node:path";
111325
111325
  import { fileURLToPath as fileURLToPath3 } from "node:url";
111326
111326
  function getCurrentVersion() {
111327
- if ("0.2.0") {
111328
- return "0.2.0";
111327
+ if ("0.2.2") {
111328
+ return "0.2.2";
111329
111329
  }
111330
111330
  try {
111331
111331
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -112302,7 +112302,7 @@ function resolve2(config2, sourceLayers, trace2) {
112302
112302
  dispatchMs: config2.timeouts?.dispatchMs ?? 6e5,
112303
112303
  staleWorkerHours: config2.timeouts?.staleWorkerHours ?? 2,
112304
112304
  sessionContextBudget: config2.timeouts?.sessionContextBudget ?? 0.6,
112305
- stallTimeoutMinutes: config2.timeouts?.stallTimeoutMinutes ?? 15,
112305
+ stallTimeoutMinutes: config2.timeouts?.stallTimeoutMinutes ?? 5,
112306
112306
  sessionConfirmAttempts: config2.timeouts?.sessionConfirmAttempts ?? 5,
112307
112307
  sessionConfirmDelayMs: config2.timeouts?.sessionConfirmDelayMs ?? 250,
112308
112308
  sessionLabelMaxLength: config2.timeouts?.sessionLabelMaxLength ?? 64,
@@ -124578,97 +124578,6 @@ function createProjectRegisterTool(ctx) {
124578
124578
  import { jsonResult as jsonResult15 } from "openclaw/plugin-sdk";
124579
124579
  init_audit();
124580
124580
 
124581
- // lib/services/heartbeat/diagnostic.ts
124582
- async function diagnoseStall(input) {
124583
- const { owner, repo, issueId, dispatchAttemptCount } = input;
124584
- if ((dispatchAttemptCount ?? 0) >= 2) {
124585
- const hasArtifacts = await checkForArtifacts(owner, repo, issueId);
124586
- if (!hasArtifacts) {
124587
- return { action: "needs_human_review", reason: "stall", evidence: `${dispatchAttemptCount} attempts, zero artifacts` };
124588
- }
124589
- }
124590
- const pr = await findPrForIssue(owner, repo, issueId);
124591
- if (pr) {
124592
- const qaStatus = await checkPrQaStatus(owner, repo, pr.number);
124593
- if (qaStatus === "pass") {
124594
- return { action: "transition_to_review", reason: "stall", evidence: `PR #${pr.number} exists, QA passing` };
124595
- }
124596
- return { action: "redispatch_same_level", reason: "stall", evidence: `PR #${pr.number} exists, QA ${qaStatus}` };
124597
- }
124598
- const hasCommits = await checkForBranchCommits(owner, repo, issueId);
124599
- if (hasCommits) {
124600
- return { action: "nudge_open_pr", reason: "stall", evidence: "Branch has commits but no PR" };
124601
- }
124602
- const sessionAge = Date.now() - input.sessionUpdatedAt;
124603
- const isSessionDead = sessionAge > 30 * 6e4;
124604
- if (isSessionDead) {
124605
- return { action: "log_infra", reason: "infra", evidence: "Session dead, zero artifacts" };
124606
- }
124607
- return { action: "escalate_level", reason: "complexity", evidence: "Active session without commits" };
124608
- }
124609
- async function findPrForIssue(owner, repo, issueId) {
124610
- try {
124611
- const { execa } = await import("execa");
124612
- const { stdout } = await execa("gh", [
124613
- "pr",
124614
- "list",
124615
- "--repo",
124616
- `${owner}/${repo}`,
124617
- "--search",
124618
- `${issueId}`,
124619
- "--json",
124620
- "number",
124621
- "--limit",
124622
- "1"
124623
- ]);
124624
- const prs = JSON.parse(stdout);
124625
- return prs.length > 0 ? prs[0] : null;
124626
- } catch {
124627
- return null;
124628
- }
124629
- }
124630
- async function checkPrQaStatus(owner, repo, prNumber) {
124631
- try {
124632
- const { execa } = await import("execa");
124633
- const { stdout } = await execa("gh", [
124634
- "pr",
124635
- "checks",
124636
- String(prNumber),
124637
- "--repo",
124638
- `${owner}/${repo}`,
124639
- "--json",
124640
- "state"
124641
- ]);
124642
- const checks = JSON.parse(stdout);
124643
- if (checks.every((c) => c.state === "SUCCESS")) return "pass";
124644
- if (checks.some((c) => c.state === "FAILURE")) return "fail";
124645
- return "pending";
124646
- } catch {
124647
- return "pending";
124648
- }
124649
- }
124650
- async function checkForBranchCommits(owner, repo, issueId) {
124651
- try {
124652
- const { execa } = await import("execa");
124653
- const { stdout } = await execa("gh", [
124654
- "api",
124655
- `repos/${owner}/${repo}/commits`,
124656
- "--jq",
124657
- ".[0].sha",
124658
- "-f",
124659
- `sha=issue-${issueId}`
124660
- ]);
124661
- return stdout.trim().length > 0;
124662
- } catch {
124663
- return false;
124664
- }
124665
- }
124666
- async function checkForArtifacts(owner, repo, issueId) {
124667
- const pr = await findPrForIssue(owner, repo, issueId);
124668
- if (pr) return true;
124669
- return checkForBranchCommits(owner, repo, issueId);
124670
- }
124671
-
124672
124581
  // lib/services/heartbeat/health.ts
124673
124582
  init_audit();
124674
124583
  init_workflow();
@@ -124676,8 +124585,6 @@ init_context3();
124676
124585
  init_labels();
124677
124586
  var GRACE_PERIOD_MS = 5 * 60 * 1e3;
124678
124587
  var DISPATCH_CONFIRMATION_TIMEOUT_MS = 2 * 60 * 1e3;
124679
- var NUDGE_MESSAGE = `You appear to have stalled. Continue working on your current task. If you are blocked or unable to proceed, call work_finish with result "blocked".`;
124680
- var MAX_STALL_NUDGES = 3;
124681
124588
  async function auditHealthFixApplied(workspaceDir, fix, details) {
124682
124589
  if (!fix.fixed) return;
124683
124590
  await withCorrelationContext({
@@ -124709,8 +124616,7 @@ async function auditHealthFixApplied(workspaceDir, fix, details) {
124709
124616
  toLabel: details.toLabel ?? fix.issue.expectedLabel ?? null,
124710
124617
  idleMinutes: details.idleMinutes ?? null,
124711
124618
  deliveryState: details.deliveryState ?? null,
124712
- labelReverted: fix.labelReverted ?? null,
124713
- nudgeSent: fix.nudgeSent ?? false
124619
+ labelReverted: fix.labelReverted ?? null
124714
124620
  }).catch(() => {
124715
124621
  });
124716
124622
  }));
@@ -125043,155 +124949,6 @@ async function checkWorkerHealth(opts) {
125043
124949
  fixes.push(fix);
125044
124950
  continue;
125045
124951
  }
125046
- if (slot.active && sessionKey && sessions && !withinGracePeriod && isSessionAlive(sessionKey, sessions)) {
125047
- const session = sessions.get(sessionKey);
125048
- const stallThresholdMs = (opts.stallTimeoutMinutes ?? 15) * 6e4;
125049
- if (session.updatedAt == null) continue;
125050
- const sessionIdleMs = Date.now() - session.updatedAt;
125051
- if (sessionIdleMs > stallThresholdMs) {
125052
- const idleMinutes = Math.round(sessionIdleMs / 6e4);
125053
- const modelUnresponsiveMs = stallThresholdMs * MAX_STALL_NUDGES;
125054
- const slotAgeMs = slot.startTime ? Date.now() - new Date(slot.startTime).getTime() : sessionIdleMs;
125055
- if (slotAgeMs > modelUnresponsiveMs) {
125056
- const diagnostic = await diagnoseStall({
125057
- projectSlug,
125058
- owner: project.remote?.owner ?? "",
125059
- repo: project.remote?.repo ?? "",
125060
- issueId: issueIdNum ?? 0,
125061
- sessionKey,
125062
- slotStartTime: slot.startTime ? new Date(slot.startTime).getTime() : Date.now() - sessionIdleMs,
125063
- sessionUpdatedAt: session.updatedAt,
125064
- dispatchAttemptCount: issueRuntime?.dispatchAttemptCount ?? 0
125065
- });
125066
- if (issueIdNum) {
125067
- await updateIssueRuntime(workspaceDir, projectSlug, String(issueIdNum), {
125068
- lastDiagnosticResult: diagnostic.evidence,
125069
- lastFailureReason: diagnostic.reason,
125070
- dispatchAttemptCount: (issueRuntime?.dispatchAttemptCount ?? 0) + 1
125071
- }).catch(() => {
125072
- });
125073
- }
125074
- const diagnosticType = `diagnostic_${diagnostic.action}`;
125075
- const fix2 = {
125076
- issue: {
125077
- type: diagnosticType,
125078
- severity: diagnostic.action === "needs_human_review" ? "critical" : "warning",
125079
- project: project.name,
125080
- projectSlug,
125081
- role,
125082
- level,
125083
- sessionKey,
125084
- issueId: slot.issueId,
125085
- slotIndex,
125086
- message: `${role.toUpperCase()} ${level}[${slotIndex}] stall diagnosed: ${diagnostic.action} \u2014 ${diagnostic.evidence}`
125087
- },
125088
- fixed: false
125089
- };
125090
- if (autoFix) {
125091
- switch (diagnostic.action) {
125092
- case "transition_to_review":
125093
- await revertLabel(fix2, expectedLabel, "To Review");
125094
- if (!fix2.labelRevertFailed) {
125095
- await deactivateSlot();
125096
- fix2.fixed = true;
125097
- }
125098
- break;
125099
- case "redispatch_same_level":
125100
- case "nudge_open_pr":
125101
- case "retry_infra":
125102
- await revertLabel(fix2, expectedLabel, slotQueueLabel);
125103
- if (!fix2.labelRevertFailed) {
125104
- await deactivateSlot();
125105
- fix2.fixed = true;
125106
- }
125107
- break;
125108
- case "escalate_level":
125109
- await revertLabel(fix2, expectedLabel, slotQueueLabel);
125110
- if (!fix2.labelRevertFailed) {
125111
- await deactivateSlot();
125112
- fix2.fixed = true;
125113
- }
125114
- break;
125115
- case "needs_human_review":
125116
- await deactivateSlot();
125117
- fix2.fixed = true;
125118
- break;
125119
- case "log_infra":
125120
- await deactivateSlot();
125121
- fix2.fixed = true;
125122
- break;
125123
- }
125124
- }
125125
- await log(workspaceDir, "stall_diagnostic", {
125126
- project: project.name,
125127
- projectSlug,
125128
- role,
125129
- level,
125130
- sessionKey,
125131
- issueId: slot.issueId,
125132
- slotIndex,
125133
- idleMinutes,
125134
- diagnostic: diagnostic.action,
125135
- reason: diagnostic.reason,
125136
- evidence: diagnostic.evidence,
125137
- dispatchAttemptCount: (issueRuntime?.dispatchAttemptCount ?? 0) + 1
125138
- }).catch(() => {
125139
- });
125140
- fixes.push(fix2);
125141
- continue;
125142
- }
125143
- const fix = {
125144
- issue: {
125145
- type: "session_stalled",
125146
- severity: "critical",
125147
- project: project.name,
125148
- projectSlug,
125149
- role,
125150
- level,
125151
- sessionKey,
125152
- issueId: slot.issueId,
125153
- slotIndex,
125154
- message: `${role.toUpperCase()} ${level}[${slotIndex}] session idle ${idleMinutes}m \u2014 sending nudge`
125155
- },
125156
- fixed: false
125157
- };
125158
- if (autoFix) {
125159
- sendToAgent(sessionKey, NUDGE_MESSAGE, {
125160
- agentId: opts.agentId,
125161
- projectName: project.name,
125162
- issueId: issueIdNum,
125163
- role,
125164
- level,
125165
- slotIndex,
125166
- workspaceDir,
125167
- runCommand: opts.runCommand,
125168
- runtime: opts.runtime
125169
- });
125170
- fix.nudgeSent = true;
125171
- fix.fixed = true;
125172
- await auditHealthFixApplied(workspaceDir, fix, {
125173
- action: "nudge_session",
125174
- idleMinutes,
125175
- deliveryState
125176
- });
125177
- }
125178
- await log(workspaceDir, "session_stalled", {
125179
- project: project.name,
125180
- projectSlug,
125181
- role,
125182
- level,
125183
- sessionKey,
125184
- issueId: slot.issueId,
125185
- slotIndex,
125186
- idleMinutes,
125187
- deliveryState,
125188
- action: "nudge"
125189
- }).catch(() => {
125190
- });
125191
- fixes.push(fix);
125192
- continue;
125193
- }
125194
- }
125195
124952
  if (slot.active && slot.startTime && sessionKey && sessions && isSessionAlive(sessionKey, sessions)) {
125196
124953
  const hours = (Date.now() - new Date(slot.startTime).getTime()) / 36e5;
125197
124954
  if (hours > staleWorkerHours) {
@@ -125225,6 +124982,32 @@ async function checkWorkerHealth(opts) {
125225
124982
  fixes.push(fix);
125226
124983
  }
125227
124984
  }
124985
+ if (slot.active && issueIdNum && issue2 && currentLabel === expectedLabel && autoFix) {
124986
+ try {
124987
+ const prStatus = await provider.getPrStatus(issueIdNum);
124988
+ if (prStatus.url && prStatus.state !== PrState.MERGED && prStatus.state !== PrState.CLOSED && prStatus.state !== PrState.CHANGES_REQUESTED && prStatus.state !== PrState.HAS_COMMENTS && prStatus.currentIssueMatch !== false) {
124989
+ const rule = getCompletionRule(workflow, role, "done");
124990
+ if (rule && rule.to !== expectedLabel) {
124991
+ await resilientLabelTransition(provider, issueIdNum, expectedLabel, rule.to);
124992
+ await deactivateSlot();
124993
+ await log(workspaceDir, "health_transition_to_review", {
124994
+ project: project.name,
124995
+ projectSlug,
124996
+ role,
124997
+ level,
124998
+ issueId: slot.issueId,
124999
+ sessionKey,
125000
+ slotIndex,
125001
+ fromLabel: expectedLabel,
125002
+ toLabel: rule.to,
125003
+ prUrl: prStatus.url
125004
+ }).catch(() => {
125005
+ });
125006
+ }
125007
+ }
125008
+ } catch {
125009
+ }
125010
+ }
125228
125011
  if (!slot.active && issue2 && currentLabel === expectedLabel) {
125229
125012
  const fix = {
125230
125013
  issue: {
@@ -125504,8 +125287,7 @@ function createHealthTool(ctx) {
125504
125287
  role,
125505
125288
  sessions,
125506
125289
  autoFix: fix,
125507
- provider,
125508
- runCommand: ctx.runCommand
125290
+ provider
125509
125291
  });
125510
125292
  issues.push(...healthFixes.map((f3) => ({ ...f3, project: project.name, role })));
125511
125293
  const orphanFixes = await scanOrphanedLabels({
@@ -131644,10 +131426,6 @@ async function performHealthPass(workspaceDir, projectSlug, project, sessions, p
131644
131426
  autoFix: true,
131645
131427
  provider,
131646
131428
  staleWorkerHours,
131647
- stallTimeoutMinutes,
131648
- runCommand,
131649
- runtime,
131650
- agentId,
131651
131429
  workflow: resolvedConfig?.workflow,
131652
131430
  dispatchConfirmTimeoutMs: resolvedConfig?.timeouts?.dispatchConfirmTimeoutMs
131653
131431
  });
@@ -138727,16 +138505,32 @@ async function classifyDmIntent(ctx, content, _workspaceDir) {
138727
138505
  idempotencyKey: sessionKey
138728
138506
  });
138729
138507
  const waitResult = await runtime.subagent.waitForRun({ runId, timeoutMs: 15e3 });
138730
- if (waitResult.status !== "ok") return null;
138731
- const messages = await runtime.subagent.getSessionMessages({ sessionKey });
138732
- const lastAssistant = messages?.filter((m2) => m2.role === "assistant").pop();
138733
- const text = lastAssistant?.content ?? "";
138734
- if (!text.trim()) return null;
138508
+ if (waitResult.status !== "ok") {
138509
+ ctx.logger.warn(`[telegram-bootstrap] classify waitForRun status=${waitResult.status} (expected ok)`);
138510
+ return null;
138511
+ }
138512
+ const messagesResult = await runtime.subagent.getSessionMessages({ sessionKey });
138513
+ const messages = Array.isArray(messagesResult) ? messagesResult : Array.isArray(messagesResult?.messages) ? messagesResult.messages : [];
138514
+ const lastAssistant = messages.filter((m2) => m2.role === "assistant").pop();
138515
+ if (!lastAssistant) {
138516
+ ctx.logger.warn(`[telegram-bootstrap] classify: no assistant message found (messages=${messages.length}, resultType=${typeof messagesResult})`);
138517
+ return null;
138518
+ }
138519
+ const rawContent = lastAssistant.content;
138520
+ const text = typeof rawContent === "string" ? rawContent : Array.isArray(rawContent) ? rawContent.find((b) => b.type === "text")?.text ?? "" : "";
138521
+ if (!text.trim()) {
138522
+ ctx.logger.warn(`[telegram-bootstrap] classify: empty text from assistant (contentType=${typeof rawContent}, isArray=${Array.isArray(rawContent)})`);
138523
+ return null;
138524
+ }
138735
138525
  const jsonStr = text.replace(/^```(json)?/gm, "").replace(/```$/gm, "").trim();
138736
138526
  const intentData = JSON.parse(jsonStr);
138737
138527
  const validated = DmIntentSchema.safeParse(intentData);
138528
+ if (!validated.success) {
138529
+ ctx.logger.warn(`[telegram-bootstrap] classify: schema validation failed: ${validated.error?.message}`);
138530
+ }
138738
138531
  return validated.success ? validated.data : null;
138739
- } catch {
138532
+ } catch (err) {
138533
+ ctx.logger.warn(`[telegram-bootstrap] classify exception: ${err.message ?? err}`);
138740
138534
  return null;
138741
138535
  }
138742
138536
  }
@@ -138816,14 +138610,16 @@ function buildTopicDeepLink(chatId, topicId) {
138816
138610
  function buildDmAck(projectName, topicLink, language = "pt") {
138817
138611
  return BOOTSTRAP_MESSAGES.registered[language](projectName, topicLink);
138818
138612
  }
138819
- function buildTopicKickoff(projectName, idea) {
138820
- return [
138821
- `\u{1F9F1} Projeto registrado automaticamente pela Fabrica.`,
138822
- `Projeto: ${projectName}`,
138823
- "",
138824
- "Resumo do pedido inicial:",
138825
- idea
138826
- ].join("\n");
138613
+ function buildTopicKickoff(projectName, idea, language = "pt") {
138614
+ const header = language === "en" ? `\u{1F9F1} Project automatically registered by Fabrica.
138615
+ Project: ${projectName}
138616
+
138617
+ Original request summary:` : `\u{1F9F1} Projeto registrado automaticamente pela Fabrica.
138618
+ Projeto: ${projectName}
138619
+
138620
+ Resumo do pedido inicial:`;
138621
+ return `${header}
138622
+ ${idea}`;
138827
138623
  }
138828
138624
  async function sendTelegramText(ctx, target, message, opts) {
138829
138625
  const sendOpts = {
@@ -139113,7 +138909,8 @@ Erro: ${result.error ?? "erro desconhecido"}`
139113
138909
  accountId: telegramConfig.projectsForumAccountId ?? void 0
139114
138910
  }
139115
138911
  });
139116
- await sendTelegramText(ctx, projectChannelId, buildTopicKickoff(resolvedProjectName, request.rawIdea), {
138912
+ const sessionLang = currentSession?.language ?? "pt";
138913
+ await sendTelegramText(ctx, projectChannelId, buildTopicKickoff(resolvedProjectName, request.rawIdea, sessionLang), {
139117
138914
  accountId: telegramConfig.projectsForumAccountId,
139118
138915
  messageThreadId
139119
138916
  });
@@ -139132,7 +138929,6 @@ Erro: ${result.error ?? "erro desconhecido"}`
139132
138929
  logBootstrapWarning(ctx, `[telegram-bootstrap] immediate projectTick failed: ${error48 instanceof Error ? error48.message : String(error48)}`);
139133
138930
  });
139134
138931
  }
139135
- const sessionLang = currentSession?.language ?? "pt";
139136
138932
  await sendTelegramText(ctx, conversationId, buildDmAck(resolvedProjectName, buildTopicDeepLink(String(projectChannelId), messageThreadId), sessionLang));
139137
138933
  await upsertTelegramBootstrapSession(workspaceDir, {
139138
138934
  conversationId,
@@ -139652,6 +139448,36 @@ function registerGatewayLifecycleHook(api, ctx) {
139652
139448
 
139653
139449
  // lib/dispatch/subagent-lifecycle-hook.ts
139654
139450
  init_audit();
139451
+
139452
+ // lib/dispatch/reactive-dispatch-hook.ts
139453
+ var COMPLETION_TOOLS = /* @__PURE__ */ new Set(["work_finish", "review_submit"]);
139454
+ var spawnTimes = /* @__PURE__ */ new Map();
139455
+ function getSpawnTime(sessionKey) {
139456
+ return spawnTimes.get(sessionKey);
139457
+ }
139458
+ function clearSpawnTime(sessionKey) {
139459
+ spawnTimes.delete(sessionKey);
139460
+ }
139461
+ function registerReactiveDispatchHooks(api, ctx) {
139462
+ api.on("after_tool_call", async (event, _eventCtx) => {
139463
+ if (!COMPLETION_TOOLS.has(event.toolName)) return;
139464
+ ctx.runtime?.system.requestHeartbeatNow({ reason: "work_finish", coalesceMs: 2e3 });
139465
+ });
139466
+ api.on("agent_end", async (_event, eventCtx) => {
139467
+ const sessionKey = eventCtx.sessionKey;
139468
+ if (!sessionKey) return;
139469
+ const parsed = parseFabricaSessionKey(sessionKey);
139470
+ if (!parsed) return;
139471
+ ctx.runtime?.system.requestHeartbeatNow({ reason: "agent_end", coalesceMs: 2e3 });
139472
+ });
139473
+ api.on("subagent_spawned", async (event, _eventCtx) => {
139474
+ const sessionKey = event.childSessionKey;
139475
+ if (!sessionKey) return;
139476
+ spawnTimes.set(sessionKey, Date.now());
139477
+ });
139478
+ }
139479
+
139480
+ // lib/dispatch/subagent-lifecycle-hook.ts
139655
139481
  function registerSubagentLifecycleHook(api, ctx) {
139656
139482
  const workspaceDir = resolveWorkspaceDir(ctx.config);
139657
139483
  if (!workspaceDir) return;
@@ -139661,11 +139487,15 @@ function registerSubagentLifecycleHook(api, ctx) {
139661
139487
  const parsed = parseFabricaSessionKey(sessionKey);
139662
139488
  if (!parsed) return;
139663
139489
  const { projectName, role } = parsed;
139490
+ const spawnTime = getSpawnTime(sessionKey);
139491
+ clearSpawnTime(sessionKey);
139492
+ const durationMs = spawnTime != null ? Date.now() - spawnTime : void 0;
139664
139493
  await log(workspaceDir, "subagent_ended", {
139665
139494
  sessionKey,
139666
139495
  project: projectName,
139667
139496
  role,
139668
- outcome: event.outcome ?? "unknown"
139497
+ outcome: event.outcome ?? "unknown",
139498
+ ...durationMs != null ? { durationMs } : {}
139669
139499
  }).catch(() => {
139670
139500
  });
139671
139501
  ctx.logger.info(
@@ -139729,6 +139559,23 @@ function registerModelResolveHook(api, ctx) {
139729
139559
  });
139730
139560
  }
139731
139561
 
139562
+ // lib/dispatch/worker-context-hook.ts
139563
+ var WORK_FINISH_CONTEXT = `## Task Completion
139564
+
139565
+ When you have finished your task, you MUST call the \`work_finish\` tool to signal completion.
139566
+ Do NOT rely on your session ending automatically \u2014 you must explicitly call \`work_finish\`.
139567
+ This is required for the pipeline to advance to the next stage.
139568
+ `;
139569
+ function registerWorkerContextHook(api, _ctx) {
139570
+ api.on("before_agent_start", async (_event, eventCtx) => {
139571
+ const sessionKey = eventCtx.sessionKey;
139572
+ if (!sessionKey) return;
139573
+ const parsed = parseFabricaSessionKey(sessionKey);
139574
+ if (!parsed) return;
139575
+ return { prependSystemContext: WORK_FINISH_CONTEXT };
139576
+ });
139577
+ }
139578
+
139732
139579
  // index.ts
139733
139580
  var plugin = {
139734
139581
  id: "fabrica",
@@ -139869,8 +139716,10 @@ var plugin = {
139869
139716
  registerGatewayLifecycleHook(api, ctx);
139870
139717
  registerSubagentLifecycleHook(api, ctx);
139871
139718
  registerModelResolveHook(api, ctx);
139719
+ registerWorkerContextHook(api, ctx);
139720
+ registerReactiveDispatchHooks(api, ctx);
139872
139721
  ctx.logger.info(
139873
- "Fabrica plugin registered (25 tools, 1 CLI command group, 1 service, 7 hooks total: bootstrap, telegram-dm bootstrap, attachment, gateway lifecycle, subagent lifecycle, model-resolve, optional GitHub webhook route)"
139722
+ "Fabrica plugin registered (25 tools, 1 CLI command group, 1 service, 9 hooks total: bootstrap, telegram-dm bootstrap, attachment, gateway lifecycle, subagent lifecycle, model-resolve, worker-context, reactive-dispatch, optional GitHub webhook route)"
139874
139723
  );
139875
139724
  }
139876
139725
  };