@markus-global/cli 0.6.2 → 0.6.4

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/markus.mjs CHANGED
@@ -5184,12 +5184,45 @@ ${truncatedContent}
5184
5184
  `;
5185
5185
  }
5186
5186
  if (updated.length > MEMORY_MD_TOTAL_MAX_CHARS) {
5187
- log5.warn("MEMORY.md total size exceeds limit, refusing write", {
5187
+ log5.warn("MEMORY.md total size exceeds limit, attempting compression", {
5188
5188
  key: key2,
5189
5189
  fileSize: updated.length,
5190
5190
  limit: MEMORY_MD_TOTAL_MAX_CHARS
5191
5191
  });
5192
- return;
5192
+ const compressed = this.compressLongTermMemory();
5193
+ if (compressed.charsAfter < updated.length) {
5194
+ log5.info("Compression freed space, retrying write", {
5195
+ key: key2,
5196
+ charsFreed: updated.length - compressed.charsAfter
5197
+ });
5198
+ existing = readFileSync4(this.longTermFile, "utf-8");
5199
+ if (existing.includes(sectionHeader)) {
5200
+ const regex = new RegExp(`(## ${key2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})\\n[\\s\\S]*?(?=\\n## |$)`);
5201
+ updated = existing.replace(regex, `${sectionHeader}
5202
+ ${truncatedContent}
5203
+ `);
5204
+ } else {
5205
+ updated = existing + `
5206
+ ${sectionHeader}
5207
+ ${truncatedContent}
5208
+ `;
5209
+ }
5210
+ if (updated.length > MEMORY_MD_TOTAL_MAX_CHARS) {
5211
+ log5.warn("MEMORY.md still exceeds limit even after compression, refusing write", {
5212
+ key: key2,
5213
+ fileSize: updated.length,
5214
+ limit: MEMORY_MD_TOTAL_MAX_CHARS
5215
+ });
5216
+ return;
5217
+ }
5218
+ } else {
5219
+ log5.warn("MEMORY.md still exceeds limit after compression, refusing write", {
5220
+ key: key2,
5221
+ fileSize: updated.length,
5222
+ limit: MEMORY_MD_TOTAL_MAX_CHARS
5223
+ });
5224
+ return;
5225
+ }
5193
5226
  }
5194
5227
  writeFileSync3(this.longTermFile, updated);
5195
5228
  log5.debug("Long-term memory updated", { key: key2, sectionChars: truncatedContent.length, totalChars: updated.length });
@@ -5365,6 +5398,62 @@ ${tail}`;
5365
5398
  this.saveDebounce = null;
5366
5399
  }, 1e3);
5367
5400
  }
5401
+ /** Compress MEMORY.md — truncate oversized sections to prevent context bloat */
5402
+ compressLongTermMemory() {
5403
+ if (!existsSync6(this.longTermFile)) {
5404
+ return { charsBefore: 0, charsAfter: 0, sectionsBefore: 0, sectionsAfter: 0, truncatedChunks: 0 };
5405
+ }
5406
+ const content = readFileSync4(this.longTermFile, "utf-8");
5407
+ const charsBefore = content.length;
5408
+ const lines = content.split("\n");
5409
+ let i = 0;
5410
+ const preambleLines = [];
5411
+ while (i < lines.length && !lines[i].startsWith("## ")) {
5412
+ preambleLines.push(lines[i]);
5413
+ i++;
5414
+ }
5415
+ const sections = [];
5416
+ let currentHeader = "";
5417
+ let currentBody = [];
5418
+ while (i < lines.length) {
5419
+ const line = lines[i];
5420
+ if (line.startsWith("## ")) {
5421
+ if (currentHeader) {
5422
+ sections.push({ headerLine: currentHeader, body: currentBody });
5423
+ }
5424
+ currentHeader = line;
5425
+ currentBody = [];
5426
+ } else {
5427
+ currentBody.push(line);
5428
+ }
5429
+ i++;
5430
+ }
5431
+ if (currentHeader) {
5432
+ sections.push({ headerLine: currentHeader, body: currentBody });
5433
+ }
5434
+ const sectionsBefore = sections.length;
5435
+ let truncatedChunks = 0;
5436
+ const outputLines = [...preambleLines];
5437
+ for (const section4 of sections) {
5438
+ const bodyStr = section4.body.join("\n");
5439
+ if (bodyStr.length > MEMORY_MD_SECTION_MAX_CHARS) {
5440
+ const truncatedBody = bodyStr.slice(0, MEMORY_MD_SECTION_MAX_CHARS);
5441
+ outputLines.push(section4.headerLine, truncatedBody);
5442
+ truncatedChunks++;
5443
+ } else {
5444
+ outputLines.push(section4.headerLine, bodyStr);
5445
+ }
5446
+ }
5447
+ const compressed = outputLines.join("\n");
5448
+ writeFileSync3(this.longTermFile, compressed);
5449
+ return {
5450
+ charsBefore,
5451
+ charsAfter: compressed.length,
5452
+ sectionsBefore,
5453
+ sectionsAfter: sections.length,
5454
+ truncatedChunks
5455
+ };
5456
+ }
5368
5457
  };
5369
5458
  }
5370
5459
  });
@@ -6455,10 +6544,11 @@ var init_context_engine = __esm({
6455
6544
  if (opts.assignedTasks && opts.assignedTasks.length > 0) {
6456
6545
  const priorityOrder = ["critical", "high", "medium", "low"];
6457
6546
  const byPriority = (a, b) => priorityOrder.indexOf(a.priority ?? "medium") - priorityOrder.indexOf(b.priority ?? "medium");
6547
+ const CLOSED_STATUSES = /* @__PURE__ */ new Set(["completed", "cancelled", "failed", "archived", "rejected"]);
6458
6548
  const myTasks = opts.assignedTasks.filter((t) => t.assignedAgentId === opts.agentId);
6459
6549
  const otherTasks = opts.assignedTasks.filter((t) => t.assignedAgentId !== opts.agentId);
6460
- const myActive = myTasks.filter((t) => !["completed", "cancelled", "failed"].includes(t.status)).sort(byPriority);
6461
- const myDone = myTasks.filter((t) => ["completed", "cancelled", "failed"].includes(t.status));
6550
+ const myActive = myTasks.filter((t) => !CLOSED_STATUSES.has(t.status)).sort(byPriority);
6551
+ const myDone = myTasks.filter((t) => CLOSED_STATUSES.has(t.status));
6462
6552
  const MY_TASK_LIMIT = SYSTEM_MY_TASKS_MAX;
6463
6553
  const TEAM_TASK_LIMIT = SYSTEM_TEAM_TASKS_MAX;
6464
6554
  parts.push("\n## Task Board");
@@ -6480,8 +6570,8 @@ var init_context_engine = __esm({
6480
6570
  parts.push(`_(${myDone.length} completed/closed tasks)_`);
6481
6571
  }
6482
6572
  if (otherTasks.length > 0) {
6483
- const otherActive = otherTasks.filter((t) => !["completed", "cancelled", "failed"].includes(t.status)).sort(byPriority);
6484
- const otherDone = otherTasks.filter((t) => ["completed", "cancelled", "failed"].includes(t.status));
6573
+ const otherActive = otherTasks.filter((t) => !CLOSED_STATUSES.has(t.status)).sort(byPriority);
6574
+ const otherDone = otherTasks.filter((t) => CLOSED_STATUSES.has(t.status));
6485
6575
  if (otherActive.length > 0) {
6486
6576
  parts.push("### Team Tasks (assigned to others):");
6487
6577
  const shown = otherActive.slice(0, TEAM_TASK_LIMIT);
@@ -6506,7 +6596,7 @@ var init_context_engine = __esm({
6506
6596
  parts.push("");
6507
6597
  parts.push("**Requirements** (governance gate):");
6508
6598
  parts.push("- `requirement_propose` \u2192 pending human approval \u2192 approved \u2192 link tasks via `requirement_id`");
6509
- parts.push("- When governance requires it, every task MUST reference an approved `requirement_id`.");
6599
+ parts.push("- Every task MUST reference an approved `requirement_id`. Use `requirement_propose` first if no requirement exists.");
6510
6600
  parts.push("");
6511
6601
  parts.push("**Task lifecycle** \u2014 Create \u2192 Execute \u2192 Review \u2192 Complete:");
6512
6602
  parts.push('- **Create**: `task_create` (REQUIRED: `assigned_agent_id`, `reviewer_id`; optional `reviewer_type`: "agent"|"human"). Check `task_list` first to avoid duplicates.');
@@ -6532,7 +6622,8 @@ var init_context_engine = __esm({
6532
6622
  parts.push("- `recall_activity` \u2014 query your own past execution logs by task or activity type. Use when you need to review what you did previously (e.g., to answer a follow-up question).");
6533
6623
  parts.push("");
6534
6624
  parts.push("**Communicating with other agents**:");
6535
- parts.push("- `agent_send_message` \u2014 send a direct message to a peer agent. Use for coordination, questions, sharing context, or instructions. The message enters their mailbox and they will process it.");
6625
+ parts.push("- `agent_send_message` \u2014 send a direct message to a peer agent. **By default this is asynchronous (fire-and-forget)**: the message enters their mailbox and you continue working without waiting. Set `wait_for_reply: true` only when you need the answer before you can proceed (rare \u2014 prefer async).");
6626
+ parts.push("- A2A messaging is inherently **non-blocking**. You send a message, the recipient processes it on their own schedule, and may reply later via their own `agent_send_message`. Do NOT spin-wait or poll for responses.");
6536
6627
  parts.push("- For substantial work requests, create a `task_create` assigned to the target agent instead of asking via message.");
6537
6628
  parts.push("- Do NOT use A2A messages for routine task status notifications \u2014 the system handles those automatically.");
6538
6629
  }
@@ -43522,6 +43613,15 @@ var init_attention = __esm({
43522
43613
  clearPreemptionSignal() {
43523
43614
  this.criticalInterruptResolve = void 0;
43524
43615
  }
43616
+ /**
43617
+ * Clear the lastYieldDecision flag set by checkYieldPoint().
43618
+ * Used by processing code that handles preemption internally (e.g. task
43619
+ * execution delegates resumption to TaskService) so the attention loop
43620
+ * completes the item normally instead of deferring it.
43621
+ */
43622
+ clearLastYieldDecision() {
43623
+ this.lastYieldDecision = void 0;
43624
+ }
43525
43625
  /**
43526
43626
  * Called from the Agent's tool loop at safe yield points (between LLM turns).
43527
43627
  * If an interrupt signal is pending, evaluates whether to continue or switch.
@@ -45629,11 +45729,19 @@ ${notification.stdoutTail}`);
45629
45729
  }
45630
45730
  case "task_status_update": {
45631
45731
  if (extra.triggerExecution && item.payload.taskId) {
45732
+ if (typeof extra.onLog !== "function") {
45733
+ log17.info("Skipping resurfaced task execution item (closures lost)", {
45734
+ agentId: this.id,
45735
+ taskId: item.payload.taskId
45736
+ });
45737
+ resolveResponse("");
45738
+ return;
45739
+ }
45632
45740
  const taskId2 = item.payload.taskId;
45633
45741
  const description = item.payload.content;
45634
- const onLog = extra.onLog ?? (() => {
45635
- });
45742
+ const onLog = extra.onLog;
45636
45743
  await this.executeTask(taskId2, description, onLog, extra.cancelToken, extra.taskProjectContext, extra.executionRound, item.payload.requirementId);
45744
+ this.attentionController.clearLastYieldDecision();
45637
45745
  resolveResponse("");
45638
45746
  return;
45639
45747
  }
@@ -47127,6 +47235,12 @@ ${chatYield.item.payload.content}`;
47127
47235
  }
47128
47236
  }
47129
47237
  async handleMessageStream(userMessage, onEvent, senderId, senderInfo, cancelToken, images, fileNames) {
47238
+ if (cancelToken?.cancelled) {
47239
+ log17.info("Stream cancelled before processing started", { agentId: this.id });
47240
+ if (this.activeTasks.size === 0)
47241
+ this.setStatus("idle");
47242
+ return "";
47243
+ }
47130
47244
  if (this.activeTasks.size === 0) {
47131
47245
  this.setStatus("working");
47132
47246
  }
@@ -47248,10 +47362,11 @@ ${chatYield.item.payload.content}`;
47248
47362
  }
47249
47363
  if (cancelToken?.cancelled) {
47250
47364
  log17.info("Stream cancelled by user during tool loop", { agentId: this.id });
47251
- if (lastResponseContent && this.currentSessionId) {
47365
+ if (this.currentSessionId) {
47366
+ const content = lastResponseContent ? lastResponseContent + "\n\n[interrupted by user]" : "[interrupted by user]";
47252
47367
  this.memory.appendMessage(this.currentSessionId, {
47253
47368
  role: "assistant",
47254
- content: lastResponseContent + "\n\n[interrupted by user]"
47369
+ content
47255
47370
  });
47256
47371
  }
47257
47372
  if (streamChatActivityId)
@@ -47271,6 +47386,19 @@ ${chatYield.item.payload.content}`;
47271
47386
  content: "[Continue from where you left off. Do not repeat what you already said.]"
47272
47387
  });
47273
47388
  } else {
47389
+ if (cancelToken?.cancelled) {
47390
+ log17.info("Stream cancelled before tool execution", { agentId: this.id });
47391
+ this.memory.appendMessage(this.currentSessionId, {
47392
+ role: "assistant",
47393
+ content: response.content + "\n\n[interrupted by user]",
47394
+ reasoningContent: response.reasoningContent
47395
+ });
47396
+ if (streamChatActivityId)
47397
+ this.endActivity(streamChatActivityId);
47398
+ if (this.activeTasks.size === 0)
47399
+ this.setStatus("idle");
47400
+ return response.content || lastResponseContent || "";
47401
+ }
47274
47402
  this.memory.appendMessage(this.currentSessionId, {
47275
47403
  role: "assistant",
47276
47404
  content: response.content,
@@ -47373,10 +47501,11 @@ ${streamYield.item.payload.content}`;
47373
47501
  const updatedMessages = preparedCont.messages;
47374
47502
  if (cancelToken?.cancelled) {
47375
47503
  log17.info("Stream cancelled before LLM re-call", { agentId: this.id });
47376
- if (lastResponseContent && this.currentSessionId) {
47504
+ if (this.currentSessionId) {
47505
+ const content = lastResponseContent ? lastResponseContent + "\n\n[interrupted by user]" : "[interrupted by user]";
47377
47506
  this.memory.appendMessage(this.currentSessionId, {
47378
47507
  role: "assistant",
47379
- content: lastResponseContent + "\n\n[interrupted by user]"
47508
+ content
47380
47509
  });
47381
47510
  }
47382
47511
  if (streamChatActivityId)
@@ -47433,11 +47562,12 @@ ${streamYield.item.payload.content}`;
47433
47562
  if (streamChatActivityId)
47434
47563
  this.endActivity(streamChatActivityId, { success: !cancelToken?.cancelled });
47435
47564
  if (cancelToken?.cancelled) {
47436
- if (lastResponseContent && this.currentSessionId) {
47565
+ if (this.currentSessionId) {
47437
47566
  try {
47567
+ const content = lastResponseContent ? lastResponseContent + "\n\n[interrupted by user]" : "[interrupted by user]";
47438
47568
  this.memory.appendMessage(this.currentSessionId, {
47439
47569
  role: "assistant",
47440
- content: lastResponseContent + "\n\n[interrupted by user]"
47570
+ content
47441
47571
  });
47442
47572
  } catch {
47443
47573
  }
@@ -48732,6 +48862,19 @@ ${escalationReason}`;
48732
48862
  }
48733
48863
  async handleHeartbeat(ctx) {
48734
48864
  log17.info("Processing heartbeat check-in");
48865
+ const queuedNonHeartbeat = this.mailbox.getQueuedItems().filter((i) => i.sourceType !== "heartbeat" && i.status === "queued").length;
48866
+ const fingerprint = `q:${queuedNonHeartbeat}`;
48867
+ if (fingerprint === this.lastHeartbeatFingerprint && queuedNonHeartbeat === 0) {
48868
+ this.consecutiveIdleHeartbeats++;
48869
+ log17.info("Heartbeat: no changes detected, skipping LLM call", {
48870
+ consecutiveIdle: this.consecutiveIdleHeartbeats
48871
+ });
48872
+ this.state.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
48873
+ this.metricsCollector.recordHeartbeat(true);
48874
+ return;
48875
+ }
48876
+ this.lastHeartbeatFingerprint = fingerprint;
48877
+ this.consecutiveIdleHeartbeats = 0;
48735
48878
  const activityId = this.startActivity("heartbeat", "Heartbeat check-in", {});
48736
48879
  let lastHeartbeatSummary = "";
48737
48880
  try {
@@ -49049,12 +49192,15 @@ ${todayLog.slice(0, HEARTBEAT_DAILY_LOG_CHARS)}
49049
49192
  * 2. Memory dream: prune, deduplicate, merge (once per day)
49050
49193
  */
49051
49194
  lastDreamDate = "";
49195
+ lastHeartbeatFingerprint = "";
49196
+ consecutiveIdleHeartbeats = 0;
49052
49197
  async consolidateMemory() {
49053
49198
  try {
49054
49199
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
49055
49200
  const entries2 = this.memory.getEntries();
49056
- if (entries2.length >= 50 && this.lastDreamDate !== today) {
49057
- this.lastDreamDate = today;
49201
+ const dreamKey = entries2.length > 500 ? `${today}_${Math.floor(Date.now() / (6 * 36e5))}` : today;
49202
+ if (entries2.length >= 50 && this.lastDreamDate !== dreamKey) {
49203
+ this.lastDreamDate = dreamKey;
49058
49204
  await this.dreamConsolidateMemory(entries2);
49059
49205
  this.pruneMemoryMd();
49060
49206
  }
@@ -49068,7 +49214,7 @@ ${todayLog.slice(0, HEARTBEAT_DAILY_LOG_CHARS)}
49068
49214
  * outdated items, and merge opportunities. Apply changes programmatically.
49069
49215
  */
49070
49216
  async dreamConsolidateMemory(entries2) {
49071
- const MAX_ENTRIES_FOR_LLM = 200;
49217
+ const MAX_ENTRIES_FOR_LLM = 500;
49072
49218
  const truncated = entries2.length > MAX_ENTRIES_FOR_LLM;
49073
49219
  const batch = truncated ? entries2.slice(-MAX_ENTRIES_FOR_LLM) : entries2;
49074
49220
  const entryList = batch.map((e, i) => {
@@ -49139,8 +49285,8 @@ ${knowledgePreview}
49139
49285
  return;
49140
49286
  }
49141
49287
  const rawPlan = JSON.parse(jsonMatch[0]);
49142
- const MAX_REMOVE_PER_CYCLE = 10;
49143
- const MAX_MERGE_PER_CYCLE = 5;
49288
+ const MAX_REMOVE_PER_CYCLE = 50;
49289
+ const MAX_MERGE_PER_CYCLE = 20;
49144
49290
  const entryIds = new Set(batch.map((e) => e.id));
49145
49291
  const plan = {
49146
49292
  remove: (rawPlan.remove ?? []).filter((id) => entryIds.has(id)).slice(0, MAX_REMOVE_PER_CYCLE),
@@ -49240,7 +49386,8 @@ ${promo.content}` : promo.content;
49240
49386
  }
49241
49387
  /**
49242
49388
  * Enforce MEMORY.md hygiene: remove daily-report sections (they belong in
49243
- * daily-logs/), strip leaked LLM <think> blocks, and enforce section size limits.
49389
+ * daily-logs/), strip leaked LLM <think> blocks, and enforce section/total
49390
+ * size limits via heuristic compression.
49244
49391
  */
49245
49392
  pruneMemoryMd() {
49246
49393
  const content = this.memory.getLongTermMemory();
@@ -49260,6 +49407,52 @@ ${promo.content}` : promo.content;
49260
49407
  if (!inDailyReport)
49261
49408
  afterSectionPrune.push(line);
49262
49409
  }
49410
+ const sections = [];
49411
+ let currentHeading = "";
49412
+ let currentBody = [];
49413
+ let sectionStart = 0;
49414
+ for (let i = 0; i <= afterSectionPrune.length; i++) {
49415
+ const line = i < afterSectionPrune.length ? afterSectionPrune[i] : void 0;
49416
+ const isHeading = line !== void 0 && /^#{1,3}\s/.test(line);
49417
+ if (isHeading || line === void 0) {
49418
+ if (currentHeading || currentBody.length > 0) {
49419
+ sections.push({
49420
+ heading: currentHeading,
49421
+ body: currentBody.join("\n").trim(),
49422
+ startIdx: sectionStart
49423
+ });
49424
+ }
49425
+ currentHeading = line ?? "";
49426
+ currentBody = [];
49427
+ sectionStart = i;
49428
+ } else {
49429
+ currentBody.push(line);
49430
+ }
49431
+ }
49432
+ const headingLastIdx = /* @__PURE__ */ new Map();
49433
+ const normalizeHeading = (h) => h.replace(/^#+\s*/, "").trim().toLowerCase();
49434
+ for (let i = 0; i < sections.length; i++) {
49435
+ const key2 = normalizeHeading(sections[i].heading);
49436
+ if (key2)
49437
+ headingLastIdx.set(key2, i);
49438
+ }
49439
+ const deduped = [];
49440
+ for (let i = 0; i < sections.length; i++) {
49441
+ const s = sections[i];
49442
+ const key2 = normalizeHeading(s.heading);
49443
+ if (key2 && headingLastIdx.get(key2) !== i && headingLastIdx.has(key2)) {
49444
+ continue;
49445
+ }
49446
+ deduped.push(s);
49447
+ }
49448
+ afterSectionPrune.length = 0;
49449
+ for (const s of deduped) {
49450
+ if (s.heading)
49451
+ afterSectionPrune.push(s.heading);
49452
+ if (s.body)
49453
+ afterSectionPrune.push(s.body);
49454
+ afterSectionPrune.push("");
49455
+ }
49263
49456
  const outputLines = [];
49264
49457
  let inThinkBlock = false;
49265
49458
  for (const line of afterSectionPrune) {
@@ -49280,6 +49473,15 @@ ${promo.content}` : promo.content;
49280
49473
  writeFileSync8(memoryMdPath, pruned + "\n");
49281
49474
  log17.info("Pruned MEMORY.md: removed daily-report sections and LLM artifacts", { agentId: this.id });
49282
49475
  }
49476
+ const compressed = this.memory.compressLongTermMemory();
49477
+ if (compressed.truncatedChunks > 0) {
49478
+ log17.info("Compressed MEMORY.md during Dream Cycle", {
49479
+ agentId: this.id,
49480
+ charsBefore: compressed.charsBefore,
49481
+ charsAfter: compressed.charsAfter,
49482
+ sectionsTrimmed: compressed.truncatedChunks
49483
+ });
49484
+ }
49283
49485
  }
49284
49486
  };
49285
49487
  }
@@ -50572,7 +50774,7 @@ function createA2ATools(ctx) {
50572
50774
  type: "string",
50573
50775
  description: 'Required when scope="channel". The channel key (e.g., "group:<teamId>" for team chats, "dm:<id1>_<id2>" for DMs).'
50574
50776
  },
50575
- limit: { type: "number", description: "Number of items to fetch (default 30, max 50)." },
50777
+ limit: { type: "number", description: "Number of items to fetch (default 80, max 200)." },
50576
50778
  before: { type: "string", description: "ISO timestamp \u2014 fetch items older than this for pagination. Omit for most recent." }
50577
50779
  },
50578
50780
  required: ["scope"]
@@ -50584,11 +50786,11 @@ function createA2ATools(ctx) {
50584
50786
  if (!channelKey) {
50585
50787
  return JSON.stringify({ status: "error", error: 'channel_key is required when scope="channel"' });
50586
50788
  }
50587
- const limit = Math.min(args["limit"] ?? 30, 50);
50789
+ const limit = Math.min(args["limit"] ?? 80, 200);
50588
50790
  const before2 = args["before"];
50589
50791
  try {
50590
50792
  const result = await ctx.getChannelMessages(channelKey, limit, before2);
50591
- const formatted = result.messages.map((m) => `[${m.createdAt}] ${m.senderType === "agent" ? `[agent] ${m.senderName}` : `[human] ${m.senderName}`}: ${m.text.slice(0, 500)}`);
50793
+ const formatted = result.messages.map((m) => `[${m.createdAt}] ${m.senderType === "agent" ? `[agent] ${m.senderName}` : `[human] ${m.senderName}`}: ${m.text.slice(0, 2e3)}`);
50592
50794
  return JSON.stringify({
50593
50795
  messages: formatted,
50594
50796
  count: result.messages.length,
@@ -51044,6 +51246,15 @@ function createAgentTaskTools(ctx) {
51044
51246
  items: { type: "string" },
51045
51247
  description: "Update the list of task IDs that block this task. Only the task creator can modify this field. Pass an empty array to clear all blockers."
51046
51248
  },
51249
+ reviewer_id: {
51250
+ type: "string",
51251
+ description: "New reviewer agent or human ID. Only the task creator or a manager can change the reviewer."
51252
+ },
51253
+ reviewer_type: {
51254
+ type: "string",
51255
+ enum: ["agent", "human"],
51256
+ description: "Whether the new reviewer is an agent or a human user. Required when changing reviewer_id."
51257
+ },
51047
51258
  schedule: {
51048
51259
  type: "object",
51049
51260
  description: 'Update the schedule for a scheduled (recurring) task. Only works on tasks with taskType "scheduled". Set either "every" OR "cron", not both.',
@@ -51064,20 +51275,30 @@ function createAgentTaskTools(ctx) {
51064
51275
  const note = args["note"];
51065
51276
  const description = args["description"];
51066
51277
  const blockedBy = args["blocked_by"];
51067
- if (blockedBy !== void 0) {
51278
+ const reviewerId = args["reviewer_id"];
51279
+ const reviewerType = args["reviewer_type"];
51280
+ if (blockedBy !== void 0 || reviewerId !== void 0) {
51068
51281
  const existing = ctx.getTask ? await ctx.getTask(taskId2) : null;
51069
51282
  const createdBy = existing?.["createdBy"];
51070
- if (createdBy && createdBy !== ctx.agentId) {
51283
+ if (blockedBy !== void 0 && createdBy && createdBy !== ctx.agentId) {
51071
51284
  return JSON.stringify({
51072
51285
  status: "denied",
51073
51286
  error: "Only the task creator can modify blocked_by. You are not the creator of this task."
51074
51287
  });
51075
51288
  }
51289
+ if (reviewerId !== void 0 && createdBy && createdBy !== ctx.agentId) {
51290
+ return JSON.stringify({
51291
+ status: "denied",
51292
+ error: "Only the task creator or a manager can change the reviewer."
51293
+ });
51294
+ }
51076
51295
  }
51077
- if ((description !== void 0 || blockedBy !== void 0) && ctx.updateTaskFields) {
51296
+ if ((description !== void 0 || blockedBy !== void 0 || reviewerId !== void 0) && ctx.updateTaskFields) {
51078
51297
  await ctx.updateTaskFields(taskId2, {
51079
51298
  ...description !== void 0 ? { description } : {},
51080
- ...blockedBy !== void 0 ? { blockedBy } : {}
51299
+ ...blockedBy !== void 0 ? { blockedBy } : {},
51300
+ ...reviewerId !== void 0 ? { reviewerId } : {},
51301
+ ...reviewerType !== void 0 ? { reviewerType } : {}
51081
51302
  });
51082
51303
  }
51083
51304
  const schedule = args["schedule"];
@@ -51266,7 +51487,7 @@ function createAgentTaskTools(ctx) {
51266
51487
  ...ctx.addTaskNote ? [
51267
51488
  {
51268
51489
  name: "task_note",
51269
- description: "Add a progress note or comment to a task without changing its status. Use this to log intermediate findings, decisions, or observations while working on a task.",
51490
+ description: "Add a one-way progress note to a task's timeline log without changing its status. Use this to record intermediate findings, decisions, or milestones. For interactive discussion with other agents or humans, use task_comment instead.",
51270
51491
  inputSchema: {
51271
51492
  type: "object",
51272
51493
  properties: {
@@ -52395,16 +52616,23 @@ function createMemoryTools(ctx) {
52395
52616
  let entries2 = semResults.map((r) => r.entry);
52396
52617
  if (type)
52397
52618
  entries2 = entries2.filter((e) => e.type === type);
52398
- log24.debug("Semantic memory search", { agentId: ctx.agentId, query: query2, results: entries2.length });
52399
- return JSON.stringify({
52400
- results: entries2.map((e) => ({
52401
- id: e.id,
52402
- type: e.type,
52403
- content: e.content,
52404
- timestamp: e.timestamp,
52405
- similarity: semResults.find((r) => r.entry.id === e.id)?.similarity
52406
- })),
52407
- count: entries2.length
52619
+ if (entries2.length > 0) {
52620
+ log24.debug("Semantic memory search", { agentId: ctx.agentId, query: query2, results: entries2.length });
52621
+ return JSON.stringify({
52622
+ results: entries2.map((e) => ({
52623
+ id: e.id,
52624
+ type: e.type,
52625
+ content: e.content,
52626
+ timestamp: e.timestamp,
52627
+ similarity: semResults.find((r) => r.entry.id === e.id)?.similarity
52628
+ })),
52629
+ count: entries2.length,
52630
+ searchMethod: "semantic"
52631
+ });
52632
+ }
52633
+ log24.info("Semantic search returned 0 results, falling back to substring", {
52634
+ agentId: ctx.agentId,
52635
+ query: query2
52408
52636
  });
52409
52637
  } catch (err) {
52410
52638
  log24.warn("Semantic search failed, falling back to substring", { error: String(err) });
@@ -52423,7 +52651,8 @@ function createMemoryTools(ctx) {
52423
52651
  timestamp: e.timestamp,
52424
52652
  tags: e.metadata?.tags
52425
52653
  })),
52426
- count: results.length
52654
+ count: results.length,
52655
+ searchMethod: "substring"
52427
52656
  });
52428
52657
  }
52429
52658
  },
@@ -52496,6 +52725,48 @@ ${content}` : content;
52496
52725
  log24.info("Agent updated long-term memory", { agentId: ctx.agentId, section: section4, mode, contentLen: content.length });
52497
52726
  return JSON.stringify({ status: "updated", section: section4, mode });
52498
52727
  }
52728
+ },
52729
+ {
52730
+ name: "memory_delete",
52731
+ description: "Delete specific entries from your memory buffer (memories.json). Use this to clean up outdated, incorrect, or redundant observations. Provide either a list of entry IDs (from memory_list/memory_search) or a tag to remove all entries with that tag. Maximum 20 entries per call.",
52732
+ inputSchema: {
52733
+ type: "object",
52734
+ properties: {
52735
+ ids: {
52736
+ type: "array",
52737
+ items: { type: "string" },
52738
+ description: "Array of memory entry IDs to delete. Use memory_list or memory_search to find IDs."
52739
+ },
52740
+ tag: {
52741
+ type: "string",
52742
+ description: "Delete all entries with this tag. Alternative to specifying individual IDs."
52743
+ }
52744
+ }
52745
+ },
52746
+ async execute(args) {
52747
+ const ids = args["ids"];
52748
+ const tag = args["tag"];
52749
+ if (!ids?.length && !tag) {
52750
+ return JSON.stringify({ status: "error", error: "Provide either ids or tag to delete." });
52751
+ }
52752
+ const MAX_DELETE = 20;
52753
+ let removed = 0;
52754
+ if (ids?.length) {
52755
+ const capped = ids.slice(0, MAX_DELETE);
52756
+ removed = ctx.memory.removeEntries(capped);
52757
+ if (ctx.semanticSearch?.isEnabled()) {
52758
+ for (const id of capped) {
52759
+ ctx.semanticSearch.deleteMemory(id).catch((err) => {
52760
+ log24.warn("Failed to remove memory from semantic index", { error: String(err) });
52761
+ });
52762
+ }
52763
+ }
52764
+ } else if (tag) {
52765
+ removed = ctx.memory.removeEntriesByTag(tag);
52766
+ }
52767
+ log24.info("Agent deleted memories", { agentId: ctx.agentId, removed, byTag: tag ?? null });
52768
+ return JSON.stringify({ status: "deleted", removed });
52769
+ }
52499
52770
  }
52500
52771
  ];
52501
52772
  }
@@ -53143,29 +53414,34 @@ var init_semantic_search = __esm({
53143
53414
  log27.warn("Failed to index memory entry", { id: entry.id, error: String(err) });
53144
53415
  }
53145
53416
  }
53417
+ /**
53418
+ * Search indexed memories by semantic similarity.
53419
+ *
53420
+ * Returns an empty array when the service is not enabled.
53421
+ *
53422
+ * @throws {Error} Propagates embedding or vector-store errors to the caller
53423
+ * so that callers (e.g. memory_search tool) can implement their own
53424
+ * fallback strategy. Contrast with {@link indexMemory}, which silently
53425
+ * swallows errors because indexing is best-effort.
53426
+ */
53146
53427
  async search(query2, opts) {
53147
53428
  if (!this.enabled)
53148
53429
  return [];
53149
- try {
53150
- const queryEmbedding = await this.embedding.embed(query2);
53151
- const results = await this.vectorStore.search(queryEmbedding, {
53152
- topK: opts?.topK ?? 5,
53153
- agentId: opts?.agentId,
53154
- minSimilarity: opts?.minSimilarity ?? 0.3
53155
- });
53156
- return results.map((r) => ({
53157
- entry: {
53158
- id: r.id,
53159
- content: r.content,
53160
- type: r.type,
53161
- timestamp: ""
53162
- },
53163
- similarity: r.similarity
53164
- }));
53165
- } catch (err) {
53166
- log27.warn("Semantic search failed, returning empty results", { error: String(err) });
53167
- return [];
53168
- }
53430
+ const queryEmbedding = await this.embedding.embed(query2);
53431
+ const results = await this.vectorStore.search(queryEmbedding, {
53432
+ topK: opts?.topK ?? 5,
53433
+ agentId: opts?.agentId,
53434
+ minSimilarity: opts?.minSimilarity ?? 0.3
53435
+ });
53436
+ return results.map((r) => ({
53437
+ entry: {
53438
+ id: r.id,
53439
+ content: r.content,
53440
+ type: r.type,
53441
+ timestamp: ""
53442
+ },
53443
+ similarity: r.similarity
53444
+ }));
53169
53445
  }
53170
53446
  async deleteMemory(id) {
53171
53447
  if (!this.enabled)
@@ -53711,6 +53987,25 @@ You are ${request.name}.`,
53711
53987
  "- [ ] Scan recent channel messages for anything requiring attention"
53712
53988
  ].join("\n"), "utf-8");
53713
53989
  }
53990
+ const sessionsDir = join12(agentDataDir, "sessions");
53991
+ const dailyLogsDir = join12(agentDataDir, "daily-logs");
53992
+ const memoryPath = join12(agentDataDir, "MEMORY.md");
53993
+ mkdirSync11(sessionsDir, { recursive: true });
53994
+ mkdirSync11(dailyLogsDir, { recursive: true });
53995
+ if (!existsSync17(memoryPath)) {
53996
+ writeFileSync10(memoryPath, [
53997
+ "# Agent Memory",
53998
+ "",
53999
+ "## Your Knowledge",
54000
+ "",
54001
+ "## procedures",
54002
+ "",
54003
+ "## architecture",
54004
+ "",
54005
+ "## lessons-learned",
54006
+ ""
54007
+ ].join("\n"), "utf-8");
54008
+ }
53714
54009
  const config = {
53715
54010
  id,
53716
54011
  name: request.name,
@@ -62237,14 +62532,15 @@ ${c.content}`;
62237
62532
  reviewer: task.reviewerId
62238
62533
  });
62239
62534
  if (this.hitlService && request.creatorRole !== "human" && task.status === "pending") {
62240
- const creatorName = request.createdBy ?? "unknown agent";
62535
+ const creatorId = request.createdBy ?? "system";
62536
+ const creatorName = this.resolveActorName(creatorId, "agent") ?? creatorId;
62241
62537
  this.hitlService.requestApprovalAndWait({
62242
- agentId: request.createdBy ?? "system",
62538
+ agentId: creatorId,
62243
62539
  agentName: creatorName,
62244
62540
  type: "custom",
62245
- title: `Task approval: ${task.title}`,
62541
+ title: task.title,
62246
62542
  description: `Agent "${creatorName}" wants to create task "${task.title}" (priority: ${task.priority}).`,
62247
- details: { taskId: task.id, priority: task.priority }
62543
+ details: { taskId: task.id, priority: task.priority, subType: "task" }
62248
62544
  }).then((result) => {
62249
62545
  const current = this.tasks.get(task.id);
62250
62546
  if (!current || current.status !== "pending")
@@ -62531,9 +62827,10 @@ ${c.content}`;
62531
62827
  Deliverables:
62532
62828
  ${deliverablesSummary}` : ""
62533
62829
  ].filter(Boolean).join("\n");
62830
+ const assigneeName = this.resolveActorName(task.assignedAgentId, "agent") ?? task.assignedAgentId;
62534
62831
  this.hitlService.requestApprovalAndWait({
62535
62832
  agentId: task.assignedAgentId,
62536
- agentName: task.assignedAgentId,
62833
+ agentName: assigneeName,
62537
62834
  type: "custom",
62538
62835
  title: `Review: ${task.title}`,
62539
62836
  description,
@@ -62891,6 +63188,12 @@ Action: ${guidance}` : ""
62891
63188
  if (data.reviewerType !== void 0)
62892
63189
  task.reviewerType = data.reviewerType;
62893
63190
  if (data.blockedBy !== void 0) {
63191
+ if (data.blockedBy.length > 0) {
63192
+ const cycle = this.detectBlockedByCycle(id, data.blockedBy);
63193
+ if (cycle) {
63194
+ throw new Error(`Circular dependency detected: ${cycle.join(" \u2192 ")}. Cannot set blocked_by \u2014 this would create a deadlock.`);
63195
+ }
63196
+ }
62894
63197
  task.blockedBy = data.blockedBy;
62895
63198
  if (this.taskRepo && "updateBlockedBy" in this.taskRepo) {
62896
63199
  this.taskRepo.updateBlockedBy(id, data.blockedBy).catch((err) => log50.warn("Failed to persist blockedBy to DB", { error: String(err) }));
@@ -63015,6 +63318,31 @@ Action: ${guidance}` : ""
63015
63318
  this.cascadeCancelDependents(task);
63016
63319
  }
63017
63320
  }
63321
+ /**
63322
+ * Detect cycles in blocked_by dependencies using BFS.
63323
+ * Returns the cycle path if found, or null if no cycle exists.
63324
+ */
63325
+ detectBlockedByCycle(taskId2, proposedBlockers) {
63326
+ for (const blockerId of proposedBlockers) {
63327
+ const visited = /* @__PURE__ */ new Set();
63328
+ const queue = [{ id: blockerId, path: [taskId2, blockerId] }];
63329
+ while (queue.length > 0) {
63330
+ const { id: current, path } = queue.shift();
63331
+ if (current === taskId2)
63332
+ return path;
63333
+ if (visited.has(current))
63334
+ continue;
63335
+ visited.add(current);
63336
+ const blockerTask = this.tasks.get(current);
63337
+ if (blockerTask?.blockedBy) {
63338
+ for (const nextId of blockerTask.blockedBy) {
63339
+ queue.push({ id: nextId, path: [...path, nextId] });
63340
+ }
63341
+ }
63342
+ }
63343
+ }
63344
+ return null;
63345
+ }
63018
63346
  areBlockersSatisfied(task) {
63019
63347
  if (!task.blockedBy?.length)
63020
63348
  return true;
@@ -64414,7 +64742,7 @@ var init_builder_service = __esm({
64414
64742
  const memberRole = member.role ?? "worker";
64415
64743
  const memberName = member.name ?? "Agent";
64416
64744
  const memberSkills = member.skills ?? [];
64417
- const memberFilesDir = this.findMemberDir(artDir, memberName, usedMemberDirs);
64745
+ const memberFilesDir = this.findMemberDir(artDir, memberName, usedMemberDirs, member.roleName);
64418
64746
  const hasCustomRole = !!memberFilesDir && existsSync24(join20(memberFilesDir, "ROLE.md"));
64419
64747
  if (memberFilesDir)
64420
64748
  usedMemberDirs.add(memberFilesDir);
@@ -64459,14 +64787,24 @@ var init_builder_service = __esm({
64459
64787
  * Find the member directory under artDir/members/ by trying multiple slug strategies.
64460
64788
  * Returns the absolute path to the member directory, or null if not found.
64461
64789
  */
64462
- findMemberDir(artDir, memberName, usedDirs) {
64790
+ findMemberDir(artDir, memberName, usedDirs, roleName) {
64463
64791
  const membersBase = join20(artDir, "members");
64464
64792
  if (!existsSync24(membersBase))
64465
64793
  return null;
64466
- const slug = kebab(memberName, "agent");
64467
- const exact = join20(membersBase, slug);
64468
- if (existsSync24(exact) && !usedDirs.has(exact))
64469
- return exact;
64794
+ const nameSlug = kebab(memberName);
64795
+ if (nameSlug && !/^pkg-/.test(nameSlug)) {
64796
+ const exact = join20(membersBase, nameSlug);
64797
+ if (existsSync24(exact) && !usedDirs.has(exact))
64798
+ return exact;
64799
+ }
64800
+ if (roleName) {
64801
+ const roleSlug = kebab(roleName);
64802
+ if (roleSlug && !/^pkg-/.test(roleSlug)) {
64803
+ const roleDir = join20(membersBase, roleSlug);
64804
+ if (existsSync24(roleDir) && !usedDirs.has(roleDir))
64805
+ return roleDir;
64806
+ }
64807
+ }
64470
64808
  try {
64471
64809
  for (const entry of readdirSync8(membersBase, { withFileTypes: true })) {
64472
64810
  if (!entry.isDirectory())
@@ -64480,8 +64818,12 @@ var init_builder_service = __esm({
64480
64818
  try {
64481
64819
  const content = readFileSync18(rolePath, "utf-8");
64482
64820
  const title = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
64483
- if (title && title.toLowerCase() === memberName.toLowerCase())
64484
- return candidateDir;
64821
+ if (title) {
64822
+ const tLower = title.toLowerCase();
64823
+ const nLower = memberName.toLowerCase();
64824
+ if (tLower === nLower || tLower.includes(nLower) || nLower.includes(tLower))
64825
+ return candidateDir;
64826
+ }
64485
64827
  } catch {
64486
64828
  }
64487
64829
  }
@@ -64997,6 +65339,9 @@ var init_sse_handler = __esm({
64997
65339
  } else if (this.options.isResume) {
64998
65340
  this.sessionId = this.options.sessionId ?? null;
64999
65341
  }
65342
+ if (this.sessionId && this.sseBuffer && !this.sseDisconnected) {
65343
+ this.sseBuffer.send({ type: "session_start", sessionId: this.sessionId });
65344
+ }
65000
65345
  const reply = await this.options.agent.sendMessageStream(this.options.userText, (event) => this.handleStreamEvent(event), this.options.senderId, this.options.senderInfo, this.cancelToken, this.options.images, this.options.fileNames);
65001
65346
  const finalNow = (/* @__PURE__ */ new Date()).toISOString();
65002
65347
  let finalThinking;
@@ -65756,7 +66101,7 @@ var init_api_server = __esm({
65756
66101
  let channelContext = [];
65757
66102
  if (this.storage) {
65758
66103
  try {
65759
- const recent = await this.storage.channelMessageRepo.getMessages(channelKey, 20);
66104
+ const recent = await this.storage.channelMessageRepo.getMessages(channelKey, 80);
65760
66105
  channelContext = (recent.messages ?? []).map((m) => ({
65761
66106
  role: m.senderType === "agent" ? "assistant" : "user",
65762
66107
  content: m.senderType === "agent" ? stripInternalBlocks(m.text) : `[${m.senderName}]: ${m.text}`
@@ -66326,13 +66671,33 @@ ${cleanText}`,
66326
66671
  toolEventCollector: toolEvents,
66327
66672
  waitForReply: isA2A ? true : void 0
66328
66673
  });
66674
+ const emitNoResponse = () => {
66675
+ const evt = {
66676
+ type: "chat:agent_no_response",
66677
+ payload: { channel, agentId: agentId2 },
66678
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
66679
+ };
66680
+ if (channel.startsWith("dm:") || channel.startsWith("notes:")) {
66681
+ const parts = channel.startsWith("notes:") ? [channel.slice(6)] : channel.slice(3).split(":");
66682
+ this.ws.sendToUsers(parts, evt);
66683
+ } else {
66684
+ const humanIds = this.resolveChannelHumanIds(channel);
66685
+ if (humanIds.length > 0)
66686
+ this.ws.sendToUsers(humanIds, evt);
66687
+ else
66688
+ this.ws.broadcast(evt);
66689
+ }
66690
+ };
66329
66691
  if (!reply || !reply.trim() || reply.includes("[NO_RESPONSE]")) {
66692
+ emitNoResponse();
66330
66693
  return;
66331
66694
  }
66332
66695
  const { thinking, clean: rawClean } = extractThinkBlocks(reply);
66333
66696
  const cleanReply = rawClean.replace(/\[NO_RESPONSE\]/gi, "").trim();
66334
- if (!cleanReply)
66697
+ if (!cleanReply) {
66698
+ emitNoResponse();
66335
66699
  return;
66700
+ }
66336
66701
  const metadata = {};
66337
66702
  if (thinking.length > 0)
66338
66703
  metadata["thinking"] = thinking;
@@ -66435,7 +66800,7 @@ ${cleanText}`,
66435
66800
  let channelContext = [];
66436
66801
  if (this.storage) {
66437
66802
  try {
66438
- const recent = await this.storage.channelMessageRepo.getMessages(channel, 20);
66803
+ const recent = await this.storage.channelMessageRepo.getMessages(channel, 80);
66439
66804
  channelContext = (recent.messages ?? []).map((m) => ({
66440
66805
  role: m.senderType === "agent" ? "assistant" : "user",
66441
66806
  content: m.senderType === "agent" ? stripInternalBlocks(m.text) : `[${m.senderName}]: ${m.text}`
@@ -66975,7 +67340,7 @@ ${cleanText}`,
66975
67340
  if (!this.storage)
66976
67341
  return [];
66977
67342
  try {
66978
- const recent = await this.storage.channelMessageRepo.getMessages(channel, 20);
67343
+ const recent = await this.storage.channelMessageRepo.getMessages(channel, 80);
66979
67344
  return (recent.messages ?? []).map((m) => ({
66980
67345
  role: m.senderType === "agent" ? "assistant" : "user",
66981
67346
  content: m.senderType === "agent" ? stripInternalBlocks(m.text) : `[${m.senderName}]: ${m.text}`
@@ -70309,9 +70674,9 @@ EXPLANATION_END`;
70309
70674
  writeFileSync16(join22(artDir, "NORMS.md"), norms, "utf-8");
70310
70675
  }
70311
70676
  const rawMembers = Array.isArray(artifact.team?.members) ? artifact.team.members : Array.isArray(artifact.members) ? artifact.members : [];
70312
- for (const m of rawMembers) {
70677
+ for (const [idx, m] of rawMembers.entries()) {
70313
70678
  const mName = m.name ?? "Agent";
70314
- const slug = kebab(mName, "agent");
70679
+ const slug = kebab(mName, "member-" + idx);
70315
70680
  const memberDir = join22(artDir, "members", slug);
70316
70681
  const roleContent = m.roleContent || m.role_md;
70317
70682
  const policiesContent = m.policiesContent || m.policies_md;
@@ -71158,6 +71523,50 @@ EXPLANATION_END`;
71158
71523
  this.json(res, 200, { success: read ?? false });
71159
71524
  return;
71160
71525
  }
71526
+ if (path === "/api/activity" && req.method === "GET") {
71527
+ const authUser = await this.requireAuth(req, res);
71528
+ if (!authUser)
71529
+ return;
71530
+ const userId2 = authUser.userId;
71531
+ const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "50", 10), 200);
71532
+ const typeFilter = url.searchParams.get("type") ?? void 0;
71533
+ const items = [];
71534
+ if (!typeFilter || typeFilter === "notification") {
71535
+ const notifications = this.hitlService?.listNotifications(userId2, false, { limit }) ?? [];
71536
+ for (const n of notifications) {
71537
+ items.push({
71538
+ id: n.id,
71539
+ type: n.type,
71540
+ title: n.title,
71541
+ body: n.body,
71542
+ timestamp: n.createdAt,
71543
+ source: "notification",
71544
+ metadata: n.metadata
71545
+ });
71546
+ }
71547
+ }
71548
+ if ((!typeFilter || typeFilter === "task_comment") && this.storage?.taskCommentRepo) {
71549
+ try {
71550
+ const recentComments = this.storage.taskCommentRepo.listRecent?.(limit) ?? [];
71551
+ for (const c of recentComments) {
71552
+ items.push({
71553
+ id: c.id,
71554
+ type: "task_comment",
71555
+ title: `Comment on task ${c.taskId}`,
71556
+ body: typeof c.body === "string" ? c.body.slice(0, 300) : String(c.body ?? ""),
71557
+ timestamp: c.createdAt,
71558
+ source: "task_comment",
71559
+ metadata: { taskId: c.taskId, authorId: c.authorId, authorName: c.authorName }
71560
+ });
71561
+ }
71562
+ } catch {
71563
+ }
71564
+ }
71565
+ items.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
71566
+ const page = items.slice(0, limit);
71567
+ this.json(res, 200, { items: page, totalCount: items.length });
71568
+ return;
71569
+ }
71161
71570
  if (path === "/api/usage" && req.method === "GET") {
71162
71571
  const orgId2 = url.searchParams.get("orgId") ?? "default";
71163
71572
  const plan = this.billingService?.getOrgPlan(orgId2);
@@ -72660,11 +73069,11 @@ EXPLANATION_END`;
72660
73069
  }
72661
73070
  const rolesRoot = rolesCandidates.find((d) => ex(d)) ?? rolesDir;
72662
73071
  const files = {};
72663
- for (const member of tpl.members) {
73072
+ for (const [idx, member] of tpl.members.entries()) {
72664
73073
  const roleName = member.roleName;
72665
73074
  if (!roleName)
72666
73075
  continue;
72667
- const memberSlug = (member.name ?? roleName).toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
73076
+ const memberSlug = kebab(member.name ?? roleName, "member-" + idx);
72668
73077
  const roleDir = resolve13(rolesRoot, roleName);
72669
73078
  if (!ex(roleDir))
72670
73079
  continue;
@@ -73793,6 +74202,8 @@ EXPLANATION_END`;
73793
74202
  exact("/api/notifications", "GET"),
73794
74203
  exact("/api/notifications/mark-all-read", "POST"),
73795
74204
  startsWith("/api/notifications/", "POST"),
74205
+ // ── Activity feed ─────────────────────────────────────────────────
74206
+ exact("/api/activity", "GET"),
73796
74207
  // ── Users ────────────────────────────────────────────────────────────
73797
74208
  exact("/api/users", "GET", "POST"),
73798
74209
  regex(/^\/api\/users\/[^/]+$/, "PATCH"),
@@ -74408,7 +74819,7 @@ var init_hitl_service = __esm({
74408
74819
  this.notify({
74409
74820
  targetUserId: opts.targetUserId ?? "all",
74410
74821
  type: "approval_request",
74411
- title: `Approval needed: ${opts.title}`,
74822
+ title: opts.title,
74412
74823
  body: opts.description,
74413
74824
  priority: "high",
74414
74825
  actionType: "navigate",
@@ -75282,13 +75693,14 @@ var init_requirement_service = __esm({
75282
75693
  }
75283
75694
  this.broadcast("requirement:created", req);
75284
75695
  if (this.hitlService && req.source === "agent") {
75696
+ const creatorName = this.resolveAgentName(req.createdBy);
75285
75697
  this.hitlService.requestApprovalAndWait({
75286
75698
  agentId: req.createdBy,
75287
- agentName: req.createdBy,
75699
+ agentName: creatorName,
75288
75700
  type: "custom",
75289
- title: `Requirement approval: ${req.title}`,
75290
- description: `Agent "${req.createdBy}" proposed requirement "${req.title}" (priority: ${req.priority}).`,
75291
- details: { requirementId: req.id, priority: req.priority },
75701
+ title: req.title,
75702
+ description: `Agent "${creatorName}" proposed requirement "${req.title}" (priority: ${req.priority}).`,
75703
+ details: { requirementId: req.id, priority: req.priority, subType: "requirement" },
75292
75704
  targetUserId: "all"
75293
75705
  }).then((result) => {
75294
75706
  const current = this.requirements.get(req.id);
@@ -75424,13 +75836,14 @@ var init_requirement_service = __esm({
75424
75836
  this.broadcast("requirement:resubmitted", req);
75425
75837
  log61.info("Requirement resubmitted for review", { id, hasUpdates: !!updates });
75426
75838
  if (this.hitlService && req.source === "agent") {
75839
+ const creatorName = this.resolveAgentName(req.createdBy);
75427
75840
  this.hitlService.requestApprovalAndWait({
75428
75841
  agentId: req.createdBy,
75429
- agentName: req.createdBy,
75842
+ agentName: creatorName,
75430
75843
  type: "custom",
75431
- title: `Requirement approval (resubmitted): ${req.title}`,
75432
- description: `Agent "${req.createdBy}" resubmitted requirement "${req.title}" (priority: ${req.priority}).`,
75433
- details: { requirementId: req.id, priority: req.priority },
75844
+ title: req.title,
75845
+ description: `Agent "${creatorName}" resubmitted requirement "${req.title}" (priority: ${req.priority}).`,
75846
+ details: { requirementId: req.id, priority: req.priority, subType: "requirement_resubmit" },
75434
75847
  targetUserId: "all"
75435
75848
  }).then((result) => {
75436
75849
  const current = this.requirements.get(req.id);
@@ -75768,6 +76181,17 @@ var init_requirement_service = __esm({
75768
76181
  });
75769
76182
  }
75770
76183
  }
76184
+ resolveAgentName(agentId2) {
76185
+ if (this.agentManager) {
76186
+ try {
76187
+ const agent = this.agentManager.getAgent(agentId2);
76188
+ if (agent)
76189
+ return agent.config?.name ?? agent.name ?? agentId2;
76190
+ } catch {
76191
+ }
76192
+ }
76193
+ return agentId2;
76194
+ }
75771
76195
  broadcast(type, data) {
75772
76196
  if (this.ws) {
75773
76197
  this.ws.broadcast({
@@ -76919,13 +77343,19 @@ var init_stale_detector = __esm({
76919
77343
  taskService;
76920
77344
  config;
76921
77345
  scanInterval;
76922
- constructor(taskService, config) {
77346
+ onStaleItems;
77347
+ constructor(taskService, config, onStaleItems) {
76923
77348
  this.taskService = taskService;
76924
77349
  this.config = { ...DEFAULT_CONFIG4, ...config };
77350
+ this.onStaleItems = onStaleItems;
76925
77351
  }
76926
77352
  start(intervalMs = 36e5) {
76927
77353
  this.scanInterval = setInterval(() => {
76928
- this.scan().catch((err) => log68.warn("Stale scan failed", { error: String(err) }));
77354
+ this.scan().then((items) => {
77355
+ if (items.length > 0 && this.onStaleItems) {
77356
+ this.onStaleItems(items);
77357
+ }
77358
+ }).catch((err) => log68.warn("Stale scan failed", { error: String(err) }));
76929
77359
  }, intervalMs);
76930
77360
  log68.info("Stale detector started", { intervalMs });
76931
77361
  }
@@ -82255,6 +82685,21 @@ async function startServer(config, values) {
82255
82685
  const archiveService = new ArchiveService(taskService, projectService);
82256
82686
  archiveService.setRequirementService(requirementService);
82257
82687
  archiveService.start();
82688
+ const staleDetector = new StaleDetector(taskService, void 0, (items) => {
82689
+ for (const item of items) {
82690
+ hitlService.notify({
82691
+ targetUserId: "all",
82692
+ type: "system",
82693
+ title: item.type === "review_stale" ? "Stale review" : item.type === "stuck_task" ? "Stuck task" : "Unstarted task",
82694
+ body: item.message,
82695
+ priority: item.type === "review_stale" ? "high" : "normal",
82696
+ actionType: item.taskId ? "navigate" : "none",
82697
+ actionTarget: item.taskId ? JSON.stringify({ path: `/work?openTask=${item.taskId}` }) : void 0,
82698
+ metadata: { taskId: item.taskId, agentId: item.agentId, staleType: item.type }
82699
+ });
82700
+ }
82701
+ });
82702
+ staleDetector.start();
82258
82703
  apiServer.setLLMRouter(llmRouter);
82259
82704
  apiServer.setConfigPath(values["config"] ?? getDefaultConfigPath());
82260
82705
  if (config.hub?.url) apiServer.setHubUrl(config.hub.url);
@@ -83091,6 +83536,7 @@ ${reason}`;
83091
83536
  closeStartupLogger();
83092
83537
  closeRuntimeLogger();
83093
83538
  archiveService.stop();
83539
+ staleDetector.stop();
83094
83540
  scheduledTaskRunner.stop();
83095
83541
  apiServer.stop();
83096
83542
  agentManager.shutdown().then(() => messageRouter.disconnectAll()).then(() => process.exit(0)).catch(() => process.exit(1));