@integrity-labs/agt-cli 0.28.341 → 0.28.343

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.
@@ -46,14 +46,14 @@ import {
46
46
  resolveEffectivePinRaw,
47
47
  safeWriteJsonAtomic,
48
48
  setConfigHash
49
- } from "../chunk-SRRZO75K.js";
49
+ } from "../chunk-GAYSFNTX.js";
50
50
  import {
51
51
  getProjectDir as getProjectDir2,
52
52
  getReadyTasks,
53
53
  loadSchedulerState,
54
54
  markTaskFired,
55
55
  syncTasksToScheduler
56
- } from "../chunk-I3YS5WFV.js";
56
+ } from "../chunk-EP6E6CIY.js";
57
57
  import {
58
58
  AnchorSessionClient,
59
59
  CONVERSATION_FAILURE_CATEGORIES,
@@ -141,7 +141,7 @@ import {
141
141
  } from "../chunk-XWVM4KPK.js";
142
142
 
143
143
  // src/lib/manager-worker.ts
144
- import { createHash as createHash12 } from "crypto";
144
+ import { createHash as createHash13 } from "crypto";
145
145
  import { readFileSync as readFileSync18, writeFileSync as writeFileSync9, mkdirSync as mkdirSync8, existsSync as existsSync12, rmSync as rmSync4, readdirSync as readdirSync5, statSync as statSync4, copyFileSync } from "fs";
146
146
  import { execFileSync as syncExecFile } from "child_process";
147
147
  import { join as join21, dirname as dirname6 } from "path";
@@ -6354,11 +6354,15 @@ function resolveSlackIngestParamsFromEnv(env, ctx) {
6354
6354
  const appToken = env["SLACK_APP_TOKEN"];
6355
6355
  const botToken = env["SLACK_BOT_TOKEN"];
6356
6356
  if (!appToken || !botToken) return null;
6357
+ const ackReaction = (env["SLACK_ACK_REACTION"] ?? "").trim() || void 0;
6358
+ const skipReaction = (env["SLACK_SKIP_REACTION"] ?? "").trim() || void 0;
6357
6359
  return {
6358
6360
  appToken,
6359
6361
  botToken,
6360
6362
  senderPolicy: buildSlackSenderPolicyFromEnv(env, ctx),
6361
- peerConfig: buildSlackPeerClassifierConfigFromEnv(env)
6363
+ peerConfig: buildSlackPeerClassifierConfigFromEnv(env),
6364
+ ...ackReaction ? { ackReaction } : {},
6365
+ ...skipReaction ? { skipReaction } : {}
6362
6366
  };
6363
6367
  }
6364
6368
  var BoundedSet = class {
@@ -6396,6 +6400,12 @@ async function dispatchSlackInbound(event, ctx) {
6396
6400
  ctx.log(`[slack-ingest:${ctx.codeName}] dropped ${redact(event.channel)} reason=${decision.reason}`);
6397
6401
  return "dropped_gate";
6398
6402
  }
6403
+ let ackReactionInFlight = Promise.resolve();
6404
+ if (ctx.ackReaction && event.channel && event.ts) {
6405
+ ackReactionInFlight = ctx.addReaction({ channel: event.channel, timestamp: event.ts, name: ctx.ackReaction });
6406
+ ackReactionInFlight.catch(() => {
6407
+ });
6408
+ }
6399
6409
  const conversationKey = `${event.channel ?? "nochannel"}:${event.thread_ts ?? event.ts ?? ""}`;
6400
6410
  const meta = {
6401
6411
  source: "slack",
@@ -6423,6 +6433,23 @@ async function dispatchSlackInbound(event, ctx) {
6423
6433
  });
6424
6434
  return "admitted_replied";
6425
6435
  }
6436
+ if (ctx.skipReaction && event.channel && event.ts) {
6437
+ const channel = event.channel;
6438
+ const timestamp = event.ts;
6439
+ const { ackReaction, skipReaction } = ctx;
6440
+ if (ackReaction !== skipReaction) {
6441
+ void (async () => {
6442
+ try {
6443
+ if (ackReaction) {
6444
+ await ackReactionInFlight;
6445
+ await ctx.removeReaction({ channel, timestamp, name: ackReaction });
6446
+ }
6447
+ await ctx.addReaction({ channel, timestamp, name: skipReaction });
6448
+ } catch {
6449
+ }
6450
+ })();
6451
+ }
6452
+ }
6426
6453
  return "admitted_no_reply";
6427
6454
  }
6428
6455
  function redact(id) {
@@ -6448,6 +6475,46 @@ function defaultSendReply(botToken, fetchImpl, log2) {
6448
6475
  }
6449
6476
  };
6450
6477
  }
6478
+ function defaultAddReaction(botToken, fetchImpl, log2) {
6479
+ return async ({ channel, timestamp, name }) => {
6480
+ try {
6481
+ const res = await fetchImpl(`${SLACK_API}/reactions.add`, {
6482
+ method: "POST",
6483
+ headers: {
6484
+ Authorization: `Bearer ${botToken}`,
6485
+ "Content-Type": "application/json; charset=utf-8"
6486
+ },
6487
+ body: JSON.stringify({ channel, timestamp, name })
6488
+ });
6489
+ const data = await res.json();
6490
+ if (!data.ok && data.error !== "already_reacted") {
6491
+ log2(`[slack-ingest] reactions.add failed: ${data.error}`);
6492
+ }
6493
+ } catch (err) {
6494
+ log2(`[slack-ingest] reactions.add threw: ${err instanceof Error ? err.message : String(err)}`);
6495
+ }
6496
+ };
6497
+ }
6498
+ function defaultRemoveReaction(botToken, fetchImpl, log2) {
6499
+ return async ({ channel, timestamp, name }) => {
6500
+ try {
6501
+ const res = await fetchImpl(`${SLACK_API}/reactions.remove`, {
6502
+ method: "POST",
6503
+ headers: {
6504
+ Authorization: `Bearer ${botToken}`,
6505
+ "Content-Type": "application/json; charset=utf-8"
6506
+ },
6507
+ body: JSON.stringify({ channel, timestamp, name })
6508
+ });
6509
+ const data = await res.json();
6510
+ if (!data.ok && data.error !== "no_reaction") {
6511
+ log2(`[slack-ingest] reactions.remove failed: ${data.error}`);
6512
+ }
6513
+ } catch (err) {
6514
+ log2(`[slack-ingest] reactions.remove threw: ${err instanceof Error ? err.message : String(err)}`);
6515
+ }
6516
+ };
6517
+ }
6451
6518
  function startSlackIngest(config2) {
6452
6519
  const fetchImpl = config2.fetchImpl ?? globalThis.fetch;
6453
6520
  const socketFactory = config2.socketFactory ?? ((url) => {
@@ -6456,6 +6523,8 @@ function startSlackIngest(config2) {
6456
6523
  return new Ctor(url);
6457
6524
  });
6458
6525
  const sendReply = config2.sendReply ?? defaultSendReply(config2.botToken, fetchImpl, config2.log);
6526
+ const addReaction = config2.addReaction ?? defaultAddReaction(config2.botToken, fetchImpl, config2.log);
6527
+ const removeReaction = config2.removeReaction ?? defaultRemoveReaction(config2.botToken, fetchImpl, config2.log);
6459
6528
  const seen = new BoundedSet();
6460
6529
  let stopped = false;
6461
6530
  let ws = null;
@@ -6466,6 +6535,10 @@ function startSlackIngest(config2) {
6466
6535
  peerConfig: config2.peerConfig,
6467
6536
  inject: config2.inject,
6468
6537
  sendReply,
6538
+ addReaction,
6539
+ removeReaction,
6540
+ ...config2.ackReaction ? { ackReaction: config2.ackReaction } : {},
6541
+ ...config2.skipReaction ? { skipReaction: config2.skipReaction } : {},
6469
6542
  log: config2.log,
6470
6543
  seen
6471
6544
  };
@@ -6564,7 +6637,9 @@ function fingerprint(params) {
6564
6637
  appToken: params.appToken,
6565
6638
  botToken: params.botToken,
6566
6639
  senderPolicy: params.senderPolicy,
6567
- peerConfig: params.peerConfig
6640
+ peerConfig: params.peerConfig,
6641
+ ackReaction: params.ackReaction ?? null,
6642
+ skipReaction: params.skipReaction ?? null
6568
6643
  })
6569
6644
  ).digest("hex");
6570
6645
  }
@@ -6618,6 +6693,8 @@ async function ensureOpencodeSlackIngest(opts) {
6618
6693
  peerConfig: params.peerConfig,
6619
6694
  inject: (msg) => injectOpencodeMessage(codeName, msg, { awaitReply: true }),
6620
6695
  log: log2,
6696
+ ...params.ackReaction ? { ackReaction: params.ackReaction } : {},
6697
+ ...params.skipReaction ? { skipReaction: params.skipReaction } : {},
6621
6698
  ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {},
6622
6699
  ...opts.socketFactory ? { socketFactory: opts.socketFactory } : {}
6623
6700
  });
@@ -6635,6 +6712,98 @@ function stopAllOpencodeSlackIngests(log2) {
6635
6712
  for (const codeName of [...ingests.keys()]) stopOpencodeSlackIngest(codeName, log2);
6636
6713
  }
6637
6714
 
6715
+ // src/lib/manager/opencode-scheduler.ts
6716
+ import { createHash as createHash12 } from "crypto";
6717
+ var MAX_OPENCODE_SCHED_CONCURRENCY = 2;
6718
+ var opencodeSchedulerStates = /* @__PURE__ */ new Map();
6719
+ var inFlightOpencodeTasks = /* @__PURE__ */ new Set();
6720
+ var opencodeTaskConcurrency = /* @__PURE__ */ new Map();
6721
+ var knownOpencodeTaskHashes = /* @__PURE__ */ new Map();
6722
+ function shouldDeliver(task, reply) {
6723
+ if ((task.deliveryPolicy ?? "always") === "never") return false;
6724
+ if (task.deliveryMode === "none") return false;
6725
+ if (task.deliveryTo == null) return false;
6726
+ return typeof reply === "string" && reply.trim().length > 0;
6727
+ }
6728
+ async function syncAndCheckOpencodeScheduler(agent, tasks, refreshData) {
6729
+ const codeName = agent.code_name;
6730
+ const tasksHash = createHash12("sha256").update(JSON.stringify(tasks)).digest("hex").slice(0, 16);
6731
+ if (knownOpencodeTaskHashes.get(agent.agent_id) !== tasksHash) {
6732
+ const taskInputs = tasks.map((t) => buildSchedulerTaskInput(t));
6733
+ const state8 = syncTasksToScheduler(codeName, agent.agent_id, taskInputs);
6734
+ opencodeSchedulerStates.set(codeName, state8);
6735
+ knownOpencodeTaskHashes.set(agent.agent_id, tasksHash);
6736
+ log(`[opencode-scheduler] tasks synced for '${codeName}' (${taskInputs.length} task(s))`);
6737
+ }
6738
+ if (!opencodeSchedulerStates.has(codeName)) {
6739
+ opencodeSchedulerStates.set(codeName, loadSchedulerState(codeName));
6740
+ }
6741
+ const state7 = opencodeSchedulerStates.get(codeName);
6742
+ const ready = getReadyTasks(state7, inFlightOpencodeTasks);
6743
+ if (ready.length === 0) return;
6744
+ if (!isOpencodeSessionHealthy(codeName)) {
6745
+ log(`[opencode-scheduler] '${codeName}' has ${ready.length} due task(s) but no healthy serve yet - deferring`);
6746
+ return;
6747
+ }
6748
+ for (const task of ready) {
6749
+ if ((opencodeTaskConcurrency.get(codeName) ?? 0) >= MAX_OPENCODE_SCHED_CONCURRENCY) break;
6750
+ if (KANBAN_WORK_TEMPLATES.has(task.templateId)) {
6751
+ opencodeSchedulerStates.set(codeName, markTaskFired(codeName, task.taskId, "ok"));
6752
+ continue;
6753
+ }
6754
+ inFlightOpencodeTasks.add(task.taskId);
6755
+ opencodeTaskConcurrency.set(codeName, (opencodeTaskConcurrency.get(codeName) ?? 0) + 1);
6756
+ void fireOpencodeScheduledTask(agent, task).finally(() => {
6757
+ inFlightOpencodeTasks.delete(task.taskId);
6758
+ opencodeTaskConcurrency.set(
6759
+ codeName,
6760
+ Math.max(0, (opencodeTaskConcurrency.get(codeName) ?? 1) - 1)
6761
+ );
6762
+ });
6763
+ }
6764
+ }
6765
+ async function fireOpencodeScheduledTask(agent, task) {
6766
+ const codeName = agent.code_name;
6767
+ const conversationKey = `scheduled:${task.taskId}:${Date.now()}`;
6768
+ try {
6769
+ const result = await injectOpencodeMessage(
6770
+ codeName,
6771
+ {
6772
+ channelId: "scheduler",
6773
+ conversationKey,
6774
+ senderId: "scheduler",
6775
+ text: task.prompt,
6776
+ meta: {
6777
+ source: "scheduled_task",
6778
+ task: task.name,
6779
+ // The manager delivers the output; the agent isn't replying into a
6780
+ // channel thread itself, so it must not try to.
6781
+ requires_reply: "false"
6782
+ }
6783
+ },
6784
+ { awaitReply: true }
6785
+ );
6786
+ if (result.status === "declined") {
6787
+ log(`[opencode-scheduler] task '${task.name}' for '${codeName}' declined: ${result.reason}`);
6788
+ return;
6789
+ }
6790
+ const reply = result.status === "replied" ? result.reply : null;
6791
+ if (shouldDeliver(task, reply)) {
6792
+ try {
6793
+ await deliverScheduledTaskOutput(codeName, agent.agent_id, task.deliveryTo, reply, task.taskId);
6794
+ } catch (err) {
6795
+ log(`[opencode-scheduler] delivery failed for task '${task.name}' on '${codeName}': ${err.message}`);
6796
+ }
6797
+ } else if ((task.deliveryPolicy ?? "always") !== "never" && task.deliveryTo != null) {
6798
+ log(`[opencode-scheduler] task '${task.name}' for '${codeName}' produced no output to deliver`);
6799
+ }
6800
+ opencodeSchedulerStates.set(codeName, markTaskFired(codeName, task.taskId, "ok"));
6801
+ } catch (err) {
6802
+ log(`[opencode-scheduler] task '${task.name}' for '${codeName}' failed: ${err.message}`);
6803
+ opencodeSchedulerStates.set(codeName, markTaskFired(codeName, task.taskId, "error"));
6804
+ }
6805
+ }
6806
+
6638
6807
  // src/lib/wedge-detection.ts
6639
6808
  var DEFAULTS = {
6640
6809
  inboundWaitSeconds: 120,
@@ -8045,7 +8214,7 @@ var sessionLaunchManagedStructure = /* @__PURE__ */ new Map();
8045
8214
  function projectMcpHash(_codeName, projectDir) {
8046
8215
  try {
8047
8216
  const raw = readFileSync18(join21(projectDir, ".mcp.json"), "utf-8");
8048
- return createHash12("sha256").update(canonicalJson(JSON.parse(raw))).digest("hex");
8217
+ return createHash13("sha256").update(canonicalJson(JSON.parse(raw))).digest("hex");
8049
8218
  } catch {
8050
8219
  return null;
8051
8220
  }
@@ -8391,7 +8560,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
8391
8560
  var lastVersionCheckAt = 0;
8392
8561
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
8393
8562
  var lastResponsivenessProbeAt = 0;
8394
- var agtCliVersion = true ? "0.28.341" : "dev";
8563
+ var agtCliVersion = true ? "0.28.343" : "dev";
8395
8564
  function resolveBrewPath(execFileSync2) {
8396
8565
  try {
8397
8566
  const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -9485,7 +9654,7 @@ async function pollCycle() {
9485
9654
  claudeAuth = await detectClaudeAuth();
9486
9655
  } catch (err) {
9487
9656
  const errText = err instanceof Error ? err.message : String(err);
9488
- const errId = createHash12("sha256").update(errText).digest("hex").slice(0, 12);
9657
+ const errId = createHash13("sha256").update(errText).digest("hex").slice(0, 12);
9489
9658
  log(`Claude auth detection failed (error_id=${errId})`);
9490
9659
  }
9491
9660
  const hostHasClaudeCode = state6.agents.some(
@@ -9643,7 +9812,7 @@ async function pollCycle() {
9643
9812
  livePendingInboundOldestAgeSeconds,
9644
9813
  parkPendingInbound
9645
9814
  } = await import("../responsiveness-probe-XLS2FXIX.js");
9646
- const { getProjectDir: wedgeProjectDir } = await import("../claude-scheduler-FATCLHDM.js");
9815
+ const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-HYOOQV4A.js");
9647
9816
  const wedgeNow = /* @__PURE__ */ new Date();
9648
9817
  const liveAgents = agentState.persistentSessionAgents;
9649
9818
  for (const tracked of consecutiveWedgeCycles.keys()) {
@@ -10094,6 +10263,15 @@ async function pollCycle() {
10094
10263
  pollDirectChatMessages(agentStates).catch((err) => {
10095
10264
  log(`Direct chat poll error: ${err.message}`);
10096
10265
  });
10266
+ } else {
10267
+ const opencodeAgents = agentStates.filter(
10268
+ (a) => (agentFrameworkCache.get(a.codeName) ?? DEFAULT_FRAMEWORK) === "opencode"
10269
+ );
10270
+ if (opencodeAgents.length > 0) {
10271
+ pollDirectChatMessages(opencodeAgents).catch((err) => {
10272
+ log(`Direct chat poll error (opencode): ${err.message}`);
10273
+ });
10274
+ }
10097
10275
  }
10098
10276
  ensureRealtimeStarted(agentStates);
10099
10277
  ensureRealtimeDriftStarted(agentStates);
@@ -10896,7 +11074,7 @@ async function processAgent(agent, agentStates) {
10896
11074
  const behaviourSubset = extractMsTeamsBehaviourSubset(
10897
11075
  msteamsEntry?.config
10898
11076
  );
10899
- const behaviourHash = createHash12("sha256").update(canonicalJson(behaviourSubset)).digest("hex");
11077
+ const behaviourHash = createHash13("sha256").update(canonicalJson(behaviourSubset)).digest("hex");
10900
11078
  const prevBehaviourHash = agentState.knownMsTeamsBehaviourHashes.get(agent.agent_id);
10901
11079
  const msteamsBehaviourRestrictive = isMsTeamsBehaviourRestrictive(behaviourSubset);
10902
11080
  const behaviourDecision = decideSenderPolicyRestart({
@@ -10952,7 +11130,7 @@ async function processAgent(agent, agentStates) {
10952
11130
  const slackBehaviourSubset = extractSlackBehaviourSubset(
10953
11131
  slackEntry?.config
10954
11132
  );
10955
- const slackBehaviourHash = createHash12("sha256").update(canonicalJson(slackBehaviourSubset)).digest("hex");
11133
+ const slackBehaviourHash = createHash13("sha256").update(canonicalJson(slackBehaviourSubset)).digest("hex");
10956
11134
  const prevSlackBehaviourHash = agentState.knownSlackBehaviourHashes.get(agent.agent_id);
10957
11135
  const slackBehaviourRestrictive = isSlackBehaviourRestrictive(slackBehaviourSubset);
10958
11136
  const slackBehaviourDecision = decideSenderPolicyRestart({
@@ -11302,10 +11480,10 @@ async function processAgent(agent, agentStates) {
11302
11480
  desiredEntries.push({ serverId, url, headers: mcpHeaders, name: tk.toolkit_name });
11303
11481
  }
11304
11482
  const hashBasis = desiredEntries.slice().sort((a, b) => a.serverId.localeCompare(b.serverId)).map((e) => {
11305
- const headersHash = createHash12("sha256").update(canonicalJson(e.headers ?? {})).digest("hex").slice(0, 16);
11483
+ const headersHash = createHash13("sha256").update(canonicalJson(e.headers ?? {})).digest("hex").slice(0, 16);
11306
11484
  return `${e.serverId}|${e.url}|${headersHash}`;
11307
11485
  }).join("\n");
11308
- const mcpHash = createHash12("sha256").update(hashBasis).digest("hex").slice(0, 16);
11486
+ const mcpHash = createHash13("sha256").update(hashBasis).digest("hex").slice(0, 16);
11309
11487
  const prevMcpHash = agentState.knownManagedMcpHashes.get(agent.agent_id);
11310
11488
  const structureHash = managedMcpStructureHash(desiredEntries);
11311
11489
  const prevStructureHash = agentState.knownManagedMcpStructure.get(agent.agent_id);
@@ -11320,7 +11498,7 @@ async function processAgent(agent, agentStates) {
11320
11498
  if (mcpHash !== prevMcpHash) {
11321
11499
  for (const e of desiredEntries) {
11322
11500
  frameworkAdapter.writeMcpServer(agent.code_name, e.serverId, { url: e.url, headers: e.headers });
11323
- const urlHash = createHash12("sha256").update(e.url).digest("hex").slice(0, 12);
11501
+ const urlHash = createHash13("sha256").update(e.url).digest("hex").slice(0, 12);
11324
11502
  log(`[managed-toolkit] ${agent.code_name}: wrote '${e.name}' (serverId=${e.serverId}, url_hash=${urlHash})`);
11325
11503
  }
11326
11504
  if (frameworkAdapter.removeMcpServer && frameworkAdapter.getMcpPath) {
@@ -11429,7 +11607,7 @@ async function processAgent(agent, agentStates) {
11429
11607
  if (frameworkAdapter.installSkillFiles) {
11430
11608
  const currentIntegrationSkillIds = /* @__PURE__ */ new Set();
11431
11609
  const installedIntegrationSkills = [];
11432
- const { createHash: createHash13 } = await import("crypto");
11610
+ const { createHash: createHash14 } = await import("crypto");
11433
11611
  const refreshAny = refreshData;
11434
11612
  const contexts = refreshAny.integration_contexts ?? refreshAny.plugin_contexts ?? [];
11435
11613
  const contextBySlug = /* @__PURE__ */ new Map();
@@ -11458,7 +11636,7 @@ async function processAgent(agent, agentStates) {
11458
11636
  )
11459
11637
  }));
11460
11638
  const bundle = buildIntegrationBundle(renderedScopes);
11461
- const contentHash = createHash13("sha256").update(bundleFingerprint(bundle.files)).digest("hex").slice(0, 12);
11639
+ const contentHash = createHash14("sha256").update(bundleFingerprint(bundle.files)).digest("hex").slice(0, 12);
11462
11640
  const hashKey = `plugin-skill:${agent.agent_id}:${integrationSkillId}`;
11463
11641
  if (agentState.knownSkillHashes.get(hashKey) === contentHash) continue;
11464
11642
  frameworkAdapter.installSkillFiles(agent.code_name, integrationSkillId, bundle.files);
@@ -11525,7 +11703,7 @@ async function processAgent(agent, agentStates) {
11525
11703
  const plan = planGlobalSkillSync(
11526
11704
  [...globalSkillsPayload ?? [], ...sharedSkillsPayload ?? []],
11527
11705
  prevIds,
11528
- (content) => createHash13("sha256").update(content).digest("hex").slice(0, 12),
11706
+ (content) => createHash14("sha256").update(content).digest("hex").slice(0, 12),
11529
11707
  (skillId) => agentState.knownSkillHashes.get(`global-skill:${agent.agent_id}:${skillId}`),
11530
11708
  { desiredResolved }
11531
11709
  );
@@ -11576,7 +11754,7 @@ async function processAgent(agent, agentStates) {
11576
11754
  const slug = hook.integration_slug ?? hook.plugin_slug;
11577
11755
  if (!slug) continue;
11578
11756
  try {
11579
- const scriptHash = createHash13("sha256").update(hook.script).digest("hex").slice(0, 12);
11757
+ const scriptHash = createHash14("sha256").update(hook.script).digest("hex").slice(0, 12);
11580
11758
  const hookKey = `${agent.agent_id}:${frameworkAdapter.id}:plugin-hook:${slug}:on_install`;
11581
11759
  if (agentState.knownSkillHashes.get(hookKey) === scriptHash) continue;
11582
11760
  const result = await frameworkAdapter.executePluginHook({
@@ -11591,9 +11769,9 @@ async function processAgent(agent, agentStates) {
11591
11769
  } else if (result.timedOut) {
11592
11770
  log(`Integration hook on_install '${slug}' TIMED OUT for '${agent.code_name}' after ${result.durationMs}ms`);
11593
11771
  } else {
11594
- const stderrHash = createHash13("sha256").update(result.stderr).digest("hex").slice(0, 12);
11772
+ const stderrHash = createHash14("sha256").update(result.stderr).digest("hex").slice(0, 12);
11595
11773
  const missingCmd = result.exitCode === 127 ? extractCommandNotFound(result.stderr) : null;
11596
- const missingCmdHash = missingCmd ? createHash13("sha256").update(missingCmd).digest("hex").slice(0, 8) : null;
11774
+ const missingCmdHash = missingCmd ? createHash14("sha256").update(missingCmd).digest("hex").slice(0, 8) : null;
11597
11775
  log(
11598
11776
  `Integration hook on_install '${slug}' exited ${result.exitCode} for '${agent.code_name}' ` + (missingCmdHash ? `[missing_command_hash=${missingCmdHash}] ` : "") + `[stderr_hash=${stderrHash} stderr_len=${result.stderr.length}]`
11599
11777
  );
@@ -11701,7 +11879,7 @@ async function processAgent(agent, agentStates) {
11701
11879
  }
11702
11880
  }
11703
11881
  }
11704
- if (agentFw === "claude-code" && sessionMode === "persistent") {
11882
+ if (agentFw === "claude-code" && sessionMode === "persistent" || agentFw === "opencode") {
11705
11883
  const wasSessionHealthy = isSessionHealthy(agent.code_name);
11706
11884
  let psResult;
11707
11885
  try {
@@ -11733,167 +11911,172 @@ async function processAgent(agent, agentStates) {
11733
11911
  } else if (stuck.recovered) {
11734
11912
  log(`[persistent-session-stuck] agent=${agent.code_name} recovered from stuck state`);
11735
11913
  }
11736
- try {
11737
- checkMcpConfigDriftAndScheduleRestart(agent.code_name, getProjectDir(agent.code_name));
11738
- } catch (err) {
11739
- log(`[hot-reload] .mcp.json drift check failed for '${agent.code_name}': ${err.message}`);
11740
- }
11741
- if (channelConfigConverged) {
11914
+ if (agentFw === "claude-code") {
11742
11915
  try {
11743
- checkChannelSecretDriftAndScheduleRestart(agent.code_name, getProjectDir(agent.code_name));
11916
+ checkMcpConfigDriftAndScheduleRestart(agent.code_name, getProjectDir(agent.code_name));
11744
11917
  } catch (err) {
11745
- log(`[hot-reload] channel-secret drift check failed for '${agent.code_name}': ${err.message}`);
11918
+ log(`[hot-reload] .mcp.json drift check failed for '${agent.code_name}': ${err.message}`);
11746
11919
  }
11747
- }
11748
- try {
11749
- const sess = getSessionState(agent.code_name);
11750
- let mcpJsonParsed = null;
11751
- try {
11752
- const mcpPath = join21(getProjectDir(agent.code_name), ".mcp.json");
11753
- mcpJsonParsed = JSON.parse(readFileSync18(mcpPath, "utf-8"));
11754
- } catch {
11920
+ if (channelConfigConverged) {
11921
+ try {
11922
+ checkChannelSecretDriftAndScheduleRestart(agent.code_name, getProjectDir(agent.code_name));
11923
+ } catch (err) {
11924
+ log(`[hot-reload] channel-secret drift check failed for '${agent.code_name}': ${err.message}`);
11925
+ }
11755
11926
  }
11756
- reapMissingMcpSessions({
11757
- log,
11758
- codeName: agent.code_name,
11759
- mcpJson: mcpJsonParsed,
11760
- sessionStartedAt: sess?.startedAt ?? null,
11761
- // ENG-6660: under Docker the MCP children live in the agent's container
11762
- // PID namespace - probe inside it (docker exec ps) instead of the host,
11763
- // or every declared MCP looks "missing" -> restart loop -> flapping.
11764
- isolated: isolationMode(agent.code_name) === "docker",
11765
- // ENG-6486: a restart is already scheduled/deferred for this agent (e.g.
11766
- // a freshly-added channel awaiting its convergence restart, deferred by
11767
- // the maintenance window). The pending restart will spawn the declared-
11768
- // but-not-yet-live MCP, so don't let the reaper re-fire + inflate the
11769
- // McpReaperRestartsHourly metric into a false flapping alarm.
11770
- restartPending: pendingSessionRestarts.has(agent.code_name),
11771
- // ENG-5441 / ENG-5547: a mcp-presence-reaper restart counts against
11772
- // the agent-wide breaker ONLY when several distinct MCP servers are
11773
- // missing at once — a thrashing collection that rotates restarts of
11774
- // the whole session, which the per-MCP cap can't see. A restart
11775
- // driven by a SINGLE persistently-failing server is left to that
11776
- // server's own ENG-5279 give-up cap (→ marked unhealthy via onGiveUp
11777
- // → dropped from the next .mcp.json), so one bad integration is
11778
- // isolated and the agent keeps running degraded instead of being
11779
- // paused wholesale. reaperRestartBreakerReason() encodes that
11780
- // single-vs-multi decision; undefined means "restart, don't count".
11781
- //
11782
- // ENG-6264: the breaker-count reason (above, undefined for a single dead
11783
- // MCP) is decoupled from the GATE reason. Pre-6264 an undefined breaker
11784
- // reason also made the restart non-gateable → 'bypass' → the session was
11785
- // torn down mid-turn (the common single-MCP case interrupted busy
11786
- // agents). Always pass 'mcp-presence-reaper' as the gate reason so the
11787
- // restart defers-until-idle, while breakerReason still governs whether it
11788
- // counts against the breaker.
11789
- stopSession: (codeName, ctx) => stopPersistentSessionAndForgetMcpBaseline(
11790
- codeName,
11791
- reaperRestartBreakerReason(ctx.activeKeys),
11792
- "mcp-presence-reaper"
11793
- ),
11794
- // ENG-5292: when the reaper gives up on a managed MCP (cap from
11795
- // ENG-5279 + state-preservation from ENG-5285 both said "this
11796
- // MCP keeps failing after 3 restart cycles"), mark the matching
11797
- // integration unhealthy server-side. The next /host/managed-
11798
- // toolkits poll drops it from the response → next writeMcpServer
11799
- // omits it → broken MCP is cleanly gone instead of declared-but-
11800
- // dead.
11801
- //
11802
- // Async fire-and-forget — the reaper's correctness must not
11803
- // depend on the API call succeeding. Errors are caught inside
11804
- // the .then/.catch chain and logged.
11805
- onGiveUp: (codeName, serverKey) => {
11806
- const mapping = managedToolkitIdByAgentAndServerId.get(
11807
- managedToolkitMapKey(agent.agent_id, serverKey)
11808
- );
11809
- if (!mapping) {
11810
- log(
11811
- `[mcp-presence-reaper] give-up on '${codeName}:${serverKey}' \u2014 not a managed toolkit (no toolkit_id mapping), skipping integration-health API call`
11812
- );
11813
- return;
11814
- }
11815
- const { toolkitId, connectionKey } = mapping;
11816
- const agentId = agent.agent_id;
11817
- api.post("/host/integration-health", {
11818
- agent_id: agentId,
11819
- toolkit_id: toolkitId,
11820
- // ENG-7543: scope the mark-unhealthy to the specific connection whose
11821
- // MCP server gave up, so a dead 2nd mailbox doesn't disable the first.
11822
- connection_key: connectionKey,
11823
- action: "mark_unhealthy",
11824
- reason: `mcp-presence-reaper cap (${3} consecutive failed restart cycles) tripped on '${serverKey}'`
11825
- }).then((res) => {
11826
- log(
11827
- `[mcp-presence-reaper] marked integration unhealthy for '${codeName}' toolkit '${toolkitId}' (ENG-5292): ${JSON.stringify(res).slice(0, 200)}`
11828
- );
11829
- }).catch((err) => {
11830
- log(
11831
- `[mcp-presence-reaper] failed to mark integration unhealthy for '${codeName}' toolkit '${toolkitId}' (ENG-5292): ${err.message} \u2014 local give-up state still recorded; will retry on next agent provisioning cycle if .mcp.json changes`
11832
- );
11833
- });
11834
- },
11835
- // ENG-5286: record the restart event with the API so the
11836
- // agent-heartbeat-monitor cron can aggregate per-agent counts
11837
- // into the McpReaperRestartsHourly CloudWatch metric. Fire-and-
11838
- // forget — the reaper's correctness must not depend on this.
11839
- onRestart: (codeName, { activeKeys, givenUpKeys }) => {
11840
- api.post("/host/mcp-reaper-restart", {
11841
- agent_id: agent.agent_id,
11842
- code_name: codeName,
11843
- server_keys: activeKeys,
11844
- given_up_keys: givenUpKeys
11845
- }).catch((err) => {
11846
- log(
11847
- `[mcp-presence-reaper] failed to record restart event for '${codeName}' (ENG-5286): ${err.message} \u2014 local restart still proceeded; CloudWatch metric will under-count this event`
11848
- );
11849
- });
11850
- void reportReaperEvent(
11851
- agent.agent_id,
11852
- "mcp-presence-reaper",
11853
- "restart",
11854
- givenUpKeys.length > 0 ? `restarted session; MCP server(s) still down after restart budget: ${givenUpKeys.join(", ")}` : `restarted session to recover MCP server(s): ${activeKeys.join(", ")}`
11855
- );
11856
- },
11857
- // ENG-5932: classify each declared key for the quarantine path. Reads a
11858
- // server-declared criticality field off the .mcp.json entry when
11859
- // present (Slice 2 will stamp it); otherwise fails closed to ESSENTIAL
11860
- // for everything except the known optional channel set. Tool servers
11861
- // (composio_*/xero) classify ESSENTIAL here and stay on the existing
11862
- // managed-toolkit onGiveUp path above.
11863
- classifyKey: (serverKey) => classifyChannelCriticality(
11864
- serverKey,
11865
- mcpJsonParsed?.mcpServers?.[serverKey]
11866
- ),
11867
- quarantineDwellMs: channelQuarantineDwellMs(),
11868
- quarantineMode: channelQuarantineMode(),
11869
- // ENG-5932: enforce-mode quarantine. Persist the local marker; the next
11870
- // provisioning poll then omits the channel from .mcp.json + the launch
11871
- // flags, and the channel-set-change / .mcp.json-drift restart paths are
11872
- // taught to NOT bounce the session for a quarantine-driven removal.
11873
- // Local-authoritative: no API call — with the control plane down, the
11874
- // flap still stops.
11875
- onQuarantine: (codeName, serverKey) => {
11876
- const newly = channelQuarantineStore().quarantine(
11927
+ try {
11928
+ const sess = getSessionState(agent.code_name);
11929
+ let mcpJsonParsed = null;
11930
+ try {
11931
+ const mcpPath = join21(getProjectDir(agent.code_name), ".mcp.json");
11932
+ mcpJsonParsed = JSON.parse(readFileSync18(mcpPath, "utf-8"));
11933
+ } catch {
11934
+ }
11935
+ reapMissingMcpSessions({
11936
+ log,
11937
+ codeName: agent.code_name,
11938
+ mcpJson: mcpJsonParsed,
11939
+ sessionStartedAt: sess?.startedAt ?? null,
11940
+ // ENG-6660: under Docker the MCP children live in the agent's container
11941
+ // PID namespace - probe inside it (docker exec ps) instead of the host,
11942
+ // or every declared MCP looks "missing" -> restart loop -> flapping.
11943
+ isolated: isolationMode(agent.code_name) === "docker",
11944
+ // ENG-6486: a restart is already scheduled/deferred for this agent (e.g.
11945
+ // a freshly-added channel awaiting its convergence restart, deferred by
11946
+ // the maintenance window). The pending restart will spawn the declared-
11947
+ // but-not-yet-live MCP, so don't let the reaper re-fire + inflate the
11948
+ // McpReaperRestartsHourly metric into a false flapping alarm.
11949
+ restartPending: pendingSessionRestarts.has(agent.code_name),
11950
+ // ENG-5441 / ENG-5547: a mcp-presence-reaper restart counts against
11951
+ // the agent-wide breaker ONLY when several distinct MCP servers are
11952
+ // missing at once — a thrashing collection that rotates restarts of
11953
+ // the whole session, which the per-MCP cap can't see. A restart
11954
+ // driven by a SINGLE persistently-failing server is left to that
11955
+ // server's own ENG-5279 give-up cap (→ marked unhealthy via onGiveUp
11956
+ // → dropped from the next .mcp.json), so one bad integration is
11957
+ // isolated and the agent keeps running degraded instead of being
11958
+ // paused wholesale. reaperRestartBreakerReason() encodes that
11959
+ // single-vs-multi decision; undefined means "restart, don't count".
11960
+ //
11961
+ // ENG-6264: the breaker-count reason (above, undefined for a single dead
11962
+ // MCP) is decoupled from the GATE reason. Pre-6264 an undefined breaker
11963
+ // reason also made the restart non-gateable → 'bypass' → the session was
11964
+ // torn down mid-turn (the common single-MCP case interrupted busy
11965
+ // agents). Always pass 'mcp-presence-reaper' as the gate reason so the
11966
+ // restart defers-until-idle, while breakerReason still governs whether it
11967
+ // counts against the breaker.
11968
+ stopSession: (codeName, ctx) => stopPersistentSessionAndForgetMcpBaseline(
11877
11969
  codeName,
11878
- serverKey,
11879
- `mcp-presence-reaper: optional channel dead past restart budget + dwell`,
11880
- Date.now()
11881
- );
11882
- if (newly) {
11883
- log(
11884
- `[channel-quarantine] persisted quarantine marker for '${codeName}:${serverKey}' \u2014 will be dropped from .mcp.json on next provisioning poll (ENG-5932)`
11970
+ reaperRestartBreakerReason(ctx.activeKeys),
11971
+ "mcp-presence-reaper"
11972
+ ),
11973
+ // ENG-5292: when the reaper gives up on a managed MCP (cap from
11974
+ // ENG-5279 + state-preservation from ENG-5285 both said "this
11975
+ // MCP keeps failing after 3 restart cycles"), mark the matching
11976
+ // integration unhealthy server-side. The next /host/managed-
11977
+ // toolkits poll drops it from the response → next writeMcpServer
11978
+ // omits it → broken MCP is cleanly gone instead of declared-but-
11979
+ // dead.
11980
+ //
11981
+ // Async fire-and-forget — the reaper's correctness must not
11982
+ // depend on the API call succeeding. Errors are caught inside
11983
+ // the .then/.catch chain and logged.
11984
+ onGiveUp: (codeName, serverKey) => {
11985
+ const mapping = managedToolkitIdByAgentAndServerId.get(
11986
+ managedToolkitMapKey(agent.agent_id, serverKey)
11885
11987
  );
11988
+ if (!mapping) {
11989
+ log(
11990
+ `[mcp-presence-reaper] give-up on '${codeName}:${serverKey}' \u2014 not a managed toolkit (no toolkit_id mapping), skipping integration-health API call`
11991
+ );
11992
+ return;
11993
+ }
11994
+ const { toolkitId, connectionKey } = mapping;
11995
+ const agentId = agent.agent_id;
11996
+ api.post("/host/integration-health", {
11997
+ agent_id: agentId,
11998
+ toolkit_id: toolkitId,
11999
+ // ENG-7543: scope the mark-unhealthy to the specific connection whose
12000
+ // MCP server gave up, so a dead 2nd mailbox doesn't disable the first.
12001
+ connection_key: connectionKey,
12002
+ action: "mark_unhealthy",
12003
+ reason: `mcp-presence-reaper cap (${3} consecutive failed restart cycles) tripped on '${serverKey}'`
12004
+ }).then((res) => {
12005
+ log(
12006
+ `[mcp-presence-reaper] marked integration unhealthy for '${codeName}' toolkit '${toolkitId}' (ENG-5292): ${JSON.stringify(res).slice(0, 200)}`
12007
+ );
12008
+ }).catch((err) => {
12009
+ log(
12010
+ `[mcp-presence-reaper] failed to mark integration unhealthy for '${codeName}' toolkit '${toolkitId}' (ENG-5292): ${err.message} \u2014 local give-up state still recorded; will retry on next agent provisioning cycle if .mcp.json changes`
12011
+ );
12012
+ });
12013
+ },
12014
+ // ENG-5286: record the restart event with the API so the
12015
+ // agent-heartbeat-monitor cron can aggregate per-agent counts
12016
+ // into the McpReaperRestartsHourly CloudWatch metric. Fire-and-
12017
+ // forget — the reaper's correctness must not depend on this.
12018
+ onRestart: (codeName, { activeKeys, givenUpKeys }) => {
12019
+ api.post("/host/mcp-reaper-restart", {
12020
+ agent_id: agent.agent_id,
12021
+ code_name: codeName,
12022
+ server_keys: activeKeys,
12023
+ given_up_keys: givenUpKeys
12024
+ }).catch((err) => {
12025
+ log(
12026
+ `[mcp-presence-reaper] failed to record restart event for '${codeName}' (ENG-5286): ${err.message} \u2014 local restart still proceeded; CloudWatch metric will under-count this event`
12027
+ );
12028
+ });
11886
12029
  void reportReaperEvent(
11887
12030
  agent.agent_id,
11888
12031
  "mcp-presence-reaper",
11889
- "quarantine",
11890
- `optional channel '${serverKey}' dead past restart budget + dwell \u2014 quarantined`
12032
+ "restart",
12033
+ givenUpKeys.length > 0 ? `restarted session; MCP server(s) still down after restart budget: ${givenUpKeys.join(", ")}` : `restarted session to recover MCP server(s): ${activeKeys.join(", ")}`
12034
+ );
12035
+ },
12036
+ // ENG-5932: classify each declared key for the quarantine path. Reads a
12037
+ // server-declared criticality field off the .mcp.json entry when
12038
+ // present (Slice 2 will stamp it); otherwise fails closed to ESSENTIAL
12039
+ // for everything except the known optional channel set. Tool servers
12040
+ // (composio_*/xero) classify ESSENTIAL here and stay on the existing
12041
+ // managed-toolkit onGiveUp path above.
12042
+ classifyKey: (serverKey) => classifyChannelCriticality(
12043
+ serverKey,
12044
+ mcpJsonParsed?.mcpServers?.[serverKey]
12045
+ ),
12046
+ quarantineDwellMs: channelQuarantineDwellMs(),
12047
+ quarantineMode: channelQuarantineMode(),
12048
+ // ENG-5932: enforce-mode quarantine. Persist the local marker; the next
12049
+ // provisioning poll then omits the channel from .mcp.json + the launch
12050
+ // flags, and the channel-set-change / .mcp.json-drift restart paths are
12051
+ // taught to NOT bounce the session for a quarantine-driven removal.
12052
+ // Local-authoritative: no API call — with the control plane down, the
12053
+ // flap still stops.
12054
+ onQuarantine: (codeName, serverKey) => {
12055
+ const newly = channelQuarantineStore().quarantine(
12056
+ codeName,
12057
+ serverKey,
12058
+ `mcp-presence-reaper: optional channel dead past restart budget + dwell`,
12059
+ Date.now()
11891
12060
  );
12061
+ if (newly) {
12062
+ log(
12063
+ `[channel-quarantine] persisted quarantine marker for '${codeName}:${serverKey}' \u2014 will be dropped from .mcp.json on next provisioning poll (ENG-5932)`
12064
+ );
12065
+ void reportReaperEvent(
12066
+ agent.agent_id,
12067
+ "mcp-presence-reaper",
12068
+ "quarantine",
12069
+ `optional channel '${serverKey}' dead past restart budget + dwell \u2014 quarantined`
12070
+ );
12071
+ }
11892
12072
  }
11893
- }
11894
- });
11895
- } catch (err) {
11896
- log(`[mcp-presence-reaper] presence check failed for '${agent.code_name}': ${err.message}`);
12073
+ });
12074
+ } catch (err) {
12075
+ log(`[mcp-presence-reaper] presence check failed for '${agent.code_name}': ${err.message}`);
12076
+ }
12077
+ }
12078
+ if (agentFw === "opencode" && tasks.length > 0) {
12079
+ await syncAndCheckOpencodeScheduler(agent, tasks, refreshData);
11897
12080
  }
11898
12081
  } else if (agentFw === "claude-code" && tasks.length > 0) {
11899
12082
  await syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData);
@@ -12161,7 +12344,7 @@ async function ensureOpencodeRuntime(agent, refreshData, agentTimezone) {
12161
12344
  return { decision: "healthy", spawnAttempted: false, sessionHealthyAfter: true };
12162
12345
  }
12163
12346
  stopOpencodeSlackIngest(codeName, log);
12164
- const opencodeProjectDir = join21(getFramework("opencode").getAgentDir(codeName), "project");
12347
+ const opencodeProjectDir = join21(getFramework("opencode").getAgentDir(codeName), "provision");
12165
12348
  const openRouterRaw = refreshData.openrouter ?? null;
12166
12349
  const serveEnv = {
12167
12350
  AGT_HOST: requireHost(),
@@ -12360,7 +12543,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
12360
12543
  const ctx = getLastFailureContext(codeName);
12361
12544
  const recovery = prepareForRespawn(codeName);
12362
12545
  const tailSummary = !ctx.tail ? "" : KNOWN_SAFE_TAIL_SIGNATURES.has(ctx.signature) ? `; last pane output (${PANE_TAIL_PREVIEW_LINES} of ~20 lines):
12363
- ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash12("sha256").update(ctx.tail).digest("hex").slice(0, 12)} (raw at ~/.augmented/${codeName}/pane.log)`;
12546
+ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash13("sha256").update(ctx.tail).digest("hex").slice(0, 12)} (raw at ~/.augmented/${codeName}/pane.log)`;
12364
12547
  const sigSummary = ctx.signature !== "unknown" ? `; signature=${ctx.signature}` : "";
12365
12548
  const recoverySummary = recovery ? `; recovery=${recovery}` : "";
12366
12549
  log(
@@ -12374,7 +12557,7 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash12("sha256")
12374
12557
  );
12375
12558
  getHostId().then((hostId) => {
12376
12559
  if (!hostId) return;
12377
- const paneTailHash = zombie.paneTail ? `sha256:${createHash12("sha256").update(zombie.paneTail).digest("hex").slice(0, 12)}` : null;
12560
+ const paneTailHash = zombie.paneTail ? `sha256:${createHash13("sha256").update(zombie.paneTail).digest("hex").slice(0, 12)}` : null;
12378
12561
  return api.post("/host/events", {
12379
12562
  host_id: hostId,
12380
12563
  agent_code_name: codeName,
@@ -12523,7 +12706,7 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash12("sha256")
12523
12706
  if (!claudeAuthTupleBySession.has(codeName)) {
12524
12707
  claudeAuthTupleBySession.set(codeName, currentAuthTuple);
12525
12708
  }
12526
- const stableTasksHash = createHash12("sha256").update(JSON.stringify(tasks)).digest("hex").slice(0, 16);
12709
+ const stableTasksHash = createHash13("sha256").update(JSON.stringify(tasks)).digest("hex").slice(0, 16);
12527
12710
  const prevHash = agentState.knownTasksHashes.get(agent.agent_id);
12528
12711
  if (stableTasksHash !== prevHash) {
12529
12712
  const taskInputs = tasks.map((t) => buildSchedulerTaskInput(t));
@@ -13462,8 +13645,15 @@ function generateArtifacts(agent, refreshData, adapter) {
13462
13645
  log(
13463
13646
  `[provision/active-tasks] agent=${agent.code_name} count=${activeTasksFromRefresh.length} estimated_tokens=${activeTasksTokens}`
13464
13647
  );
13648
+ const provisionAgent = (() => {
13649
+ const rawAgent = refreshData.agent;
13650
+ if (rawAgent?.["framework"] !== "opencode") return rawAgent;
13651
+ const hostOverride = process.env["AGT_OPENCODE_MODEL"]?.trim();
13652
+ const resolved = hostOverride || resolveModelChain(refreshData).primary || rawAgent?.["primary_model"];
13653
+ return resolved ? { ...rawAgent, primary_model: resolved } : rawAgent;
13654
+ })();
13465
13655
  const provisionInput = {
13466
- agent: refreshData.agent,
13656
+ agent: provisionAgent,
13467
13657
  charterFrontmatter,
13468
13658
  charterContent,
13469
13659
  toolsFrontmatter,
@@ -13551,7 +13741,7 @@ async function syncMemories(agent, configDir, log2) {
13551
13741
  if (!file.endsWith(".md")) continue;
13552
13742
  try {
13553
13743
  const raw = readFileSync18(join21(memoryDir, file), "utf-8");
13554
- const fileHash = createHash12("sha256").update(raw).digest("hex").slice(0, 16);
13744
+ const fileHash = createHash13("sha256").update(raw).digest("hex").slice(0, 16);
13555
13745
  currentHashes.set(file, fileHash);
13556
13746
  if (prevHashes.get(file) === fileHash) continue;
13557
13747
  const parsed = parseMemoryFile(raw, file.replace(/\.md$/, ""));
@@ -13589,14 +13779,14 @@ async function syncMemories(agent, configDir, log2) {
13589
13779
  }
13590
13780
  async function downloadMemories(agent, memoryDir, log2, { force }) {
13591
13781
  const localFiles = existsSync12(memoryDir) ? readdirSync5(memoryDir).filter((f) => f.endsWith(".md")).sort() : [];
13592
- const localListHash = createHash12("sha256").update(localFiles.join(",")).digest("hex").slice(0, 16);
13782
+ const localListHash = createHash13("sha256").update(localFiles.join(",")).digest("hex").slice(0, 16);
13593
13783
  const prevLocalHash = lastLocalFileHash.get(agent.agent_id);
13594
13784
  const prevDownload = lastDownloadHash.get(agent.agent_id);
13595
13785
  try {
13596
13786
  const dbMemories = await api.post("/host/memories", {
13597
13787
  agent_id: agent.agent_id
13598
13788
  });
13599
- const responseHash = createHash12("sha256").update(JSON.stringify(dbMemories.memories ?? [])).digest("hex").slice(0, 16);
13789
+ const responseHash = createHash13("sha256").update(JSON.stringify(dbMemories.memories ?? [])).digest("hex").slice(0, 16);
13600
13790
  if (!force && prevDownload && prevLocalHash === localListHash && lastDownloadHash.get(agent.agent_id) === responseHash) {
13601
13791
  return true;
13602
13792
  }
@@ -13635,7 +13825,7 @@ ${mem.content}
13635
13825
  }
13636
13826
  if (written > 0 || overwritten > 0) {
13637
13827
  const updatedFiles = readdirSync5(memoryDir).filter((f) => f.endsWith(".md")).sort();
13638
- lastLocalFileHash.set(agent.agent_id, createHash12("sha256").update(updatedFiles.join(",")).digest("hex").slice(0, 16));
13828
+ lastLocalFileHash.set(agent.agent_id, createHash13("sha256").update(updatedFiles.join(",")).digest("hex").slice(0, 16));
13639
13829
  log2(`Memory download for '${agent.code_name}': wrote ${written} new, overwrote ${overwritten} stale`);
13640
13830
  }
13641
13831
  }
@@ -14051,7 +14241,7 @@ function deployMcpAssets() {
14051
14241
  const fileHash = (p) => {
14052
14242
  try {
14053
14243
  if (!existsSync12(p)) return null;
14054
- return createHash12("sha256").update(readFileSync18(p)).digest("hex");
14244
+ return createHash13("sha256").update(readFileSync18(p)).digest("hex");
14055
14245
  } catch {
14056
14246
  return null;
14057
14247
  }