@alisio/core 0.4.3 → 0.4.5

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.
@@ -321,6 +321,22 @@ export async function createApplication(options = {}) {
321
321
  }
322
322
  }
323
323
  }
324
+ /**
325
+ * Plugin resources (`api.resources.agents`) register during activation, but the built-in
326
+ * subagents plugin discovers agent files in its own setup, which runs BEFORE external plugins
327
+ * (config, command line, plugin directories) are activated. Re-run its discovery once every
328
+ * plugin has registered, so the first `/api/agents` (web selector and Agents page, TUI cycle)
329
+ * already lists the plugin primary agents instead of waiting for a manual `/agents reload`.
330
+ */
331
+ const rediscoverAgents = plugins.commands.get("agents");
332
+ if (rediscoverAgents) {
333
+ try {
334
+ await rediscoverAgents("reload");
335
+ }
336
+ catch {
337
+ /* keeps the built-ins: agent discovery is best-effort */
338
+ }
339
+ }
324
340
  // Every plugin has registered its providers: start the configured one in the background
325
341
  // (`activate` is bounded, never fatal and never awaited here).
326
342
  decisionService.syncActive();
@@ -2,6 +2,32 @@
2
2
  export const CHANGELOG_DATA = {
3
3
  schema: 1,
4
4
  entries: [
5
+ {
6
+ version: "0.4.5",
7
+ date: "2026-10-07",
8
+ sections: [
9
+ {
10
+ title: "Fixed",
11
+ items: [
12
+ "The chat agent selector, the Agents page and Shift+Tab list a plugin's `primary` agents on the first open, instead of only the built-ins until you visited the Agents window and came back.",
13
+ 'Choosing "Full access" applies to the session that is already running: it stops asking for approvals from the next tool call, not only on the following turn.',
14
+ ],
15
+ },
16
+ ],
17
+ },
18
+ {
19
+ version: "0.4.4",
20
+ date: "2026-10-04",
21
+ sections: [
22
+ {
23
+ title: "Fixed",
24
+ items: [
25
+ 'Automatic compaction now also triggers at 85 % of `limits.maxContextChars`, so a long conversation with a large-window model is compacted before the run fails instead of stopping with "Context budget exceeded" until you ran `/compact` yourself. A last-chance compaction runs once before a run fails, and the final error now says whether compaction ran, was skipped, failed or is disabled.',
26
+ 'The web shows "Compacting context…" during a manual `/compact` or an automatic one, and blocks sending (keeping your draft) until it finishes.',
27
+ ],
28
+ },
29
+ ],
30
+ },
5
31
  {
6
32
  version: "0.4.3",
7
33
  date: "2026-10-04",
@@ -302,7 +302,7 @@ export interface HookFailure {
302
302
  export interface RunnerExtensions {
303
303
  beforeCompact(input: {
304
304
  sessionId: string;
305
- reason: "manual" | "auto";
305
+ reason: "manual" | "auto" | "budget";
306
306
  messages: readonly Message[];
307
307
  focus?: string;
308
308
  }): Promise<{
@@ -316,7 +316,7 @@ export interface RunnerExtensions {
316
316
  }>;
317
317
  afterCompact(input: {
318
318
  sessionId: string;
319
- reason: "manual" | "auto";
319
+ reason: "manual" | "auto" | "budget";
320
320
  messages: readonly Message[];
321
321
  focus?: string;
322
322
  model: string;
@@ -189,6 +189,8 @@ export declare class AgentRunner {
189
189
  private inbox;
190
190
  /** Last provider-reported context size per session and the history length it covered. */
191
191
  private reported;
192
+ /** Conversation size in characters right after the last automatic compaction, per session. */
193
+ private compactedChars;
192
194
  /** Where human waits are opened; the clocks of the active runs listen to it. */
193
195
  readonly humanWaits: HumanWaits;
194
196
  constructor(options: RunnerOptions);
@@ -41,6 +41,13 @@ const MAX_INJECT_CHARS = 24_000;
41
41
  * fails with an actionable error instead of silently losing the whole history.
42
42
  */
43
43
  const MIN_MESSAGE_BUDGET_FLOOR = 4_000;
44
+ /**
45
+ * Fraction of the character limit the conversation must grow beyond its post-compaction size
46
+ * before the CHARACTER-based auto-compaction trigger may fire again. When the kept tail alone
47
+ * sits at or above the trigger, summarizing gains nothing, so without this margin every turn
48
+ * would pay for a summarizer call. The hard-limit last-chance compaction ignores it.
49
+ */
50
+ const RECOMPACT_GROWTH_MARGIN = 0.1;
44
51
  const allowed = (policy, effect) => effect === "read" ||
45
52
  effect === "internal" ||
46
53
  ((effect === "write" || effect === "process" || effect === "external") && !!policy[effect]);
@@ -62,6 +69,8 @@ export class AgentRunner {
62
69
  inbox = new Map();
63
70
  /** Last provider-reported context size per session and the history length it covered. */
64
71
  reported = new Map();
72
+ /** Conversation size in characters right after the last automatic compaction, per session. */
73
+ compactedChars = new Map();
65
74
  /** Where human waits are opened; the clocks of the active runs listen to it. */
66
75
  humanWaits;
67
76
  constructor(options) {
@@ -907,11 +916,24 @@ export class AgentRunner {
907
916
  // unknown (or absurdly large, see MAX_TRUSTED_WINDOW) window falls back to the char
908
917
  // estimate (`chars / 4 >= maxContextChars / 4`). `maxContextChars` below stays as the
909
918
  // post-compaction hard limit.
919
+ // The character budget is an independent trigger: `maxContextChars` is a hard limit for
920
+ // every model, so a large-window model must compact at `limit * threshold` characters
921
+ // too instead of reaching the hard check with compaction never having run.
922
+ const threshold = compaction.threshold ?? 0.85;
923
+ // What automatic compaction did this turn (at most ONE attempt per turn).
924
+ let attempt = "none";
925
+ let attemptError = "";
910
926
  if (compaction.auto !== false &&
911
- shouldCompactContext(used(), chars(), o.contextWindow?.(model), limit, compaction.threshold ?? 0.85)) {
912
- await this.compactLocked(sessionId, emit, model, combined, "auto");
927
+ (shouldCompactContext(used(), chars(), o.contextWindow?.(model), limit, threshold) ||
928
+ (charsConversation() >= limit * threshold &&
929
+ charsConversation() >=
930
+ (this.compactedChars.get(sessionId) ?? 0) + limit * RECOMPACT_GROWTH_MARGIN))) {
931
+ const result = await this.compactLocked(sessionId, emit, model, combined, "auto");
932
+ attempt = result ? "ran" : "skipped";
913
933
  instructions = await withPersona();
914
934
  messages = o.store.messages(sessionId);
935
+ if (result)
936
+ this.compactedChars.set(sessionId, charsConversation());
915
937
  }
916
938
  // A compaction that still leaves the session over the hard limit is reduced in place
917
939
  // instead of failing outright: retained content is clipped against a TOTAL character
@@ -925,15 +947,45 @@ export class AgentRunner {
925
947
  // prompts. Only if even the reduction floor cannot fit (instructions alone crowd out the
926
948
  // limit) the run fails with an actionable error.
927
949
  if (charsConversation() > limit) {
928
- const targetChars = Math.max(MIN_MESSAGE_BUDGET_FLOOR, limit - instructions.length);
929
- const reduction = reduceMessagesToBudget(messages, targetChars);
930
- if (reduction.truncated > 0) {
931
- o.store.overwrite(sessionId, reduction.messages);
932
- messages = reduction.messages;
933
- emit("context_reduced", { messages: reduction.truncated });
950
+ const reduceInPlace = () => {
951
+ const targetChars = Math.max(MIN_MESSAGE_BUDGET_FLOOR, limit - instructions.length);
952
+ const reduction = reduceMessagesToBudget(messages, targetChars);
953
+ if (reduction.truncated > 0) {
954
+ o.store.overwrite(sessionId, reduction.messages);
955
+ messages = reduction.messages;
956
+ emit("context_reduced", { messages: reduction.truncated });
957
+ }
958
+ };
959
+ reduceInPlace();
960
+ // Last chance: clipping alone was not enough and no compaction was attempted this
961
+ // turn, so summarize history once (reason "budget") and reduce again before failing.
962
+ if (charsConversation() > limit && compaction.auto !== false && attempt === "none") {
963
+ try {
964
+ const result = await this.compactLocked(sessionId, emit, model, combined, "budget");
965
+ attempt = result ? "ran" : "skipped";
966
+ }
967
+ catch (error) {
968
+ combined.throwIfAborted();
969
+ attempt = "failed";
970
+ attemptError = error instanceof Error ? error.message : String(error);
971
+ }
972
+ instructions = await withPersona();
973
+ messages = o.store.messages(sessionId);
974
+ if (charsConversation() > limit)
975
+ reduceInPlace();
976
+ if (attempt === "ran")
977
+ this.compactedChars.set(sessionId, charsConversation());
978
+ }
979
+ if (charsConversation() > limit) {
980
+ const what = compaction.auto === false
981
+ ? "Automatic compaction is disabled (compaction.auto is false)."
982
+ : attempt === "ran"
983
+ ? "Automatic compaction ran but could not reduce it enough."
984
+ : attempt === "failed"
985
+ ? `Automatic compaction failed (${attemptError}).`
986
+ : "Automatic compaction was skipped because there is no safe boundary to summarize (the conversation is a single long turn or too short).";
987
+ throw new Error(`Context budget exceeded (approximately ${charsConversation()} characters of conversation; limit ${limit}). ${what} Run /compact, trim large tool outputs, start a new session, or disable unneeded MCP servers with /plugins.`);
934
988
  }
935
- if (charsConversation() > limit)
936
- throw new Error(`Context budget exceeded and compaction could not reduce it (approximately ${charsConversation()} characters of conversation; limit ${limit}). Run /compact, trim large tool outputs, start a new session, or disable unneeded MCP servers with /plugins.`);
937
989
  }
938
990
  let completion;
939
991
  let truncated = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/core",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
5
5
  "author": "Gustavo Gutiérrez",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "node": ">=22.16"
42
42
  },
43
43
  "dependencies": {
44
- "@alisio/sdk": "0.4.3",
44
+ "@alisio/sdk": "0.4.5",
45
45
  "@mixmark-io/domino": "2.2.0",
46
46
  "@modelcontextprotocol/client": "2.1.0",
47
47
  "ajv": "8.20.0",