@wrongstack/core 0.302.2 → 0.305.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +1 -1
  2. package/dist/chronicle/index.js +117 -30
  3. package/dist/chronicle/project-server.js +12 -5
  4. package/dist/coordination/agents/index.js +4645 -3448
  5. package/dist/coordination/agents/project-agent-auto-optimize.d.ts +116 -0
  6. package/dist/coordination/agents/project-agent-capture-window.d.ts +29 -0
  7. package/dist/coordination/agents/project-agent-config-io.d.ts +11 -0
  8. package/dist/coordination/agents/project-agent-consolidation.d.ts +34 -2
  9. package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
  10. package/dist/coordination/agents/project-agent-files.d.ts +12 -3
  11. package/dist/coordination/agents/project-agent-identity-types.d.ts +4 -0
  12. package/dist/coordination/agents/project-agent-identity.d.ts +38 -11
  13. package/dist/coordination/agents/project-agent-learning-entries.d.ts +8 -2
  14. package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
  15. package/dist/coordination/agents/project-agent-learning-structured.d.ts +72 -1
  16. package/dist/coordination/agents/project-agent-optimizer.d.ts +49 -0
  17. package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
  18. package/dist/coordination/agents/project-agent-skill-layer.d.ts +146 -0
  19. package/dist/coordination/agents/role-skills.d.ts +11 -1
  20. package/dist/coordination/agents/types.d.ts +10 -2
  21. package/dist/coordination/director-prompts.d.ts +19 -6
  22. package/dist/coordination/director-tools.d.ts +2 -2
  23. package/dist/coordination/fleet.d.ts +0 -6
  24. package/dist/coordination/index.d.ts +1 -1
  25. package/dist/coordination/index.js +5746 -4326
  26. package/dist/coordination/mail-tools.d.ts +1 -1
  27. package/dist/core/agent-types.d.ts +4 -2
  28. package/dist/core/agent.d.ts +1 -0
  29. package/dist/core/context.d.ts +19 -0
  30. package/dist/core/conversation-state.d.ts +14 -0
  31. package/dist/core/fallback-profile-manager.d.ts +70 -2
  32. package/dist/core/index.js +317 -108
  33. package/dist/core/system-prompt-blocks.d.ts +1 -1
  34. package/dist/core/system-prompt-builder.d.ts +13 -1
  35. package/dist/core/system-prompt-glossary.d.ts +73 -0
  36. package/dist/core/system-prompt-memory-skills.d.ts +2 -2
  37. package/dist/defaults/index.js +1635 -1120
  38. package/dist/execution/council-orchestrator.d.ts +3 -13
  39. package/dist/execution/index.js +3136 -2706
  40. package/dist/execution/one-shot-llm.d.ts +5 -0
  41. package/dist/goal/index.js +7 -0
  42. package/dist/hq/index.js +17 -7
  43. package/dist/hq/protocol/kanban.d.ts +21 -0
  44. package/dist/hq/protocol.js +5 -1
  45. package/dist/hq/redaction.d.ts +14 -0
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.js +5502 -3483
  48. package/dist/infrastructure/index.js +247 -122
  49. package/dist/kernel/events/agent-events.d.ts +28 -0
  50. package/dist/plugin/index.js +111 -7
  51. package/dist/registry/index.js +11 -0
  52. package/dist/registry/tool-registry.d.ts +8 -0
  53. package/dist/replay/hash.d.ts +9 -0
  54. package/dist/replay/index.js +14 -4
  55. package/dist/replay/replay-provider-runner.d.ts +31 -1
  56. package/dist/security/index.js +92 -21
  57. package/dist/security/kanban-boundary.d.ts +5 -1
  58. package/dist/security/secret-vault.d.ts +2 -0
  59. package/dist/session-catalog/index.js +86 -10
  60. package/dist/session-catalog/project-server.js +100 -17
  61. package/dist/session-catalog/protocol.d.ts +20 -4
  62. package/dist/session-catalog/store.d.ts +19 -3
  63. package/dist/storage/index.js +305 -69
  64. package/dist/storage/memory-consolidator.d.ts +4 -2
  65. package/dist/storage/plan-store.d.ts +1 -1
  66. package/dist/storage/session-resume-validation.d.ts +24 -0
  67. package/dist/storage/session-store/directory-scan.d.ts +5 -1
  68. package/dist/storage/session-store/fork-session.d.ts +13 -1
  69. package/dist/storage/session-store/load-cache.d.ts +11 -0
  70. package/dist/storage/session-store/prune-helpers.d.ts +5 -0
  71. package/dist/storage/session-store.d.ts +18 -0
  72. package/dist/tasking/index.js +5 -0
  73. package/dist/tools/index.js +1151 -831
  74. package/dist/types/config/mcp-features.d.ts +31 -1
  75. package/dist/types/config/root.d.ts +23 -1
  76. package/dist/types/config/skills-fleet-brain.d.ts +34 -0
  77. package/dist/types/config/tools.d.ts +22 -0
  78. package/dist/types/config/ui.d.ts +14 -0
  79. package/dist/types/config.d.ts +1 -0
  80. package/dist/types/default-config.d.ts +1 -0
  81. package/dist/types/index.d.ts +2 -2
  82. package/dist/types/index.js +23 -0
  83. package/dist/types/multi-agent.d.ts +7 -0
  84. package/dist/types/session.d.ts +9 -1
  85. package/dist/types/task-graph.d.ts +2 -0
  86. package/dist/types/tool-executor.d.ts +2 -0
  87. package/dist/utils/index.d.ts +1 -0
  88. package/dist/utils/index.js +217 -76
  89. package/dist/utils/project-state-guard.d.ts +21 -0
  90. package/dist/utils/session-scoped-path.d.ts +17 -0
  91. package/dist/utils/todos-format.d.ts +20 -0
  92. package/instructions/leader-after-task.md +3 -4
  93. package/instructions/system-lite.md +17 -14
  94. package/instructions/system-pro.md +29 -30
  95. package/instructions/system.md +29 -28
  96. package/package.json +3 -3
  97. package/skills/wrongstack-kanban/SKILL.md +95 -93
@@ -2177,12 +2177,11 @@ function redactHqValueInternal(value, options, preserveRawContentKeys) {
2177
2177
  });
2178
2178
  return { value: result.value, redacted: result.redacted };
2179
2179
  }
2180
+ function redactHqEventPayload(type, payload, options = {}) {
2181
+ return redactHqValueInternal(payload, options, HQ_PROJECT_STATE_EVENT_TYPES.has(type));
2182
+ }
2180
2183
  function redactHqEvent(event, options = {}) {
2181
- const payload = redactHqValueInternal(
2182
- event.payload,
2183
- options,
2184
- HQ_PROJECT_STATE_EVENT_TYPES.has(event.type)
2185
- );
2184
+ const payload = redactHqEventPayload(event.type, event.payload, options);
2186
2185
  const nextEvent = {
2187
2186
  ...event,
2188
2187
  payload: payload.value
@@ -2334,7 +2333,9 @@ function queuedFrameCoalesceKey(frame) {
2334
2333
  if (frame.type !== "client.event" || !frame.event.type.endsWith(".snapshot")) {
2335
2334
  return void 0;
2336
2335
  }
2337
- return [frame.event.type, frame.event.sessionId ?? "", frame.event.runId ?? ""].join("|");
2336
+ const payload = frame.event.payload;
2337
+ const chunk = typeof payload?.chunkIndex === "number" && Number.isFinite(payload.chunkIndex) ? String(payload.chunkIndex) : "";
2338
+ return [frame.event.type, frame.event.sessionId ?? "", frame.event.runId ?? "", chunk].join("|");
2338
2339
  }
2339
2340
  function defaultSocketFactory(url) {
2340
2341
  const WebSocketCtor = globalThis.WebSocket;
@@ -2667,6 +2668,10 @@ var HqPublisher = class {
2667
2668
  if (bytes > this.maxQueuedBytes) {
2668
2669
  this.droppedFrames += 1;
2669
2670
  this.droppedBytes += bytes;
2671
+ process.emitWarning(
2672
+ `HQ telemetry frame of ${bytes} bytes exceeds the ${this.maxQueuedBytes}-byte offline queue cap and was dropped.`,
2673
+ { code: "WRONGSTACK_HQ_FRAME_TOO_LARGE" }
2674
+ );
2670
2675
  return;
2671
2676
  }
2672
2677
  if (coalesceKey !== void 0) {
@@ -4338,6 +4343,18 @@ function metadataReferencedByText(metadata, haystack) {
4338
4343
  }
4339
4344
 
4340
4345
  // src/utils/todos-format.ts
4346
+ function formatTodoForModel(todo) {
4347
+ const binding = todo.kanbanBoardId && todo.kanbanTaskId ? ` <kanban ${todo.kanbanBoardId}/${todo.kanbanTaskId}>` : "";
4348
+ const blocked = todo.blockedBy?.length ? ` [blocked by: ${todo.blockedBy.join("; ")}]` : "";
4349
+ return `- [${todo.status}]${blocked} ${todo.content} (${todo.id})${binding}`;
4350
+ }
4351
+ function formatTodosForModel(todos, emptyLine = "- No active todos remain.") {
4352
+ return todos.length ? todos.map(formatTodoForModel).join("\n") : emptyLine;
4353
+ }
4354
+ function hasKanbanBoundTodos(todos) {
4355
+ if (!Array.isArray(todos)) return false;
4356
+ return todos.some((todo) => Boolean(todo.kanbanBoardId && todo.kanbanTaskId));
4357
+ }
4341
4358
  function hasOpenTodos(todos) {
4342
4359
  if (!Array.isArray(todos) || todos.length === 0) return false;
4343
4360
  return todos.some((t2) => t2.status === "pending" || t2.status === "in_progress");
@@ -4620,6 +4637,12 @@ function getCalibrationState(calibrationKey = CALIBRATION_GLOBAL_KEY) {
4620
4637
  import * as path10 from "node:path";
4621
4638
 
4622
4639
  // src/core/conversation-state.ts
4640
+ function hasToolResultBlock(message) {
4641
+ return message !== void 0 && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_result");
4642
+ }
4643
+ function hasToolUseBlock(message) {
4644
+ return message?.role === "assistant" && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_use");
4645
+ }
4623
4646
  var ConversationState = class {
4624
4647
  ctx;
4625
4648
  listeners = /* @__PURE__ */ new Set();
@@ -4687,15 +4710,32 @@ var ConversationState = class {
4687
4710
  */
4688
4711
  overflowCount(arr) {
4689
4712
  let drop = Context.MAX_MESSAGES > 0 ? Math.max(0, arr.length - Context.MAX_MESSAGES) : 0;
4690
- if (Context.MAX_MESSAGE_TOKENS <= 0) return drop;
4713
+ if (Context.MAX_MESSAGE_TOKENS <= 0) return this.protocolSafeDropCount(arr, drop);
4691
4714
  let total = 0;
4692
4715
  for (let i = drop; i < arr.length; i++) total += arr[i]?._estTokens ?? 0;
4693
- if (total <= Context.MAX_MESSAGE_TOKENS) return drop;
4716
+ if (total <= Context.MAX_MESSAGE_TOKENS) return this.protocolSafeDropCount(arr, drop);
4694
4717
  while (drop < arr.length - 1 && total > Context.MAX_MESSAGE_TOKENS) {
4695
4718
  total -= arr[drop]?._estTokens ?? 0;
4696
4719
  drop++;
4697
4720
  }
4698
- return drop;
4721
+ return this.protocolSafeDropCount(arr, drop);
4722
+ }
4723
+ /**
4724
+ * Front eviction must not retain a `tool_result` after evicting the
4725
+ * immediately preceding assistant `tool_use`. Long tool-heavy sessions sit
4726
+ * at the retention cap, so an unsafe boundary would create a fresh orphan on
4727
+ * nearly every append and make the request-time repair discard protocol
4728
+ * history continuously.
4729
+ *
4730
+ * Move the boundary backward to retain the complete exchange for one more
4731
+ * eviction cycle. Moving it forward would also drop non-protocol text/images
4732
+ * that may share either message. The temporary one-message cap overshoot is
4733
+ * the minimum lossless representation; once enough newer messages exist, the
4734
+ * next eviction boundary naturally moves past both halves together.
4735
+ */
4736
+ protocolSafeDropCount(arr, drop) {
4737
+ if (drop <= 0 || drop >= arr.length) return drop;
4738
+ return hasToolResultBlock(arr[drop]) && hasToolUseBlock(arr[drop - 1]) ? drop - 1 : drop;
4699
4739
  }
4700
4740
  /**
4701
4741
  * Append a content block to the trailing user message's content array.
@@ -5103,6 +5143,28 @@ var Context = class _Context {
5103
5143
  return _Context.CONVERSATION_JOURNAL_MAX_BYTES + 1;
5104
5144
  }
5105
5145
  }
5146
+ _journalDropCount = 0;
5147
+ _journalDropWarnAt = 0;
5148
+ /** Throttled notice that a conversation event never reached the journal. */
5149
+ warnConversationJournalDrop(eventType) {
5150
+ this._journalDropCount++;
5151
+ const now = Date.now();
5152
+ if (now - this._journalDropWarnAt < 5e3) return;
5153
+ this._journalDropWarnAt = now;
5154
+ const dropped = this._journalDropCount;
5155
+ this._journalDropCount = 0;
5156
+ console.warn(
5157
+ JSON.stringify({
5158
+ level: "error",
5159
+ event: "session.conversation_journal_drop",
5160
+ sessionId: this.session?.id,
5161
+ eventType,
5162
+ droppedEvents: dropped,
5163
+ message: "Session writer is not draining; replay of this session will be incomplete.",
5164
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5165
+ })
5166
+ );
5167
+ }
5106
5168
  enqueueConversationJournal(event, writer) {
5107
5169
  const bytes = this.conversationJournalBytes(event);
5108
5170
  const shouldSnapshot = event.type === "messages_replaced" || this._conversationJournalQueue.length >= _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes + bytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES;
@@ -5120,18 +5182,21 @@ var Context = class _Context {
5120
5182
  this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - queued.bytes);
5121
5183
  this._conversationJournalQueue.splice(index, 1);
5122
5184
  }
5123
- if (snapshotBytes <= _Context.CONVERSATION_JOURNAL_MAX_BYTES) {
5124
- this._conversationJournalQueue.push({ event: snapshot, bytes: snapshotBytes, writer });
5125
- this._conversationJournalBytes += snapshotBytes;
5126
- }
5185
+ this._conversationJournalQueue.push({ event: snapshot, bytes: snapshotBytes, writer });
5186
+ this._conversationJournalBytes += snapshotBytes;
5127
5187
  } else {
5128
5188
  this._conversationJournalQueue.push({ event, bytes, writer });
5129
5189
  this._conversationJournalBytes += bytes;
5130
5190
  }
5131
5191
  while (this._conversationJournalQueue.length > _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES) {
5132
- const dropped = this._conversationJournalQueue.shift();
5192
+ const index = this._conversationJournalQueue.findIndex(
5193
+ (queued) => queued.event.type !== "messages_replaced"
5194
+ );
5195
+ if (index === -1) break;
5196
+ const [dropped] = this._conversationJournalQueue.splice(index, 1);
5133
5197
  if (!dropped) break;
5134
5198
  this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - dropped.bytes);
5199
+ this.warnConversationJournalDrop(dropped.event.type);
5135
5200
  }
5136
5201
  this.startConversationJournalDrain();
5137
5202
  }
@@ -5369,6 +5434,12 @@ var Context = class _Context {
5369
5434
  setCurrentKanbanTask(taskId, boardId) {
5370
5435
  this.currentKanbanTaskId = taskId;
5371
5436
  this.currentKanbanBoardId = boardId;
5437
+ const existing = this.meta["kanban"] && typeof this.meta["kanban"] === "object" ? this.meta["kanban"] : {};
5438
+ this.state.setMeta("kanban", {
5439
+ ...existing,
5440
+ ...taskId ? { taskId } : { taskId: void 0 },
5441
+ ...boardId ? { boardId } : { boardId: void 0 }
5442
+ });
5372
5443
  }
5373
5444
  /**
5374
5445
  * Record a comprehensive file event for the audit trail.
@@ -7087,6 +7158,9 @@ ${text}` : text;
7087
7158
  const responseProvider = providerBoundToRequest(req) ?? requestProvider;
7088
7159
  const responseResult = await handlers.response.processResponse(res, req, responseProvider);
7089
7160
  await refreshProviderContextLimit(responseProvider, req.model, { probe: false });
7161
+ if (responseResult.finalText) {
7162
+ a.ctx.meta["lastAgentOutput"] = responseResult.finalText;
7163
+ }
7090
7164
  if (responseResult.aborted) {
7091
7165
  return {
7092
7166
  status: "aborted",
@@ -7211,7 +7285,7 @@ ${text}` : text;
7211
7285
  if (a.ctx.agentId === "leader" && a.tools.get("todo") !== void 0 && hasOpenTodos(a.ctx.todos) && todoReconcileSteers < 2) {
7212
7286
  todoReconcileSteers++;
7213
7287
  queueLoopSteer(
7214
- "[todo-reconciliation] The live todo/Kanban list still has open work, but you tried to end the turn without reconciling it. Call the `todo` tool now with the complete current list. Mark work you actually finished as completed, put the one item you are actively working on in_progress, and leave the rest pending. If the current item is genuinely unfinished, continue doing the work before answering; do not merely repeat the previous final response or emit <nextsteps>."
7288
+ "[todo-reconciliation] The live todo/Kanban list still has open work, but you tried to end the turn without reconciling it. Call the `todo` tool now with the complete current list. Mark work you actually finished as completed, put the one item you are actively working on in_progress, and leave the rest pending. If the current item is genuinely unfinished, continue doing the work before answering; do not merely repeat the previous final response or emit <nextsteps>.\nCanonical live list:\n" + formatTodosForModel(a.ctx.todos) + (hasKanbanBoundTodos(a.ctx.todos) ? "\nEach <kanban board/task> binding must be resent verbatim as `kanbanBoardId`/`kanbanTaskId`; a row without it is not applied to its card." : "")
7215
7289
  );
7216
7290
  await a.extensions.runAfterIteration(a.ctx, i);
7217
7291
  continue;
@@ -7432,12 +7506,15 @@ function buildLiveNextStepsGateBlock(ctx) {
7432
7506
  const todoSnapshot = openTodos.slice(0, MAX_TODO_SNAPSHOT_ITEMS).map((todo) => {
7433
7507
  const normalized = todo.content.replace(/\s+/g, " ").trim();
7434
7508
  const content = normalized.length > MAX_TODO_SNAPSHOT_CONTENT ? `${normalized.slice(0, MAX_TODO_SNAPSHOT_CONTENT - 1)}\u2026` : normalized;
7435
- return `- [${todo.status}] ${content}`;
7509
+ return formatTodoForModel({ ...todo, content });
7436
7510
  });
7437
7511
  const omitted = openTodos.length - todoSnapshot.length;
7438
7512
  if (omitted > 0) todoSnapshot.push(`- \u2026and ${omitted} more open todo(s)`);
7439
7513
  const todoReconciliation = ctx.tools?.some((tool) => tool.name === "todo") ? [
7440
- "Before ending the turn, you MUST call the `todo` tool with the complete current list to reconcile actual progress: finished items completed, exactly one actively worked item in_progress, and untouched items pending. A prose claim that work is done does not update the Todo/Kanban state."
7514
+ "Before ending the turn, you MUST call the `todo` tool with the complete current list to reconcile actual progress: finished items completed, exactly one actively worked item in_progress, and untouched items pending. A prose claim that work is done does not update the Todo/Kanban state.",
7515
+ ...hasKanbanBoundTodos(openTodos) ? [
7516
+ "Rows below carry a <kanban board/task> binding. Pass those exact ids back as `kanbanBoardId`/`kanbanTaskId` on every row you resend; a row that arrives without its binding is not applied to its card."
7517
+ ] : []
7441
7518
  ] : [];
7442
7519
  return {
7443
7520
  type: "text",
@@ -8153,6 +8230,7 @@ var Agent = class {
8153
8230
  /** Resolved loop-detector settings (see `tools.loopDetection`). */
8154
8231
  loopDetection;
8155
8232
  autonomousContinue;
8233
+ refreshSystemPrompt;
8156
8234
  tracer;
8157
8235
  extensions;
8158
8236
  _toolHandler;
@@ -8182,6 +8260,7 @@ var Agent = class {
8182
8260
  this.autoExtendLimit = init.autoExtendLimit ?? true;
8183
8261
  this.loopDetection = resolveLoopDetection(init.loopDetection);
8184
8262
  this.autonomousContinue = init.autonomousContinue ?? false;
8263
+ this.refreshSystemPrompt = init.refreshSystemPrompt ?? false;
8185
8264
  this.tracer = init.tracer;
8186
8265
  this.extensions = init.extensions ?? new ExtensionRegistry();
8187
8266
  this._logger = this.container.resolve(TOKENS.Logger).child({ sessionId: this.ctx.session.id });
@@ -8259,6 +8338,7 @@ var Agent = class {
8259
8338
  context: { phase: "concurrency-guard" }
8260
8339
  });
8261
8340
  }
8341
+ let newInputHash;
8262
8342
  const inputText = typeof userInput === "string" ? userInput : userInput?.prompt ?? "";
8263
8343
  if (inputText.length > 0) {
8264
8344
  const hash2 = createHash7("sha256").update(inputText).digest("hex");
@@ -8268,7 +8348,7 @@ var Agent = class {
8268
8348
  iterations: 0
8269
8349
  };
8270
8350
  }
8271
- this._lastInputHash = hash2;
8351
+ newInputHash = hash2;
8272
8352
  }
8273
8353
  this._runInProgress = true;
8274
8354
  const controller = new RunController({ parentSignal: opts.signal });
@@ -8285,16 +8365,33 @@ var Agent = class {
8285
8365
  });
8286
8366
  this.ctx.tools = this.tools.listForProvider();
8287
8367
  this.ctx.catalogTools = this.tools.list();
8288
- const span = this.tracer?.startSpan("agent.run", {
8289
- "agent.model": opts.model ?? this.ctx.model,
8290
- "agent.executionStrategy": opts.executionStrategy ?? this.executionStrategy
8291
- });
8292
- const { blocks, text } = normalizeInput(userInput);
8293
- const inputPayload = { content: blocks, text, ctx: this.ctx };
8294
- await this.extensions.runBeforeRun(this.ctx, inputPayload);
8295
8368
  const runStartedAt = Date.now();
8296
8369
  const runStartedIso = new Date(runStartedAt).toISOString();
8370
+ let span;
8297
8371
  try {
8372
+ if (this.refreshSystemPrompt) {
8373
+ const builder = this.container.safeResolve(TOKENS.SystemPromptBuilder);
8374
+ if (builder) {
8375
+ const onlineAgents = Array.isArray(this.ctx.meta["promptOnlineAgents"]) ? this.ctx.meta["promptOnlineAgents"] : void 0;
8376
+ this.ctx.systemPrompt = await builder.build({
8377
+ cwd: this.ctx.cwd,
8378
+ projectRoot: this.ctx.projectRoot,
8379
+ tools: this.ctx.tools,
8380
+ catalogTools: this.ctx.catalogTools,
8381
+ provider: this.ctx.provider.id,
8382
+ model: opts.model ?? this.ctx.model,
8383
+ onlineAgents
8384
+ });
8385
+ }
8386
+ }
8387
+ span = this.tracer?.startSpan("agent.run", {
8388
+ "agent.model": opts.model ?? this.ctx.model,
8389
+ "agent.executionStrategy": opts.executionStrategy ?? this.executionStrategy
8390
+ });
8391
+ const { blocks, text } = normalizeInput(userInput);
8392
+ const inputPayload = { content: blocks, text, ctx: this.ctx };
8393
+ await this.extensions.runBeforeRun(this.ctx, inputPayload);
8394
+ this._lastInputHash = newInputHash;
8298
8395
  this.events.emit("agent.run.started", {
8299
8396
  sessionId,
8300
8397
  ctx: this.ctx,
@@ -8762,12 +8859,97 @@ var FallbackProfileManager = class {
8762
8859
  return this.resolveRefs([ref], exclude);
8763
8860
  }
8764
8861
  /**
8765
- * Resolve every usable configured target as an uncapped last-resort chain.
8862
+ * Resolve every usable configured target as a bounded last-resort chain.
8766
8863
  * Normal smart defaults stay bounded; callers append this only after the
8767
- * preferred chain and only when automatic fallback is enabled.
8864
+ * preferred chain and only when automatic fallback is enabled. The cap
8865
+ * ({@link MAX_LAST_RESORT_CANDIDATES}) prevents a config with many providers
8866
+ * from producing a degenerate chain of doomed requests during a systemic
8867
+ * outage — by this point the smart default, bridge, and default profile
8868
+ * have already failed.
8768
8869
  */
8769
8870
  resolveAllConfigured(exclude) {
8770
- return this.smartDefault(exclude, Number.POSITIVE_INFINITY);
8871
+ return this.smartDefault(exclude, this.lastResortCap());
8872
+ }
8873
+ /**
8874
+ * Effective cap for the last-resort append. Reads the user-configurable
8875
+ * {@link Config.fallbackMaxLastResortCandidates} when set and valid;
8876
+ * otherwise falls back to the compiled-in default
8877
+ * {@link MAX_LAST_RESORT_CANDIDATES}.
8878
+ */
8879
+ lastResortCap() {
8880
+ const configured = this.config.fallbackMaxLastResortCandidates;
8881
+ if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
8882
+ return Math.floor(configured);
8883
+ }
8884
+ return MAX_LAST_RESORT_CANDIDATES;
8885
+ }
8886
+ /**
8887
+ * Build the complete fallback candidate chain shared by the agent-loop
8888
+ * extension and the one-shot orchestrator. Centralizes the bridge → primary →
8889
+ * selected → default-profile → all-configured ladder and the fromExplicitSource
8890
+ * gate so both consumers produce identical ordering and depth semantics.
8891
+ *
8892
+ * Layering (each step deduped against all prior):
8893
+ * 1. Bridge (emergency continuity route).
8894
+ * 2. Configured primary, when the live context drifted from it.
8895
+ * 3. The selected chain (explicit refs → named profile → smart default),
8896
+ * via {@link resolveEffective}.
8897
+ * 4. The "default" profile — extra depth, ONLY when the chain was auto-derived.
8898
+ * 5. Every other configured provider — last resort, ONLY when the chain was
8899
+ * auto-derived AND `effectiveFallbackAuto` is true.
8900
+ *
8901
+ * Returns the empty chain when `closedWorld` is true and no explicit
8902
+ * refs/profile resolved — a model allowlist never leaks to unlisted models.
8903
+ *
8904
+ * @internal caller-aware options (primary, closedWorld) are accepted because
8905
+ * the agent loop has context the manager does not own; the resolution
8906
+ * pipeline itself is identical for both callers.
8907
+ */
8908
+ resolveCandidates(current, opts = {}) {
8909
+ const configuredPrimary = opts.primary ?? {
8910
+ providerId: this.config.provider,
8911
+ model: this.config.model
8912
+ };
8913
+ const configFallbackAuto = this.config.fallbackAuto;
8914
+ const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
8915
+ const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
8916
+ const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
8917
+ const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
8918
+ const fromExplicitSource = explicitUsable || profileUsable;
8919
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
8920
+ fallbackModels: explicitRefs,
8921
+ fallbackProfile: opts.fallbackProfile,
8922
+ fallbackAuto: effectiveFallbackAuto,
8923
+ exclude: current
8924
+ });
8925
+ const candidates = [];
8926
+ if (opts.closedWorld) {
8927
+ candidates.push(...selectedChain);
8928
+ } else {
8929
+ candidates.push(...this.resolveBridge(current));
8930
+ if (!(configuredPrimary.providerId === current.providerId && configuredPrimary.model === current.model)) {
8931
+ candidates.push({
8932
+ providerId: configuredPrimary.providerId,
8933
+ model: configuredPrimary.model,
8934
+ providerSwitched: configuredPrimary.providerId !== current.providerId
8935
+ });
8936
+ }
8937
+ candidates.push(...selectedChain);
8938
+ if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
8939
+ candidates.push(...this.resolve("default", { exclude: current }));
8940
+ }
8941
+ if (!fromExplicitSource && effectiveFallbackAuto) {
8942
+ const cap = this.lastResortCap();
8943
+ if (cap > 0) {
8944
+ const usedKeys = new Set(
8945
+ candidates.map((c) => `${c.providerId}/${c.model}`)
8946
+ );
8947
+ const lastResort = this.smartDefault(current, Number.POSITIVE_INFINITY).filter((c) => !usedKeys.has(`${c.providerId}/${c.model}`)).slice(0, cap);
8948
+ candidates.push(...lastResort);
8949
+ }
8950
+ }
8951
+ }
8952
+ return dedupeChain(candidates, current);
8771
8953
  }
8772
8954
  // ── Provider availability (read-only) ──────────────────────────────────
8773
8955
  checkProvider(providerId) {
@@ -8891,7 +9073,20 @@ var FallbackProfileManager = class {
8891
9073
  );
8892
9074
  }
8893
9075
  };
9076
+ var MAX_LAST_RESORT_CANDIDATES = 12;
8894
9077
  var FREEZER_EMPTY = Object.freeze([]);
9078
+ function dedupeChain(entries, current) {
9079
+ const seen = /* @__PURE__ */ new Set();
9080
+ const currentKey = `${current.providerId}/${current.model}`;
9081
+ return Object.freeze(
9082
+ entries.filter((entry) => {
9083
+ const key = `${entry.providerId}/${entry.model}`;
9084
+ if (key === currentKey || seen.has(key)) return false;
9085
+ seen.add(key);
9086
+ return true;
9087
+ })
9088
+ );
9089
+ }
8895
9090
 
8896
9091
  // src/core/fallback-model.ts
8897
9092
  function fallbackProfileChain(config, profileName) {
@@ -8940,56 +9135,12 @@ function sameTarget(a, b) {
8940
9135
  }
8941
9136
  function fallbackCandidates(config, current, opts = {}) {
8942
9137
  const mgr = opts.sharedManager ?? new FallbackProfileManager(config);
8943
- const configuredPrimary = opts.primary ?? primaryTarget(config);
8944
- const configFallbackAuto = config.fallbackAuto;
8945
- const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
8946
- const explicitRefs = opts.fallbackModels ?? config.fallbackModels;
8947
- const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && mgr.resolveRefs(explicitRefs, current).length > 0;
8948
- const profileUsable = opts.fallbackProfile !== void 0 && mgr.hasProfile(opts.fallbackProfile) && mgr.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
8949
- const fromExplicitSource = explicitUsable || profileUsable;
8950
- const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? mgr.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? mgr.resolve(opts.fallbackProfile, { exclude: current }) : Object.freeze([]) : mgr.resolveEffective({
8951
- fallbackModels: explicitRefs,
9138
+ return mgr.resolveCandidates(current, {
9139
+ fallbackModels: opts.fallbackModels,
8952
9140
  fallbackProfile: opts.fallbackProfile,
8953
- fallbackAuto: effectiveFallbackAuto,
8954
- exclude: current
9141
+ primary: opts.primary ?? primaryTarget(config),
9142
+ closedWorld: opts.closedWorld
8955
9143
  });
8956
- const candidates = [];
8957
- if (opts.closedWorld) {
8958
- candidates.push(...selectedChain);
8959
- const seen2 = /* @__PURE__ */ new Set();
8960
- return Object.freeze(
8961
- candidates.filter((entry) => {
8962
- const key = `${entry.providerId}/${entry.model}`;
8963
- if (key === `${current.providerId}/${current.model}` || seen2.has(key)) return false;
8964
- seen2.add(key);
8965
- return true;
8966
- })
8967
- );
8968
- }
8969
- candidates.push(...mgr.resolveBridge(current));
8970
- if (!sameTarget(configuredPrimary, current)) {
8971
- candidates.push({
8972
- providerId: configuredPrimary.providerId,
8973
- model: configuredPrimary.model,
8974
- providerSwitched: configuredPrimary.providerId !== current.providerId
8975
- });
8976
- }
8977
- candidates.push(...selectedChain);
8978
- if (!fromExplicitSource && opts.fallbackProfile !== "default") {
8979
- candidates.push(...mgr.resolve("default", { exclude: current }));
8980
- }
8981
- if (!fromExplicitSource && effectiveFallbackAuto) {
8982
- candidates.push(...mgr.resolveAllConfigured(current));
8983
- }
8984
- const seen = /* @__PURE__ */ new Set();
8985
- return Object.freeze(
8986
- candidates.filter((entry) => {
8987
- const key = `${entry.providerId}/${entry.model}`;
8988
- if (key === `${current.providerId}/${current.model}` || seen.has(key)) return false;
8989
- seen.add(key);
8990
- return true;
8991
- })
8992
- );
8993
9144
  }
8994
9145
  var primaryTarget = (cfg) => ({ providerId: cfg.provider, model: cfg.model });
8995
9146
  function maxContextOf(provider) {
@@ -10491,7 +10642,7 @@ async function buildEnvironment(ctx, env) {
10491
10642
  ]);
10492
10643
  const tier = env.tier;
10493
10644
  const lines = ["## Environment"];
10494
- if (tier === "minimal") {
10645
+ if (tier === "minimal" || tier === "aggressive") {
10495
10646
  lines.push(`- Git: ${git} | Date: ${today}`);
10496
10647
  } else {
10497
10648
  lines.push(`- Operating system: ${platform2}`);
@@ -10499,38 +10650,26 @@ async function buildEnvironment(ctx, env) {
10499
10650
  lines.push(`- Shell: ${shell}`);
10500
10651
  lines.push(`- Node.js: ${node}`);
10501
10652
  }
10502
- if (tier === "off" || tier === "medium" || tier === "aggressive") {
10653
+ if (tier === "off" || tier === "medium") {
10503
10654
  lines.push(`- Detected languages: ${langs}`);
10504
10655
  }
10505
10656
  lines.push(`- Git status: ${git}`);
10506
10657
  lines.push(`- Today's date: ${today}`);
10507
- if (tier === "aggressive") {
10508
- if (ctx.provider || ctx.model) {
10509
- lines.push(
10510
- `- Running on: ${ctx.provider ?? "<unknown provider>"}/${ctx.model ?? "<unknown model>"}`
10511
- );
10512
- }
10513
- if (modelCapabilities) {
10514
- lines.push(
10515
- `- Context window: ${modelCapabilities.maxContextTokens.toLocaleString()} tokens max`
10516
- );
10517
- }
10518
- }
10519
- if (tier !== "aggressive" && modelCapabilities) {
10658
+ if (modelCapabilities) {
10520
10659
  lines.push(
10521
10660
  `- Context window: ${modelCapabilities.maxContextTokens.toLocaleString()} tokens max`
10522
10661
  );
10523
10662
  }
10524
- if (tier !== "aggressive" && (ctx.provider || ctx.model)) {
10663
+ if (ctx.provider || ctx.model) {
10525
10664
  lines.push(
10526
10665
  `- Running on: ${ctx.provider ?? "<unknown provider>"}/${ctx.model ?? "<unknown model>"}`
10527
10666
  );
10528
10667
  }
10529
- if (tier !== "aggressive" && env.modeId && env.modeId !== "default") {
10668
+ if (env.modeId && env.modeId !== "default") {
10530
10669
  lines.push(`- Mode: ${env.modeId}`);
10531
10670
  }
10532
10671
  }
10533
- if (ctx.tools.some((tool) => tool.name === "bash") && effShell !== "posix" && tier !== "minimal") {
10672
+ if (ctx.tools.some((tool) => tool.name === "bash") && effShell !== "posix" && tier !== "minimal" && tier !== "aggressive") {
10534
10673
  const guide = shellGuidanceBlock(effShell, tier === "light" ? "short" : "full");
10535
10674
  if (guide) lines.push("", guide);
10536
10675
  }
@@ -10553,6 +10692,70 @@ async function buildEnvironment(ctx, env) {
10553
10692
  return text;
10554
10693
  }
10555
10694
 
10695
+ // src/core/system-prompt-glossary.ts
10696
+ function findDomainGlossaryEntries(memory) {
10697
+ return safeList(memory, { limit: 200 }).then((rows) => rows ?? []).then(
10698
+ (rows) => rows.filter(
10699
+ (r) => Array.isArray(r?.tags) && r.tags.includes("domain-term")
10700
+ )
10701
+ );
10702
+ }
10703
+ async function safeList(memory, opts) {
10704
+ try {
10705
+ const rows = await memory.list("project-memory", opts.limit);
10706
+ if (!Array.isArray(rows) || rows.length === 0) return null;
10707
+ return rows;
10708
+ } catch {
10709
+ return null;
10710
+ }
10711
+ }
10712
+ var GLOSSARY_HEADING = "# Project Jargon Dictionary (auto-mined from SAGE; update via SageDomainTermExtractor)";
10713
+ async function renderDomainGlossary(ctx, memory, options = {}) {
10714
+ if (!memory) return null;
10715
+ const maxEntries = options.maxEntries ?? 12;
10716
+ const maxEntryChars = options.maxEntryChars ?? 80;
10717
+ const entries = await findDomainGlossaryEntries(memory);
10718
+ if (entries.length === 0) return "";
10719
+ const sorted = [...entries].sort((a, b) => {
10720
+ const dc = (b.confidence ?? 0) - (a.confidence ?? 0);
10721
+ if (dc !== 0) return dc;
10722
+ return b.ts.localeCompare(a.ts);
10723
+ });
10724
+ const lines = [GLOSSARY_HEADING, ""];
10725
+ let emitted = 0;
10726
+ for (const entry of sorted) {
10727
+ if (emitted >= maxEntries) break;
10728
+ const { term, definition } = parseTermEntry(entry.text);
10729
+ if (!term) continue;
10730
+ const def = (definition || "").slice(0, maxEntryChars).trim();
10731
+ lines.push(def ? `- **${term}** \u2014 ${def}` : `- **${term}**`);
10732
+ emitted += 1;
10733
+ }
10734
+ if (emitted === 0) return "";
10735
+ lines.push("");
10736
+ lines.push(
10737
+ `_Source: <${ctx.projectRoot}>/.wrongstack/domain-terms.md (auto-generated; do not edit by hand)._`
10738
+ );
10739
+ lines.push(
10740
+ '_Lookup tag: `domain-term` (search: `memory_search({ query: "domain-term", scope: "project-memory" })`)._'
10741
+ );
10742
+ return lines.join("\n");
10743
+ }
10744
+ function parseTermEntry(text) {
10745
+ const trimmed = text.trim();
10746
+ const separators = [" \u2014 ", " \u2013 ", " - ", " -- "];
10747
+ for (const sep of separators) {
10748
+ const idx = trimmed.indexOf(sep);
10749
+ if (idx > 0) {
10750
+ return {
10751
+ term: trimmed.slice(0, idx).trim(),
10752
+ definition: trimmed.slice(idx + sep.length).trim()
10753
+ };
10754
+ }
10755
+ }
10756
+ return { term: trimmed, definition: "" };
10757
+ }
10758
+
10556
10759
  // src/skills/limits.ts
10557
10760
  var SKILL_LIMITS = {
10558
10761
  /**
@@ -10819,7 +11022,7 @@ function renderOnlineAgents(agents, tier, cache) {
10819
11022
  return { text: cache.text, cache };
10820
11023
  }
10821
11024
  const totalCount = agents.length;
10822
- if (tier === "minimal" || tier === "light") {
11025
+ if (tier === "minimal" || tier === "light" || tier === "aggressive") {
10823
11026
  const text2 = ` (${totalCount} agent${totalCount !== 1 ? "s" : ""} online)`;
10824
11027
  return { text: text2, cache: { hash: hash2, text: text2 } };
10825
11028
  }
@@ -10844,8 +11047,8 @@ ${agentList}`;
10844
11047
  async function buildMemoryAndSkills(mem) {
10845
11048
  const parts = [];
10846
11049
  let skillBodyCache = mem.skillBodyCache;
10847
- const memoryCount = mem.tier === "minimal" || mem.tier === "light" ? 3 : 5;
10848
- const compactMemory = mem.tier === "minimal";
11050
+ const memoryCount = mem.tier === "minimal" || mem.tier === "light" || mem.tier === "aggressive" ? 3 : 5;
11051
+ const compactMemory = mem.tier === "minimal" || mem.tier === "aggressive";
10849
11052
  if (mem.memoryStore && mem.injectMemory !== false) {
10850
11053
  try {
10851
11054
  if (mem.memoryStore.scoreRelevant) {
@@ -11026,7 +11229,7 @@ var DefaultSystemPromptBuilder = class {
11026
11229
  }
11027
11230
  /**
11028
11231
  * Returns the max tool description length for the current tier.
11029
- * Per the design doc: off=80, minimal=40, light=50, medium=60, aggressive=70.
11232
+ * The aggressive tier has the shortest descriptions; off keeps the most detail.
11030
11233
  */
11031
11234
  toolDescLimit() {
11032
11235
  switch (this.tier) {
@@ -11037,7 +11240,7 @@ var DefaultSystemPromptBuilder = class {
11037
11240
  case "medium":
11038
11241
  return 50;
11039
11242
  case "aggressive":
11040
- return 60;
11243
+ return 20;
11041
11244
  default:
11042
11245
  return 70;
11043
11246
  }
@@ -11149,6 +11352,12 @@ var DefaultSystemPromptBuilder = class {
11149
11352
  }
11150
11353
  }
11151
11354
  }
11355
+ if (this.opts.domainGlossary) {
11356
+ const glossary = await renderDomainGlossary(ctx, this.opts.domainGlossary);
11357
+ if (glossary) {
11358
+ volatile.push(tagBlock({ type: "text", text: glossary }, "glossary"));
11359
+ }
11360
+ }
11152
11361
  if (!ctx.subagent) {
11153
11362
  session.push(
11154
11363
  tagBlock(
@@ -11306,8 +11515,8 @@ ${hint.trim()}`);
11306
11515
  return Array.isArray(role) ? role.filter((r) => typeof r === "string") : [];
11307
11516
  })();
11308
11517
  const roleList = enumValues.length > 0 ? enumValues.join(", ") : "(no roster configured)";
11309
- if (this.tier === "minimal") {
11310
- } else if (this.tier === "light" || this.tier === "medium" || this.tier === "aggressive") {
11518
+ if (this.tier === "minimal" || this.tier === "aggressive") {
11519
+ } else if (this.tier === "light" || this.tier === "medium") {
11311
11520
  const delegation = section("tool.delegation.compact", {
11312
11521
  roleList
11313
11522
  });
@@ -11343,7 +11552,7 @@ ${hint.trim()}`);
11343
11552
  }
11344
11553
  }
11345
11554
  const hasGitTool = tools.some((t2) => t2.name === "git");
11346
- if (hasGitTool && this.tier !== "minimal" && this.tier !== "light") {
11555
+ if (hasGitTool && this.tier !== "minimal" && this.tier !== "light" && this.tier !== "aggressive") {
11347
11556
  const commitHygiene = section("tool.commit.hygiene");
11348
11557
  if (commitHygiene) lines.push(commitHygiene);
11349
11558
  }
@@ -11360,7 +11569,7 @@ ${hint.trim()}`);
11360
11569
  }
11361
11570
  const hasContextManager = tools.some((t2) => t2.name === "context_manager");
11362
11571
  if (hasContextManager) {
11363
- if (this.tier === "minimal" || this.tier === "light") {
11572
+ if (this.tier === "minimal" || this.tier === "light" || this.tier === "aggressive") {
11364
11573
  } else if (this.tier === "medium") {
11365
11574
  const contextManagement = section("tool.context.management.compact");
11366
11575
  if (contextManagement) lines.push(contextManagement);
@@ -11413,11 +11622,11 @@ ${hint.trim()}`);
11413
11622
  return result.text;
11414
11623
  }
11415
11624
  async buildMode() {
11416
- if (this.opts.modePrompt) return this.opts.modePrompt;
11417
- if (!this.opts.modeStore) return "";
11418
- const mode = await this.opts.modeStore.getActiveMode();
11419
- if (!mode?.prompt) return "";
11420
- return mode.prompt;
11625
+ if (this.opts.modeStore) {
11626
+ const mode = await this.opts.modeStore.getActiveMode();
11627
+ if (mode?.prompt) return mode.prompt;
11628
+ }
11629
+ return this.opts.modePrompt ?? "";
11421
11630
  }
11422
11631
  };
11423
11632
  export {