@rubytech/taskmaster 1.0.61 → 1.0.63

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.
@@ -30,7 +30,7 @@ import { acquireSessionWriteLock } from "../session-write-lock.js";
30
30
  import { applySkillEnvOverrides, applySkillEnvOverridesFromSnapshot, loadWorkspaceSkillEntries, resolveSkillsPromptForRun, } from "../skills.js";
31
31
  import { buildEmbeddedExtensionPaths } from "./extensions.js";
32
32
  import { logToolSchemasForGoogle, sanitizeSessionHistory, sanitizeToolsForGoogle, } from "./google.js";
33
- import { getDmHistoryLimitFromSessionKey, limitHistoryTurns } from "./history.js";
33
+ import { getEffectiveHistoryLimit, limitHistoryTurns, loadPreviousSessionMessages } from "./history.js";
34
34
  import { resolveGlobalLane, resolveSessionLane } from "./lanes.js";
35
35
  import { log } from "./logger.js";
36
36
  import { buildModelAliasLines, resolveModel } from "./model.js";
@@ -338,8 +338,12 @@ export async function compactEmbeddedPiSessionDirect(params) {
338
338
  resourceLoader,
339
339
  }));
340
340
  try {
341
+ const previousMessages = loadPreviousSessionMessages(params.sessionFile, params.sessionKey);
342
+ const combinedMessages = previousMessages.length > 0
343
+ ? [...previousMessages, ...session.messages]
344
+ : session.messages;
341
345
  const prior = await sanitizeSessionHistory({
342
- messages: session.messages,
346
+ messages: combinedMessages,
343
347
  modelApi: model.api,
344
348
  modelId,
345
349
  provider,
@@ -353,7 +357,7 @@ export async function compactEmbeddedPiSessionDirect(params) {
353
357
  const validated = transcriptPolicy.validateAnthropicTurns
354
358
  ? validateAnthropicTurns(validatedGemini)
355
359
  : validatedGemini;
356
- const limited = limitHistoryTurns(validated, getDmHistoryLimitFromSessionKey(params.sessionKey, params.config));
360
+ const limited = limitHistoryTurns(validated, getEffectiveHistoryLimit(params.sessionKey, params.config));
357
361
  if (limited.length > 0) {
358
362
  session.agent.replaceMessages(limited);
359
363
  }
@@ -1,3 +1,11 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Default rolling window: keep the last 50 user turns.
5
+ * Prevents context overflow without needing slow compaction.
6
+ * ~50 turns × ~2k tokens/turn ≈ 100k tokens, well within 200k context windows.
7
+ */
8
+ const DEFAULT_HISTORY_TURNS = 50;
1
9
  const THREAD_SUFFIX_REGEX = /^(.*)(?::(?:thread|topic):\d+)$/i;
2
10
  function stripThreadSuffix(value) {
3
11
  const match = value.match(THREAD_SUFFIX_REGEX);
@@ -59,3 +67,77 @@ export function getDmHistoryLimitFromSessionKey(sessionKey, config) {
59
67
  };
60
68
  return getLimit(resolveProviderConfig(config, provider));
61
69
  }
70
+ /**
71
+ * Resolves the effective history turn limit for any session.
72
+ * Priority: per-DM/channel config → agents.defaults.historyTurns → built-in default (50).
73
+ * This ensures ALL sessions have a rolling window, preventing context overflow and compaction.
74
+ */
75
+ export function getEffectiveHistoryLimit(sessionKey, config) {
76
+ // Check per-channel DM limit first
77
+ const channelLimit = getDmHistoryLimitFromSessionKey(sessionKey, config);
78
+ if (channelLimit !== undefined)
79
+ return channelLimit;
80
+ // Check agents.defaults.historyTurns
81
+ const configDefault = config?.agents?.defaults
82
+ ?.historyTurns;
83
+ if (typeof configDefault === "number" && Number.isFinite(configDefault) && configDefault > 0) {
84
+ return configDefault;
85
+ }
86
+ return DEFAULT_HISTORY_TURNS;
87
+ }
88
+ /**
89
+ * Loads messages from all previous sessions for a given session key.
90
+ * Returns them in chronological order (oldest first) as AgentMessage[].
91
+ * Used to prepend historical context to the current session's messages,
92
+ * creating one continuous conversation that the rolling window then trims.
93
+ */
94
+ export function loadPreviousSessionMessages(sessionFile, sessionKey) {
95
+ if (!sessionKey)
96
+ return [];
97
+ const sessionsDir = path.dirname(sessionFile);
98
+ const storePath = path.join(sessionsDir, "sessions.json");
99
+ if (!fs.existsSync(storePath))
100
+ return [];
101
+ let store;
102
+ try {
103
+ store = JSON.parse(fs.readFileSync(storePath, "utf-8"));
104
+ }
105
+ catch {
106
+ return [];
107
+ }
108
+ const entry = store[sessionKey];
109
+ if (!entry?.previousSessions?.length)
110
+ return [];
111
+ const messages = [];
112
+ for (const prev of entry.previousSessions) {
113
+ if (!prev.sessionId)
114
+ continue;
115
+ const candidates = [
116
+ prev.sessionFile,
117
+ path.join(sessionsDir, `${prev.sessionId}.jsonl`),
118
+ ].filter(Boolean);
119
+ const filePath = candidates.find((p) => fs.existsSync(p));
120
+ if (!filePath)
121
+ continue;
122
+ try {
123
+ const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
124
+ for (const line of lines) {
125
+ if (!line.trim())
126
+ continue;
127
+ try {
128
+ const parsed = JSON.parse(line);
129
+ if (parsed?.message?.role) {
130
+ messages.push(parsed.message);
131
+ }
132
+ }
133
+ catch {
134
+ // skip malformed lines
135
+ }
136
+ }
137
+ }
138
+ catch {
139
+ // skip unreadable files
140
+ }
141
+ }
142
+ return messages;
143
+ }
@@ -38,7 +38,7 @@ import { buildEmbeddedExtensionPaths } from "../extensions.js";
38
38
  import { applyExtraParamsToAgent } from "../extra-params.js";
39
39
  import { appendCacheTtlTimestamp, isCacheTtlEligibleProvider } from "../cache-ttl.js";
40
40
  import { logToolSchemasForGoogle, sanitizeSessionHistory, sanitizeToolsForGoogle, } from "../google.js";
41
- import { getDmHistoryLimitFromSessionKey, limitHistoryTurns } from "../history.js";
41
+ import { getEffectiveHistoryLimit, limitHistoryTurns, loadPreviousSessionMessages } from "../history.js";
42
42
  import { log } from "../logger.js";
43
43
  import { buildModelAliasLines } from "../model.js";
44
44
  import { clearActiveEmbeddedRun, setActiveEmbeddedRun, } from "../runs.js";
@@ -445,8 +445,14 @@ export async function runEmbeddedAttempt(params) {
445
445
  activeSession.agent.streamFn = anthropicPayloadLogger.wrapStreamFn(activeSession.agent.streamFn);
446
446
  }
447
447
  try {
448
+ // Load messages from previous sessions to create one continuous conversation.
449
+ // The rolling window then keeps only the last N turns for context.
450
+ const previousMessages = loadPreviousSessionMessages(params.sessionFile, params.sessionKey);
451
+ const combinedMessages = previousMessages.length > 0
452
+ ? [...previousMessages, ...activeSession.messages]
453
+ : activeSession.messages;
448
454
  const prior = await sanitizeSessionHistory({
449
- messages: activeSession.messages,
455
+ messages: combinedMessages,
450
456
  modelApi: params.model.api,
451
457
  modelId: params.modelId,
452
458
  provider: params.provider,
@@ -461,7 +467,7 @@ export async function runEmbeddedAttempt(params) {
461
467
  const validated = transcriptPolicy.validateAnthropicTurns
462
468
  ? validateAnthropicTurns(validatedGemini)
463
469
  : validatedGemini;
464
- const limited = limitHistoryTurns(validated, getDmHistoryLimitFromSessionKey(params.sessionKey, params.config));
470
+ const limited = limitHistoryTurns(validated, getEffectiveHistoryLimit(params.sessionKey, params.config));
465
471
  cacheTrace?.recordStage("session:limited", { messages: limited });
466
472
  if (limited.length > 0) {
467
473
  activeSession.agent.replaceMessages(limited);
@@ -1,7 +1,7 @@
1
1
  export { compactEmbeddedPiSession } from "./pi-embedded-runner/compact.js";
2
2
  export { applyExtraParamsToAgent, resolveExtraParams } from "./pi-embedded-runner/extra-params.js";
3
3
  export { applyGoogleTurnOrderingFix } from "./pi-embedded-runner/google.js";
4
- export { getDmHistoryLimitFromSessionKey, limitHistoryTurns, } from "./pi-embedded-runner/history.js";
4
+ export { getDmHistoryLimitFromSessionKey, getEffectiveHistoryLimit, limitHistoryTurns, loadPreviousSessionMessages, } from "./pi-embedded-runner/history.js";
5
5
  export { resolveEmbeddedSessionLane } from "./pi-embedded-runner/lanes.js";
6
6
  export { runEmbeddedPiAgent } from "./pi-embedded-runner/run.js";
7
7
  export { abortEmbeddedPiRun, isEmbeddedPiRunActive, isEmbeddedPiRunStreaming, queueEmbeddedPiMessage, waitForEmbeddedPiRunEnd, } from "./pi-embedded-runner/runs.js";
@@ -319,6 +319,12 @@ function buildChatCommands() {
319
319
  textAlias: "/new",
320
320
  acceptsArgs: true,
321
321
  }),
322
+ defineChatCommand({
323
+ key: "restore",
324
+ nativeName: "restore",
325
+ description: "Restore the most recent previous session.",
326
+ textAlias: "/restore",
327
+ }),
322
328
  defineChatCommand({
323
329
  key: "compact",
324
330
  description: "Compact the session context.",
@@ -5,6 +5,7 @@ import { createInternalHookEvent, triggerInternalHook } from "../../hooks/intern
5
5
  import { routeReply } from "./route-reply.js";
6
6
  import { handleBashCommand } from "./commands-bash.js";
7
7
  import { handleCompactCommand } from "./commands-compact.js";
8
+ import { handleRestoreCommand } from "./commands-restore.js";
8
9
  import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
9
10
  import { handleCommandsListCommand, handleContextCommand, handleHelpCommand, handleStatusCommand, handleWhoamiCommand, } from "./commands-info.js";
10
11
  import { handleAllowlistCommand } from "./commands-allowlist.js";
@@ -35,6 +36,7 @@ const HANDLERS = [
35
36
  handleDebugCommand,
36
37
  handleModelsCommand,
37
38
  handleStopCommand,
39
+ handleRestoreCommand,
38
40
  handleCompactCommand,
39
41
  handleAbortTrigger,
40
42
  ];
@@ -0,0 +1,64 @@
1
+ import { updateSessionStore } from "../../config/sessions.js";
2
+ import { logVerbose } from "../../globals.js";
3
+ export const handleRestoreCommand = async (params) => {
4
+ const normalized = params.command.commandBodyNormalized;
5
+ if (normalized !== "/restore" && !normalized.startsWith("/restore "))
6
+ return null;
7
+ if (!params.command.isAuthorizedSender) {
8
+ logVerbose(`Ignoring /restore from unauthorized sender: ${params.command.senderId || "<unknown>"}`);
9
+ return { shouldContinue: false };
10
+ }
11
+ if (!params.sessionEntry || !params.sessionStore || !params.sessionKey || !params.storePath) {
12
+ return {
13
+ shouldContinue: false,
14
+ reply: { text: "⚙️ Restore unavailable (missing session state)." },
15
+ };
16
+ }
17
+ const previousSessions = params.sessionEntry.previousSessions;
18
+ if (!Array.isArray(previousSessions) || previousSessions.length === 0) {
19
+ return {
20
+ shouldContinue: false,
21
+ reply: { text: "⚙️ No previous session to restore." },
22
+ };
23
+ }
24
+ // Pop the most recent previous session.
25
+ const target = previousSessions[previousSessions.length - 1];
26
+ if (!target?.sessionId) {
27
+ return {
28
+ shouldContinue: false,
29
+ reply: { text: "⚙️ Previous session entry is invalid." },
30
+ };
31
+ }
32
+ // Remove the restored entry from the archive.
33
+ const remaining = previousSessions.slice(0, -1);
34
+ // Build the restored session entry, keeping per-session overrides intact.
35
+ const restored = {
36
+ ...params.sessionEntry,
37
+ sessionId: target.sessionId,
38
+ sessionFile: target.sessionFile,
39
+ updatedAt: Date.now(),
40
+ previousSessions: remaining.length > 0 ? remaining : undefined,
41
+ // Reset flags so bootstrap files are re-sent into the restored context.
42
+ systemSent: false,
43
+ };
44
+ params.sessionStore[params.sessionKey] = restored;
45
+ try {
46
+ await updateSessionStore(params.storePath, (store) => {
47
+ store[params.sessionKey] = restored;
48
+ });
49
+ }
50
+ catch (err) {
51
+ return {
52
+ shouldContinue: false,
53
+ reply: { text: `⚙️ Restore failed: ${String(err)}` },
54
+ };
55
+ }
56
+ // Update the in-memory entry so subsequent handlers see the restored state.
57
+ Object.assign(params.sessionEntry, restored);
58
+ return {
59
+ shouldContinue: false,
60
+ reply: {
61
+ text: `⚙️ Restored previous session (${target.sessionId.slice(0, 8)}). Your next message will continue where you left off.`,
62
+ },
63
+ };
64
+ };
@@ -125,7 +125,7 @@ export async function initSessionState(params) {
125
125
  }
126
126
  sessionKey = resolveSessionKey(sessionScope, sessionCtxForState, mainKey);
127
127
  const entry = sessionStore[sessionKey];
128
- const previousSessionEntry = resetTriggered && entry ? { ...entry } : undefined;
128
+ let previousSessionEntry = resetTriggered && entry ? { ...entry } : undefined;
129
129
  const now = Date.now();
130
130
  const isThread = resolveThreadFlag({
131
131
  sessionKey,
@@ -166,6 +166,11 @@ export async function initSessionState(params) {
166
166
  systemSent = false;
167
167
  abortedLastRun = false;
168
168
  }
169
+ // Archive the expiring session for implicit resets (daily/idle expiry).
170
+ // Explicit resets (/new, /reset) already captured previousSessionEntry above.
171
+ if (isNewSession && !previousSessionEntry && entry) {
172
+ previousSessionEntry = { ...entry };
173
+ }
169
174
  const baseEntry = !isNewSession && freshEntry ? entry : undefined;
170
175
  // Track the originating channel/to for announce routing (subagent announce-back).
171
176
  const lastChannelRaw = ctx.OriginatingChannel || baseEntry?.lastChannel;
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.61",
3
- "commit": "dc1cbed9545e3ba0cba8704c2bb9b0e55e48625b",
4
- "builtAt": "2026-02-17T20:12:26.299Z"
2
+ "version": "1.0.63",
3
+ "commit": "15a1377031320e9d047b3aba1bcc9124e5b707d0",
4
+ "builtAt": "2026-02-18T00:18:16.896Z"
5
5
  }
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_IDLE_MINUTES } from "./types.js";
2
2
  import { normalizeMessageChannel } from "../../utils/message-channel.js";
3
- export const DEFAULT_RESET_MODE = "daily";
3
+ export const DEFAULT_RESET_MODE = "never";
4
4
  export const DEFAULT_RESET_AT_HOUR = 4;
5
5
  const THREAD_SESSION_MARKERS = [":thread:", ":topic:"];
6
6
  const GROUP_SESSION_MARKERS = [":group:", ":channel:"];
@@ -75,6 +75,9 @@ export function resolveChannelResetConfig(params) {
75
75
  return resetByChannel[key] ?? resetByChannel[key.toLowerCase()];
76
76
  }
77
77
  export function evaluateSessionFreshness(params) {
78
+ if (params.policy.mode === "never") {
79
+ return { fresh: true };
80
+ }
78
81
  const dailyResetAt = params.policy.mode === "daily"
79
82
  ? resolveDailyResetAtMs(params.now, params.policy.atHour)
80
83
  : undefined;
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { GroupChatSchema, InboundDebounceSchema, NativeCommandsSettingSchema, QueueSchema, TtsConfigSchema, } from "./zod-schema.core.js";
3
3
  const SessionResetConfigSchema = z
4
4
  .object({
5
- mode: z.union([z.literal("daily"), z.literal("idle")]).optional(),
5
+ mode: z.union([z.literal("never"), z.literal("daily"), z.literal("idle")]).optional(),
6
6
  atHour: z.number().int().min(0).max(23).optional(),
7
7
  idleMinutes: z.number().int().positive().optional(),
8
8
  })