@rubytech/taskmaster 1.0.11 → 1.0.13

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 (32) hide show
  1. package/dist/agents/pi-embedded-helpers/errors.js +6 -7
  2. package/dist/agents/pi-embedded-runner/compact.js +0 -1
  3. package/dist/agents/pi-embedded-runner/run/attempt.js +0 -1
  4. package/dist/agents/pi-embedded-runner/run/payloads.js +4 -4
  5. package/dist/agents/pi-embedded-runner/run.js +4 -4
  6. package/dist/agents/pi-embedded-runner/system-prompt.js +0 -1
  7. package/dist/agents/system-prompt.js +5 -17
  8. package/dist/agents/taskmaster-tools.js +4 -0
  9. package/dist/agents/tool-policy.js +1 -0
  10. package/dist/agents/tools/memory-reindex-tool.js +67 -0
  11. package/dist/auto-reply/reply/agent-runner-execution.js +17 -40
  12. package/dist/auto-reply/reply/agent-runner.js +14 -24
  13. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  14. package/dist/auto-reply/reply/session.js +1 -1
  15. package/dist/build-info.json +3 -3
  16. package/dist/control-ui/assets/{index-DKEqjln4.js → index-D8ayJUWC.js} +176 -152
  17. package/dist/control-ui/assets/index-D8ayJUWC.js.map +1 -0
  18. package/dist/control-ui/assets/{index-QrR3OWrg.css → index-dMMqL7A5.css} +1 -1
  19. package/dist/control-ui/index.html +2 -2
  20. package/dist/gateway/protocol/schema/sessions-transcript.js +1 -0
  21. package/dist/gateway/server-methods/memory.js +62 -0
  22. package/dist/gateway/server-methods/sessions-transcript.js +10 -9
  23. package/dist/gateway/server-methods.js +5 -1
  24. package/dist/memory/manager.js +17 -1
  25. package/dist/memory/memory-schema.js +23 -1
  26. package/dist/web/auto-reply/monitor.js +1 -1
  27. package/package.json +1 -1
  28. package/taskmaster-docs/USER-GUIDE.md +8 -4
  29. package/templates/customer/agents/admin/AGENTS.md +6 -5
  30. package/templates/taskmaster/agents/admin/AGENTS.md +7 -5
  31. package/templates/tradesupport/agents/admin/AGENTS.md +6 -5
  32. package/dist/control-ui/assets/index-DKEqjln4.js.map +0 -1
@@ -248,10 +248,10 @@ export function formatAssistantErrorText(msg, opts) {
248
248
  return ("Context overflow: prompt too large for the model. " +
249
249
  "Try again with less input or a larger-context model.");
250
250
  }
251
- // Catch role ordering errors - including JSON-wrapped and "400" prefix variants
252
- if (/incorrect role information|roles must alternate|400.*role|"message".*role.*information/i.test(raw)) {
253
- return ("Message ordering conflict - please try again. " +
254
- "If this persists, use /new to start a fresh session.");
251
+ // Conversation history corruption role ordering, orphaned tool results, etc.
252
+ // The session is stuck; every subsequent message will fail until reset.
253
+ if (/incorrect role information|roles must alternate|400.*role|"message".*role.*information|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(raw)) {
254
+ return "This conversation got into a bad state. Use /new to start fresh.";
255
255
  }
256
256
  const invalidRequest = raw.match(/"type":"invalid_request_error".*?"message":"([^"]+)"/);
257
257
  if (invalidRequest?.[1]) {
@@ -276,9 +276,8 @@ export function sanitizeUserFacingText(text) {
276
276
  const trimmed = stripped.trim();
277
277
  if (!trimmed)
278
278
  return stripped;
279
- if (/incorrect role information|roles must alternate/i.test(trimmed)) {
280
- return ("Message ordering conflict - please try again. " +
281
- "If this persists, use /new to start a fresh session.");
279
+ if (/incorrect role information|roles must alternate|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(trimmed)) {
280
+ return "This conversation got into a bad state. Use /new to start fresh.";
282
281
  }
283
282
  if (isContextOverflowError(trimmed)) {
284
283
  return ("Context overflow: prompt too large for the model. " +
@@ -259,7 +259,6 @@ export async function compactEmbeddedPiSessionDirect(params) {
259
259
  extraSystemPrompt: params.extraSystemPrompt,
260
260
  ownerNumbers: params.ownerNumbers,
261
261
  reasoningTagHint,
262
- finalTagHint: !reasoningTagHint,
263
262
  heartbeatPrompt: isDefaultAgent
264
263
  ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
265
264
  : undefined,
@@ -284,7 +284,6 @@ export async function runEmbeddedAttempt(params) {
284
284
  extraSystemPrompt: params.extraSystemPrompt,
285
285
  ownerNumbers: params.ownerNumbers,
286
286
  reasoningTagHint,
287
- finalTagHint: !reasoningTagHint,
288
287
  heartbeatPrompt: isDefaultAgent
289
288
  ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
290
289
  : undefined,
@@ -30,10 +30,10 @@ export function buildEmbeddedRunPayloads(params) {
30
30
  : null;
31
31
  const normalizedErrorText = errorText ? normalizeTextForComparison(errorText) : null;
32
32
  const genericErrorText = "The AI service returned an error. Please try again.";
33
- if (errorText) {
34
- // Suppress raw API errors for public-facing agents
35
- const displayError = params.suppressRawErrors ? genericErrorText : errorText;
36
- replyItems.push({ text: displayError, isError: true });
33
+ // Public-facing agents: silently swallow errors — no error text of any kind.
34
+ // Admin agent: show the formatted error from formatAssistantErrorText.
35
+ if (errorText && !params.suppressRawErrors) {
36
+ replyItems.push({ text: errorText, isError: true });
37
37
  }
38
38
  const inlineToolResults = params.inlineToolResultsAllowed && params.verboseLevel !== "off" && params.toolMetas.length > 0;
39
39
  if (inlineToolResults) {
@@ -331,13 +331,13 @@ export async function runEmbeddedPiAgent(params) {
331
331
  },
332
332
  };
333
333
  }
334
- // Handle role ordering errors with a user-friendly message
335
- if (/incorrect role information|roles must alternate/i.test(errorText)) {
334
+ // Handle session corruption errors (role ordering, orphaned tool results)
335
+ // Returning kind: "role_ordering" triggers auto-recovery (session reset) upstream.
336
+ if (/incorrect role information|roles must alternate|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(errorText)) {
336
337
  return {
337
338
  payloads: [
338
339
  {
339
- text: "Message ordering conflict - please try again. " +
340
- "If this persists, use /new to start a fresh session.",
340
+ text: "This conversation got into a bad state — resetting now.",
341
341
  isError: true,
342
342
  },
343
343
  ],
@@ -8,7 +8,6 @@ export function buildEmbeddedSystemPrompt(params) {
8
8
  extraSystemPrompt: params.extraSystemPrompt,
9
9
  ownerNumbers: params.ownerNumbers,
10
10
  reasoningTagHint: params.reasoningTagHint,
11
- finalTagHint: params.finalTagHint,
12
11
  heartbeatPrompt: params.heartbeatPrompt,
13
12
  skillsPrompt: params.skillsPrompt,
14
13
  docsPath: params.docsPath,
@@ -217,15 +217,6 @@ export function buildAgentSystemPrompt(params) {
217
217
  "<final>Hey there! What would you like to do next?</final>",
218
218
  ].join(" ")
219
219
  : undefined;
220
- // For providers with native thinking (e.g. Anthropic), we only need the <final> instruction
221
- // to prevent internal reasoning/planning text from leaking to the user.
222
- const finalOnlyHint = !reasoningHint && params.finalTagHint
223
- ? [
224
- "Wrap every user-facing reply in <final>...</final> tags.",
225
- "Only text inside <final> is shown to the user; everything outside is discarded.",
226
- "Do not narrate your plan or next steps outside <final> — the user cannot see it.",
227
- ].join(" ")
228
- : undefined;
229
220
  const reasoningLevel = params.reasoningLevel ?? "off";
230
221
  const userTimezone = params.userTimezone?.trim();
231
222
  const skillsPrompt = params.skillsPrompt?.trim();
@@ -287,11 +278,11 @@ export function buildAgentSystemPrompt(params) {
287
278
  "If a task is more complex or takes longer, spawn a sub-agent. It will do the work for you and ping you when it's done. You can always check up on it.",
288
279
  "",
289
280
  "## Tool Call Style",
290
- "Default: do not narrate routine, low-risk tool calls (just call the tool).",
291
- "Narrate only when it helps: multi-step work, complex/challenging problems, sensitive actions (e.g., deletions), or when the user explicitly asks.",
292
- "Keep narration brief and value-dense; avoid repeating obvious steps.",
293
- "Use plain human language for narration unless in a technical context.",
294
- "Never narrate internal housekeeping to the user: creating profiles, looking up records, checking memory, saving notes. Do those silently. The user sees the finished result, not the process.",
281
+ "Every text block you produce is delivered as a separate message. Unnecessary text blocks = unnecessary messages sent to the user.",
282
+ "Call tools silently by default. Do not announce what you are about to do, explain what you just did, or summarise completed steps.",
283
+ "If a task requires multiple tool calls, execute them without commentary. The user sees only your final response.",
284
+ "Narrate only when the user needs to know why there is a delay, or when an action has consequences they should be aware of (e.g. a deletion or sending on their behalf).",
285
+ "Never produce process narration like 'I'll check...', 'Let me look up...', 'Perfect — found it', 'Done I've sent...'. These become unwanted messages.",
295
286
  "",
296
287
  "## Taskmaster CLI Quick Reference",
297
288
  "Taskmaster is controlled via subcommands. Do not invent commands.",
@@ -432,9 +423,6 @@ export function buildAgentSystemPrompt(params) {
432
423
  if (reasoningHint) {
433
424
  lines.push("## Reasoning Format", reasoningHint, "");
434
425
  }
435
- else if (finalOnlyHint) {
436
- lines.push("## Output Format", finalOnlyHint, "");
437
- }
438
426
  const contextFiles = params.contextFiles ?? [];
439
427
  if (contextFiles.length > 0) {
440
428
  const hasSoulFile = contextFiles.some((file) => {
@@ -8,6 +8,7 @@ import { createDocumentTool } from "./tools/document-tool.js";
8
8
  import { createGatewayTool } from "./tools/gateway-tool.js";
9
9
  import { createImageTool } from "./tools/image-tool.js";
10
10
  import { createMemoryGetTool, createMemorySaveMediaTool, createMemorySearchTool, createMemoryWriteTool, } from "./tools/memory-tool.js";
11
+ import { createMemoryReindexTool } from "./tools/memory-reindex-tool.js";
11
12
  import { createMessageTool } from "./tools/message-tool.js";
12
13
  import { createNodesTool } from "./tools/nodes-tool.js";
13
14
  import { createSessionStatusTool } from "./tools/session-status-tool.js";
@@ -142,6 +143,7 @@ export function createTaskmasterTools(options) {
142
143
  const memoryGetTool = createMemoryGetTool(memoryToolOptions);
143
144
  const memoryWriteTool = createMemoryWriteTool(memoryToolOptions);
144
145
  const memorySaveMediaTool = createMemorySaveMediaTool(memoryToolOptions);
146
+ const memoryReindexTool = createMemoryReindexTool(memoryToolOptions);
145
147
  if (memorySearchTool)
146
148
  tools.push(memorySearchTool);
147
149
  if (memoryGetTool)
@@ -150,6 +152,8 @@ export function createTaskmasterTools(options) {
150
152
  tools.push(memoryWriteTool);
151
153
  if (memorySaveMediaTool)
152
154
  tools.push(memorySaveMediaTool);
155
+ if (memoryReindexTool)
156
+ tools.push(memoryReindexTool);
153
157
  const pluginTools = resolvePluginTools({
154
158
  context: {
155
159
  config: options?.config,
@@ -9,6 +9,7 @@ export const TOOL_GROUPS = {
9
9
  "memory_get",
10
10
  "memory_write",
11
11
  "memory_save_media",
12
+ "memory_reindex",
12
13
  "document_read",
13
14
  ],
14
15
  "group:web": ["web_search", "web_fetch"],
@@ -0,0 +1,67 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { getMemorySearchManager } from "../../memory/index.js";
3
+ import { resolveSessionAgentId } from "../agent-scope.js";
4
+ import { resolveMemorySearchConfig } from "../memory-search.js";
5
+ import { jsonResult, readStringParam } from "./common.js";
6
+ const MemoryReindexSchema = Type.Object({
7
+ action: Type.String({
8
+ description: "Action to perform: 'reindex' (force full reindex) or 'status' (return index stats)",
9
+ }),
10
+ });
11
+ export function createMemoryReindexTool(options) {
12
+ const cfg = options.config;
13
+ if (!cfg)
14
+ return null;
15
+ const agentId = resolveSessionAgentId({
16
+ sessionKey: options.agentSessionKey,
17
+ config: cfg,
18
+ });
19
+ if (!resolveMemorySearchConfig(cfg, agentId))
20
+ return null;
21
+ return {
22
+ label: "Memory Reindex",
23
+ name: "memory_reindex",
24
+ description: "Trigger a memory index rebuild or check index status. " +
25
+ "Use action='reindex' to force a full reindex of all memory files and sessions. " +
26
+ "Use action='status' to return current index stats (file counts, provider, sources).",
27
+ parameters: MemoryReindexSchema,
28
+ execute: async (_toolCallId, params) => {
29
+ const action = readStringParam(params, "action", { required: true });
30
+ const { manager, error } = await getMemorySearchManager({ cfg, agentId });
31
+ if (!manager) {
32
+ return jsonResult({ ok: false, error: error ?? "memory index unavailable" });
33
+ }
34
+ if (action === "status") {
35
+ const status = manager.status();
36
+ return jsonResult({
37
+ ok: true,
38
+ action: "status",
39
+ files: status.files,
40
+ chunks: status.chunks,
41
+ sources: status.sources,
42
+ sourceCounts: status.sourceCounts,
43
+ provider: status.provider,
44
+ model: status.model,
45
+ dirty: status.dirty,
46
+ });
47
+ }
48
+ // Default: reindex
49
+ try {
50
+ await manager.sync({ reason: "agent-tool", force: true });
51
+ const status = manager.status();
52
+ return jsonResult({
53
+ ok: true,
54
+ action: "reindex",
55
+ files: status.files,
56
+ chunks: status.chunks,
57
+ sources: status.sources,
58
+ sourceCounts: status.sourceCounts,
59
+ });
60
+ }
61
+ catch (err) {
62
+ const message = err instanceof Error ? err.message : String(err);
63
+ return jsonResult({ ok: false, action: "reindex", error: message });
64
+ }
65
+ },
66
+ };
67
+ }
@@ -1,5 +1,4 @@
1
1
  import crypto from "node:crypto";
2
- import fs from "node:fs";
3
2
  import { resolveAgentModelFallbacksOverride, resolveDefaultAgentId, } from "../../agents/agent-scope.js";
4
3
  import { runCliAgent } from "../../agents/cli-runner.js";
5
4
  import { getCliSessionId } from "../../agents/cli-session.js";
@@ -7,7 +6,7 @@ import { runWithModelFallback } from "../../agents/model-fallback.js";
7
6
  import { isCliProvider } from "../../agents/model-selection.js";
8
7
  import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
9
8
  import { isCompactionFailureError, isContextOverflowError, isLikelyContextOverflowError, sanitizeUserFacingText, } from "../../agents/pi-embedded-helpers.js";
10
- import { resolveAgentIdFromSessionKey, resolveGroupSessionKey, resolveSessionTranscriptPath, updateSessionStore, } from "../../config/sessions.js";
9
+ import { resolveAgentIdFromSessionKey, resolveGroupSessionKey, } from "../../config/sessions.js";
11
10
  import { logVerbose } from "../../globals.js";
12
11
  import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
13
12
  import { defaultRuntime } from "../../runtime.js";
@@ -198,7 +197,9 @@ export async function runAgentTurnWithFallback(params) {
198
197
  ownerNumbers: params.followupRun.run.ownerNumbers,
199
198
  enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider),
200
199
  senderE164: params.sessionCtx.SenderE164,
201
- // Suppress raw API errors for non-default agents (public-facing)
200
+ // Suppress raw API errors for non-default agents (public-facing).
201
+ // Default (admin) agent gets formatted error text from formatAssistantErrorText
202
+ // which provides human-readable messages for known error types.
202
203
  suppressRawErrors: (() => {
203
204
  const cfg = params.followupRun.run.config;
204
205
  if (!cfg)
@@ -405,7 +406,7 @@ export async function runAgentTurnWithFallback(params) {
405
406
  return {
406
407
  kind: "final",
407
408
  payload: {
408
- text: "⚠️ Message ordering conflict. I've reset the conversation - please try again.",
409
+ text: "⚠️ Conversation got into a bad state. I've reset it - please try again.",
409
410
  },
410
411
  };
411
412
  }
@@ -417,7 +418,7 @@ export async function runAgentTurnWithFallback(params) {
417
418
  const isContextOverflow = isLikelyContextOverflowError(message);
418
419
  const isCompactionFailure = isCompactionFailureError(message);
419
420
  const isSessionCorruption = /function call turn comes immediately after/i.test(message);
420
- const isRoleOrderingError = /incorrect role information|roles must alternate/i.test(message);
421
+ const isRoleOrderingError = /incorrect role information|roles must alternate|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(message);
421
422
  if (isCompactionFailure &&
422
423
  !didResetAfterCompactionFailure &&
423
424
  (await params.resetSessionAfterCompactionFailure(message))) {
@@ -435,52 +436,28 @@ export async function runAgentTurnWithFallback(params) {
435
436
  return {
436
437
  kind: "final",
437
438
  payload: {
438
- text: "⚠️ Message ordering conflict. I've reset the conversation - please try again.",
439
+ text: "⚠️ Conversation got into a bad state. I've reset it - please try again.",
439
440
  },
440
441
  };
441
442
  }
442
443
  }
443
444
  // Auto-recover from Gemini session corruption by resetting the session
444
- if (isSessionCorruption &&
445
- params.sessionKey &&
446
- params.activeSessionStore &&
447
- params.storePath) {
448
- const sessionKey = params.sessionKey;
449
- const corruptedSessionId = params.getActiveSessionEntry()?.sessionId;
450
- defaultRuntime.error(`Session history corrupted (Gemini function call ordering). Resetting session: ${params.sessionKey}`);
451
- try {
452
- // Delete transcript file if it exists
453
- if (corruptedSessionId) {
454
- const transcriptPath = resolveSessionTranscriptPath(corruptedSessionId);
455
- try {
456
- fs.unlinkSync(transcriptPath);
457
- }
458
- catch {
459
- // Ignore if file doesn't exist
460
- }
461
- }
462
- // Keep the in-memory snapshot consistent with the on-disk store reset.
463
- delete params.activeSessionStore[sessionKey];
464
- // Remove session entry from store using a fresh, locked snapshot.
465
- await updateSessionStore(params.storePath, (store) => {
466
- delete store[sessionKey];
467
- });
468
- }
469
- catch (cleanupErr) {
470
- defaultRuntime.error(`Failed to reset corrupted session ${params.sessionKey}: ${String(cleanupErr)}`);
445
+ if (isSessionCorruption) {
446
+ const didReset = await params.resetSessionAfterRoleOrderingConflict(message);
447
+ if (didReset) {
448
+ return {
449
+ kind: "final",
450
+ payload: {
451
+ text: "⚠️ Conversation got into a bad state. I've reset it - please try again.",
452
+ },
453
+ };
471
454
  }
472
- return {
473
- kind: "final",
474
- payload: {
475
- text: "⚠️ Session history was corrupted. I've reset the conversation - please try again!",
476
- },
477
- };
478
455
  }
479
456
  defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
480
457
  const fallbackText = isContextOverflow
481
458
  ? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
482
459
  : isRoleOrderingError
483
- ? "⚠️ Message ordering conflict - please try again. If this persists, use /new to start a fresh session."
460
+ ? "⚠️ Conversation got into a bad state. Use /new to start a fresh session."
484
461
  : "Sorry, I'm having trouble right now. Please try again in a few minutes.";
485
462
  return {
486
463
  kind: "final",
@@ -1,12 +1,11 @@
1
1
  import crypto from "node:crypto";
2
- import fs from "node:fs";
3
2
  import { lookupContextTokens } from "../../agents/context.js";
4
3
  import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
5
4
  import { resolveModelAuthMode } from "../../agents/model-auth.js";
6
5
  import { isCliProvider } from "../../agents/model-selection.js";
7
6
  import { queueEmbeddedPiMessage } from "../../agents/pi-embedded.js";
8
7
  import { hasNonzeroUsage } from "../../agents/usage.js";
9
- import { resolveAgentIdFromSessionKey, resolveSessionFilePath, resolveSessionTranscriptPath, updateSessionStore, updateSessionStoreEntry, } from "../../config/sessions.js";
8
+ import { resolveAgentIdFromSessionKey, resolveSessionTranscriptPath, updateSessionStore, updateSessionStoreEntry, } from "../../config/sessions.js";
10
9
  import { defaultRuntime } from "../../runtime.js";
11
10
  import { estimateUsageCost, resolveModelCostConfig } from "../../utils/usage-format.js";
12
11
  import { resolveResponseUsageMode } from "../thinking.js";
@@ -128,20 +127,29 @@ export async function runReplyAgent(params) {
128
127
  agentCfgContextTokens,
129
128
  });
130
129
  let responseUsageLine;
131
- const resetSession = async ({ failureLabel, buildLogMessage, cleanupTranscripts, }) => {
130
+ const resetSession = async ({ failureLabel, buildLogMessage, }) => {
132
131
  if (!sessionKey || !activeSessionStore || !storePath)
133
132
  return false;
134
133
  const prevEntry = activeSessionStore[sessionKey] ?? activeSessionEntry;
135
134
  if (!prevEntry)
136
135
  return false;
137
- const prevSessionId = cleanupTranscripts ? prevEntry.sessionId : undefined;
138
136
  const nextSessionId = crypto.randomUUID();
137
+ // Archive the previous session so it remains accessible via sessions_history.
138
+ const prevArchive = prevEntry.sessionId && prevEntry.sessionId !== nextSessionId
139
+ ? {
140
+ sessionId: prevEntry.sessionId,
141
+ sessionFile: prevEntry.sessionFile,
142
+ endedAt: Date.now(),
143
+ }
144
+ : undefined;
145
+ const existingArchive = prevEntry.previousSessions ?? [];
139
146
  const nextEntry = {
140
147
  ...prevEntry,
141
148
  sessionId: nextSessionId,
142
149
  updatedAt: Date.now(),
143
150
  systemSent: false,
144
151
  abortedLastRun: false,
152
+ previousSessions: prevArchive ? [...existingArchive, prevArchive] : existingArchive,
145
153
  };
146
154
  const agentId = resolveAgentIdFromSessionKey(sessionKey);
147
155
  const nextSessionFile = resolveSessionTranscriptPath(nextSessionId, agentId, sessionCtx.MessageThreadId);
@@ -160,21 +168,6 @@ export async function runReplyAgent(params) {
160
168
  activeSessionEntry = nextEntry;
161
169
  activeIsNewSession = true;
162
170
  defaultRuntime.error(buildLogMessage(nextSessionId));
163
- if (cleanupTranscripts && prevSessionId) {
164
- const transcriptCandidates = new Set();
165
- const resolved = resolveSessionFilePath(prevSessionId, prevEntry, { agentId });
166
- if (resolved)
167
- transcriptCandidates.add(resolved);
168
- transcriptCandidates.add(resolveSessionTranscriptPath(prevSessionId, agentId));
169
- for (const candidate of transcriptCandidates) {
170
- try {
171
- fs.unlinkSync(candidate);
172
- }
173
- catch {
174
- // Best-effort cleanup.
175
- }
176
- }
177
- }
178
171
  return true;
179
172
  };
180
173
  const resetSessionAfterCompactionFailure = async (reason) => resetSession({
@@ -182,9 +175,8 @@ export async function runReplyAgent(params) {
182
175
  buildLogMessage: (nextSessionId) => `Auto-compaction failed (${reason}). Restarting session ${sessionKey} -> ${nextSessionId} and retrying.`,
183
176
  });
184
177
  const resetSessionAfterRoleOrderingConflict = async (reason) => resetSession({
185
- failureLabel: "role ordering conflict",
186
- buildLogMessage: (nextSessionId) => `Role ordering conflict (${reason}). Restarting session ${sessionKey} -> ${nextSessionId}.`,
187
- cleanupTranscripts: true,
178
+ failureLabel: "session corruption",
179
+ buildLogMessage: (nextSessionId) => `Session corruption (${reason}). Restarting session ${sessionKey} -> ${nextSessionId}.`,
188
180
  });
189
181
  try {
190
182
  const runStartedAt = Date.now();
@@ -207,8 +199,6 @@ export async function runReplyAgent(params) {
207
199
  isHeartbeat,
208
200
  sessionKey,
209
201
  getActiveSessionEntry: () => activeSessionEntry,
210
- activeSessionStore,
211
- storePath,
212
202
  resolvedVerboseLevel,
213
203
  });
214
204
  if (runOutcome.kind === "final") {
@@ -9,6 +9,7 @@ import { resolveGroupSessionKey, resolveSessionFilePath, } from "../../config/se
9
9
  import { logVerbose } from "../../globals.js";
10
10
  import { clearCommandLane, getQueueSize } from "../../process/command-queue.js";
11
11
  import { normalizeMainKey } from "../../routing/session-key.js";
12
+ import { isReasoningTagProvider } from "../../utils/provider-utils.js";
12
13
  import { hasControlCommand } from "../command-detection.js";
13
14
  import { buildInboundMediaNote } from "../media-note.js";
14
15
  import { formatXHighModelHint, normalizeThinkLevel, supportsXHighThinking, } from "../thinking.js";
@@ -256,7 +257,7 @@ export async function runPreparedReply(params) {
256
257
  blockReplyBreak: resolvedBlockStreamingBreak,
257
258
  ownerNumbers: command.ownerList.length > 0 ? command.ownerList : undefined,
258
259
  extraSystemPrompt: extraSystemPrompt || undefined,
259
- enforceFinalTag: true,
260
+ ...(isReasoningTagProvider(provider) ? { enforceFinalTag: true } : {}),
260
261
  skipMessageDelivery: opts?.skipMessageDelivery,
261
262
  },
262
263
  };
@@ -260,7 +260,7 @@ export async function initSessionState(params) {
260
260
  sessionFile: previousSessionEntry.sessionFile,
261
261
  endedAt: Date.now(),
262
262
  };
263
- const existing = baseEntry?.previousSessions ?? [];
263
+ const existing = entry?.previousSessions ?? [];
264
264
  sessionEntry.previousSessions = [...existing, prev];
265
265
  }
266
266
  // Preserve per-session overrides while resetting compaction state on /new.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.11",
3
- "commit": "b6abbf7e2a7a62f677e6a5969f4d7c9054ad2894",
4
- "builtAt": "2026-02-15T10:55:15.410Z"
2
+ "version": "1.0.13",
3
+ "commit": "6fc1827ae47309649cd3752f4a0a9c5bca3786c5",
4
+ "builtAt": "2026-02-15T18:37:21.651Z"
5
5
  }