opencode-matrixx-v2 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6310,6 +6310,12 @@ var DcpManualModeSchema = object({
6310
6310
  enabled: boolean2().optional(),
6311
6311
  automaticStrategies: boolean2().optional()
6312
6312
  });
6313
+ var DcpWatchdogSchema = object({
6314
+ enabled: boolean2().default(true),
6315
+ abovePercent: number2().min(1).max(95).default(25),
6316
+ targetPercent: number2().min(1).max(94).default(10),
6317
+ cooldownTurns: number2().int().min(1).default(2)
6318
+ });
6313
6319
  var DcpProfileDefinitionSchema = object({
6314
6320
  pruneNotification: _enum(["off", "minimal", "detailed"]).optional(),
6315
6321
  pruneNotificationType: _enum(["chat", "toast"]).optional(),
@@ -6338,6 +6344,12 @@ var DcpConfigSchema = object({
6338
6344
  maxMessages: 6,
6339
6345
  keepFirst: 2,
6340
6346
  keepLast: 3
6347
+ }),
6348
+ watchdog: DcpWatchdogSchema.default({
6349
+ enabled: true,
6350
+ abovePercent: 25,
6351
+ targetPercent: 10,
6352
+ cooldownTurns: 2
6341
6353
  })
6342
6354
  });
6343
6355
 
@@ -6494,6 +6506,7 @@ var HookNameEnum = _enum([
6494
6506
  "knowledge-hub-injector",
6495
6507
  "knowledge-hub-search-nudge",
6496
6508
  "dcp-nudge-sanitizer",
6509
+ "compress-watchdog",
6497
6510
  "nudge-loop-breaker",
6498
6511
  "task-notepad-writer",
6499
6512
  "session-memory",
@@ -57,6 +57,13 @@ export declare const DcpManualModeSchema: z.ZodObject<{
57
57
  automaticStrategies: z.ZodOptional<z.ZodBoolean>;
58
58
  }, z.core.$strip>;
59
59
  export type DcpManualMode = z.infer<typeof DcpManualModeSchema>;
60
+ export declare const DcpWatchdogSchema: z.ZodObject<{
61
+ enabled: z.ZodDefault<z.ZodBoolean>;
62
+ abovePercent: z.ZodDefault<z.ZodNumber>;
63
+ targetPercent: z.ZodDefault<z.ZodNumber>;
64
+ cooldownTurns: z.ZodDefault<z.ZodNumber>;
65
+ }, z.core.$strip>;
66
+ export type DcpWatchdog = z.infer<typeof DcpWatchdogSchema>;
60
67
  export declare const DcpProfileDefinitionSchema: z.ZodObject<{
61
68
  pruneNotification: z.ZodOptional<z.ZodEnum<{
62
69
  off: "off";
@@ -363,5 +370,11 @@ export declare const DcpConfigSchema: z.ZodObject<{
363
370
  keepFirst: z.ZodDefault<z.ZodNumber>;
364
371
  keepLast: z.ZodDefault<z.ZodNumber>;
365
372
  }, z.core.$strip>>;
373
+ watchdog: z.ZodDefault<z.ZodObject<{
374
+ enabled: z.ZodDefault<z.ZodBoolean>;
375
+ abovePercent: z.ZodDefault<z.ZodNumber>;
376
+ targetPercent: z.ZodDefault<z.ZodNumber>;
377
+ cooldownTurns: z.ZodDefault<z.ZodNumber>;
378
+ }, z.core.$strip>>;
366
379
  }, z.core.$strip>;
367
380
  export type DcpConfig = z.infer<typeof DcpConfigSchema>;
@@ -61,6 +61,7 @@ export declare const HookNameSchema: z.ZodUnion<readonly [z.ZodEnum<{
61
61
  "knowledge-hub-injector": "knowledge-hub-injector";
62
62
  "knowledge-hub-search-nudge": "knowledge-hub-search-nudge";
63
63
  "dcp-nudge-sanitizer": "dcp-nudge-sanitizer";
64
+ "compress-watchdog": "compress-watchdog";
64
65
  "nudge-loop-breaker": "nudge-loop-breaker";
65
66
  "task-notepad-writer": "task-notepad-writer";
66
67
  "session-memory": "session-memory";
@@ -166,6 +166,7 @@ export declare const MatrixxConfigSchema: z.ZodObject<{
166
166
  "knowledge-hub-injector": "knowledge-hub-injector";
167
167
  "knowledge-hub-search-nudge": "knowledge-hub-search-nudge";
168
168
  "dcp-nudge-sanitizer": "dcp-nudge-sanitizer";
169
+ "compress-watchdog": "compress-watchdog";
169
170
  "nudge-loop-breaker": "nudge-loop-breaker";
170
171
  "task-notepad-writer": "task-notepad-writer";
171
172
  "session-memory": "session-memory";
@@ -927,6 +928,12 @@ export declare const MatrixxConfigSchema: z.ZodObject<{
927
928
  keepFirst: z.ZodDefault<z.ZodNumber>;
928
929
  keepLast: z.ZodDefault<z.ZodNumber>;
929
930
  }, z.core.$strip>>;
931
+ watchdog: z.ZodDefault<z.ZodObject<{
932
+ enabled: z.ZodDefault<z.ZodBoolean>;
933
+ abovePercent: z.ZodDefault<z.ZodNumber>;
934
+ targetPercent: z.ZodDefault<z.ZodNumber>;
935
+ cooldownTurns: z.ZodDefault<z.ZodNumber>;
936
+ }, z.core.$strip>>;
930
937
  }, z.core.$strip>>;
931
938
  auto_update: z.ZodOptional<z.ZodBoolean>;
932
939
  skills: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodObject<{
package/dist/index.js CHANGED
@@ -25573,6 +25573,12 @@ var DcpManualModeSchema = object({
25573
25573
  enabled: boolean2().optional(),
25574
25574
  automaticStrategies: boolean2().optional()
25575
25575
  });
25576
+ var DcpWatchdogSchema = object({
25577
+ enabled: boolean2().default(true),
25578
+ abovePercent: number2().min(1).max(95).default(25),
25579
+ targetPercent: number2().min(1).max(94).default(10),
25580
+ cooldownTurns: number2().int().min(1).default(2)
25581
+ });
25576
25582
  var DcpProfileDefinitionSchema = object({
25577
25583
  pruneNotification: _enum(["off", "minimal", "detailed"]).optional(),
25578
25584
  pruneNotificationType: _enum(["chat", "toast"]).optional(),
@@ -25692,6 +25698,12 @@ var DcpConfigSchema = object({
25692
25698
  maxMessages: 6,
25693
25699
  keepFirst: 2,
25694
25700
  keepLast: 3
25701
+ }),
25702
+ watchdog: DcpWatchdogSchema.default({
25703
+ enabled: true,
25704
+ abovePercent: 25,
25705
+ targetPercent: 10,
25706
+ cooldownTurns: 2
25695
25707
  })
25696
25708
  });
25697
25709
  // src/config/schema/evolution.ts
@@ -25844,6 +25856,7 @@ var HookNameEnum = _enum([
25844
25856
  "knowledge-hub-injector",
25845
25857
  "knowledge-hub-search-nudge",
25846
25858
  "dcp-nudge-sanitizer",
25859
+ "compress-watchdog",
25847
25860
  "nudge-loop-breaker",
25848
25861
  "task-notepad-writer",
25849
25862
  "session-memory",
@@ -91160,6 +91173,268 @@ function createNativePluginContext(ctx) {
91160
91173
  };
91161
91174
  }
91162
91175
 
91176
+ // src/shared/policy/compress-watchdog.ts
91177
+ var COMPRESS_WATCHDOG_MARKER = "<matrixx-compress-watchdog>";
91178
+ var DEFAULT_COMPRESS_WATCHDOG_CONFIG = {
91179
+ enabled: true,
91180
+ abovePercent: 25,
91181
+ targetPercent: 10,
91182
+ cooldownTurns: 2
91183
+ };
91184
+ function asRecord8(value) {
91185
+ return typeof value === "object" && value !== null ? value : undefined;
91186
+ }
91187
+ function finiteNumber(value) {
91188
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
91189
+ }
91190
+ function nonEmptyString(value) {
91191
+ return typeof value === "string" && value.length > 0 ? value : undefined;
91192
+ }
91193
+ function clampPercent(value) {
91194
+ return Math.min(95, Math.max(1, Math.round(value)));
91195
+ }
91196
+ function resolveCompressWatchdogConfig(input) {
91197
+ const abovePercent = clampPercent(finiteNumber(input?.abovePercent) ?? DEFAULT_COMPRESS_WATCHDOG_CONFIG.abovePercent);
91198
+ const requestedTarget = clampPercent(finiteNumber(input?.targetPercent) ?? DEFAULT_COMPRESS_WATCHDOG_CONFIG.targetPercent);
91199
+ const targetPercent = requestedTarget < abovePercent ? requestedTarget : Math.max(1, abovePercent - 1);
91200
+ const cooldownTurns = Math.max(1, Math.floor(finiteNumber(input?.cooldownTurns) ?? DEFAULT_COMPRESS_WATCHDOG_CONFIG.cooldownTurns));
91201
+ return {
91202
+ enabled: input?.enabled ?? DEFAULT_COMPRESS_WATCHDOG_CONFIG.enabled,
91203
+ abovePercent,
91204
+ targetPercent,
91205
+ cooldownTurns
91206
+ };
91207
+ }
91208
+ function extractContextUsage(messages) {
91209
+ for (let index = messages.length - 1;index >= 0; index--) {
91210
+ const message = asRecord8(messages[index]);
91211
+ if (!message)
91212
+ continue;
91213
+ const info = asRecord8(message.info);
91214
+ const role = message.type ?? message.role ?? info?.role;
91215
+ if (role !== "assistant")
91216
+ continue;
91217
+ const owner = info ?? message;
91218
+ const tokens = asRecord8(owner.tokens) ?? asRecord8(message.tokens);
91219
+ if (!tokens)
91220
+ continue;
91221
+ const output = finiteNumber(tokens.output) ?? 0;
91222
+ if (output <= 0)
91223
+ continue;
91224
+ const cache = asRecord8(tokens.cache);
91225
+ const total = (finiteNumber(tokens.input) ?? 0) + output + (finiteNumber(tokens.reasoning) ?? 0) + (finiteNumber(cache?.read) ?? 0) + (finiteNumber(cache?.write) ?? 0);
91226
+ if (total <= 0)
91227
+ continue;
91228
+ const model = asRecord8(owner.model) ?? asRecord8(message.model);
91229
+ const providerID = nonEmptyString(owner.providerID) ?? nonEmptyString(model?.providerID);
91230
+ const modelID = nonEmptyString(owner.modelID) ?? nonEmptyString(model?.modelID) ?? nonEmptyString(model?.id);
91231
+ return {
91232
+ tokens: total,
91233
+ ...providerID !== undefined ? { providerID } : {},
91234
+ ...modelID !== undefined ? { modelID } : {}
91235
+ };
91236
+ }
91237
+ return null;
91238
+ }
91239
+ function modelListItems(raw) {
91240
+ if (Array.isArray(raw))
91241
+ return raw;
91242
+ const record = asRecord8(raw);
91243
+ if (!record)
91244
+ return [];
91245
+ const models = record.models;
91246
+ if (Array.isArray(models))
91247
+ return models;
91248
+ const data = record.data;
91249
+ if (Array.isArray(data))
91250
+ return data;
91251
+ return [];
91252
+ }
91253
+ function resolveContextLimit(models, providerID, modelID) {
91254
+ if (providerID === undefined || modelID === undefined)
91255
+ return null;
91256
+ for (const item of modelListItems(models)) {
91257
+ const record = asRecord8(item);
91258
+ if (!record)
91259
+ continue;
91260
+ const itemProvider = nonEmptyString(record.providerID);
91261
+ const itemModel = nonEmptyString(record.id) ?? nonEmptyString(record.modelID);
91262
+ if (itemProvider !== providerID || itemModel !== modelID)
91263
+ continue;
91264
+ const limit = finiteNumber(asRecord8(record.limit)?.context);
91265
+ if (limit !== undefined && limit > 0)
91266
+ return limit;
91267
+ }
91268
+ return null;
91269
+ }
91270
+ function usagePercent(tokens, limit) {
91271
+ if (!Number.isFinite(tokens) || !Number.isFinite(limit) || limit <= 0)
91272
+ return 0;
91273
+ return tokens / limit * 100;
91274
+ }
91275
+ function shouldInjectCompressDirective(args) {
91276
+ const { config, currentPercent, turnsSinceLastInjection, lastUserText } = args;
91277
+ if (!config.enabled)
91278
+ return false;
91279
+ if (currentPercent < config.abovePercent)
91280
+ return false;
91281
+ if (turnsSinceLastInjection < config.cooldownTurns)
91282
+ return false;
91283
+ if (lastUserText?.includes(COMPRESS_WATCHDOG_MARKER))
91284
+ return false;
91285
+ return true;
91286
+ }
91287
+ function buildCompressDirective(args) {
91288
+ const { currentPercent, tokens, limit, config } = args;
91289
+ const used = currentPercent.toFixed(1);
91290
+ const tokenLabel = Math.round(tokens).toLocaleString("en-US");
91291
+ const limitLabel = Math.round(limit).toLocaleString("en-US");
91292
+ return [
91293
+ COMPRESS_WATCHDOG_MARKER,
91294
+ `CONTEXT WATCHDOG: this session is at ${used}% of a ${limitLabel}-token window (${tokenLabel} tokens). The configured ceiling is ${config.abovePercent}%.`,
91295
+ `Before continuing the task, call the \`compress\` tool on the LARGEST contiguous span of CLOSED messages (never the current turn): copy the span's start and end boundary IDs exactly as injected \u2014 do not invent or omit them \u2014 and replace the span with a technical summary that preserves decisions, file paths, open tasks, and error signatures.`,
91296
+ `Repeat until usage is at or below ${config.targetPercent}%. Do not ask the user for confirmation; compress and continue.`,
91297
+ COMPRESS_WATCHDOG_MARKER
91298
+ ].join(`
91299
+ `);
91300
+ }
91301
+
91302
+ // src/plugin/message-hooks/compress-watchdog.ts
91303
+ var MODEL_LIST_TTL_MS = 60000;
91304
+ var MAX_TRACKED_SESSIONS = 500;
91305
+ function findLastUserMessage2(messages) {
91306
+ for (let index = messages.length - 1;index >= 0; index--) {
91307
+ const message = messages[index];
91308
+ if (!message || roleOf(message) !== "user")
91309
+ continue;
91310
+ if (typeof message.text !== "string" || message.text.length === 0)
91311
+ return null;
91312
+ return { index, text: message.text };
91313
+ }
91314
+ return null;
91315
+ }
91316
+ function turnsSinceLastInjection(state) {
91317
+ if (state.lastInjectedTurn === null)
91318
+ return Number.POSITIVE_INFINITY;
91319
+ return state.turns - state.lastInjectedTurn;
91320
+ }
91321
+ function isSubagentSession(sessionID) {
91322
+ return subagentSessions.has(sessionID) || getParentSessionID(sessionID) !== undefined;
91323
+ }
91324
+ function hasCompressTool(event) {
91325
+ const tools = event.tools;
91326
+ return typeof tools === "object" && tools !== null && "compress" in tools;
91327
+ }
91328
+ async function readSessionUsage(ctx, event, messages) {
91329
+ const fromEvent = extractContextUsage(messages);
91330
+ if (fromEvent)
91331
+ return fromEvent;
91332
+ try {
91333
+ const raw = await ctx.session.context({ sessionID: event.sessionID });
91334
+ if (Array.isArray(raw))
91335
+ return extractContextUsage(raw);
91336
+ const nested = raw?.messages;
91337
+ return Array.isArray(nested) ? extractContextUsage(nested) : null;
91338
+ } catch (error) {
91339
+ log("[compress-watchdog] session context read failed", {
91340
+ sessionID: event.sessionID,
91341
+ error: error instanceof Error ? error.message : String(error)
91342
+ });
91343
+ return null;
91344
+ }
91345
+ }
91346
+ async function registerCompressWatchdog(ctx, options = {}) {
91347
+ const config = resolveCompressWatchdogConfig(options.config);
91348
+ if (!config.enabled || options.hookEnabled === false)
91349
+ return [];
91350
+ const now = options.now ?? (() => Date.now());
91351
+ const sessions = new Map;
91352
+ let limitsCache = null;
91353
+ async function loadModelList(force) {
91354
+ const fetchedAt = now();
91355
+ if (!force && limitsCache && fetchedAt - limitsCache.fetchedAt < MODEL_LIST_TTL_MS) {
91356
+ return limitsCache.raw;
91357
+ }
91358
+ try {
91359
+ const raw = await ctx.model.list();
91360
+ limitsCache = { fetchedAt, raw };
91361
+ return raw;
91362
+ } catch (error) {
91363
+ log("[compress-watchdog] model list read failed", {
91364
+ error: error instanceof Error ? error.message : String(error)
91365
+ });
91366
+ return limitsCache?.raw ?? null;
91367
+ }
91368
+ }
91369
+ const registration = await ctx.session.hook("context", async (event) => {
91370
+ try {
91371
+ if (!config.enabled)
91372
+ return;
91373
+ if (isSubagentSession(event.sessionID))
91374
+ return;
91375
+ if (!hasCompressTool(event))
91376
+ return;
91377
+ const state = sessions.get(event.sessionID) ?? { turns: 0, lastInjectedTurn: null };
91378
+ state.turns += 1;
91379
+ sessions.set(event.sessionID, state);
91380
+ if (sessions.size > MAX_TRACKED_SESSIONS) {
91381
+ const oldest = sessions.keys().next().value;
91382
+ if (oldest !== undefined)
91383
+ sessions.delete(oldest);
91384
+ }
91385
+ const rawMessages = Array.isArray(event.messages) ? event.messages : [];
91386
+ const usage = await readSessionUsage(ctx, event, rawMessages);
91387
+ if (!usage)
91388
+ return;
91389
+ let limit = resolveContextLimit(await loadModelList(false), usage.providerID, usage.modelID);
91390
+ if (limit === null) {
91391
+ limit = resolveContextLimit(await loadModelList(true), usage.providerID, usage.modelID);
91392
+ }
91393
+ if (limit === null)
91394
+ return;
91395
+ const currentPercent = usagePercent(usage.tokens, limit);
91396
+ const lastUser = findLastUserMessage2(rawMessages);
91397
+ const shouldInject = shouldInjectCompressDirective({
91398
+ config,
91399
+ currentPercent,
91400
+ turnsSinceLastInjection: turnsSinceLastInjection(state),
91401
+ lastUserText: lastUser?.text
91402
+ });
91403
+ if (!shouldInject || !lastUser)
91404
+ return;
91405
+ const target = rawMessages[lastUser.index];
91406
+ if (typeof target.text !== "string")
91407
+ return;
91408
+ const directive = buildCompressDirective({
91409
+ currentPercent,
91410
+ tokens: usage.tokens,
91411
+ limit,
91412
+ config
91413
+ });
91414
+ target.text = `${directive}
91415
+
91416
+ ---
91417
+
91418
+ ${target.text}`;
91419
+ state.lastInjectedTurn = state.turns;
91420
+ log("[compress-watchdog] directive injected", {
91421
+ sessionID: event.sessionID,
91422
+ currentPercent: Number(currentPercent.toFixed(1)),
91423
+ abovePercent: config.abovePercent,
91424
+ targetPercent: config.targetPercent,
91425
+ tokens: usage.tokens,
91426
+ limit
91427
+ });
91428
+ } catch (error) {
91429
+ log("[compress-watchdog] hook failed", {
91430
+ sessionID: event.sessionID,
91431
+ error: error instanceof Error ? error.message : String(error)
91432
+ });
91433
+ }
91434
+ });
91435
+ return [registration];
91436
+ }
91437
+
91163
91438
  // src/plugin/message-hooks/context-injector.ts
91164
91439
  async function applyContextInjectorMessagesTransform(event, deps = {}) {
91165
91440
  const messages = event?.messages;
@@ -91917,6 +92192,9 @@ async function registerMessageHooks(ctx, deps = {}) {
91917
92192
  registrations.push(...await registerThinkingBlockValidator(ctx));
91918
92193
  registrations.push(...await registerEvolutionHitl(ctx, deps.evolutionHitl));
91919
92194
  registrations.push(...await registerDcpNudgeSanitizer(ctx));
92195
+ if (deps.compressWatchdog) {
92196
+ registrations.push(...await registerCompressWatchdog(ctx, deps.compressWatchdog));
92197
+ }
91920
92198
  registrations.push(...await registerToolPairValidator(ctx));
91921
92199
  return registrations;
91922
92200
  }
@@ -91950,7 +92228,7 @@ async function guard(label, fn) {
91950
92228
  return;
91951
92229
  }
91952
92230
  }
91953
- function asRecord8(value) {
92231
+ function asRecord9(value) {
91954
92232
  return value !== null && typeof value === "object" ? value : {};
91955
92233
  }
91956
92234
  function hideNativeSubagentTool(editor) {
@@ -92070,7 +92348,12 @@ async function registerDomainSessionHooks(input) {
92070
92348
  if (registration)
92071
92349
  registrations.push(registration);
92072
92350
  }
92073
- const messageHookRegistrations = await guard("registerMessageHooks", () => registerMessageHooks(ctx));
92351
+ const messageHookRegistrations = await guard("registerMessageHooks", () => registerMessageHooks(ctx, {
92352
+ compressWatchdog: {
92353
+ config: input.pluginConfig?.dcp?.watchdog,
92354
+ hookEnabled: !(input.pluginConfig?.disabled_hooks ?? []).includes("compress-watchdog")
92355
+ }
92356
+ }));
92074
92357
  if (messageHookRegistrations)
92075
92358
  registrations.push(...messageHookRegistrations);
92076
92359
  if (hooks["experimental.session.compacting"]) {
@@ -92138,7 +92421,7 @@ async function registerDomainConfigTransforms(input) {
92138
92421
  return [];
92139
92422
  }
92140
92423
  const registrations = [];
92141
- const agents = asRecord8(draft.agent ?? draft.agents);
92424
+ const agents = asRecord9(draft.agent ?? draft.agents);
92142
92425
  if (Object.keys(agents).length > 0) {
92143
92426
  const defaultAgent = typeof draft.default_agent === "string" ? draft.default_agent : undefined;
92144
92427
  const result = await guard("config:agent", () => registerAgentTransform(ctx.agent, { agents, defaultAgent }));
@@ -92152,7 +92435,7 @@ async function registerDomainConfigTransforms(input) {
92152
92435
  registrations.push(result.registration);
92153
92436
  }
92154
92437
  }
92155
- const mcps = asRecord8(draft.mcp);
92438
+ const mcps = asRecord9(draft.mcp);
92156
92439
  if (Object.keys(mcps).length > 0) {
92157
92440
  const result = await guard("config:mcp", () => registerMcpTransform(ctx.mcp, mcps));
92158
92441
  if (result) {
@@ -92162,7 +92445,7 @@ async function registerDomainConfigTransforms(input) {
92162
92445
  registrations.push(result.registration);
92163
92446
  }
92164
92447
  }
92165
- const commands = asRecord8(draft.command);
92448
+ const commands = asRecord9(draft.command);
92166
92449
  if (Object.keys(commands).length > 0) {
92167
92450
  const v2ctx = ctx;
92168
92451
  const result = await guard("config:command", () => registerCommandTransform(ctx.command, commands, {
@@ -92172,7 +92455,7 @@ async function registerDomainConfigTransforms(input) {
92172
92455
  if (result)
92173
92456
  registrations.push(result.registration);
92174
92457
  }
92175
- const skills = asRecord8(draft.skill);
92458
+ const skills = asRecord9(draft.skill);
92176
92459
  if (Object.keys(skills).length > 0) {
92177
92460
  const result = await guard("config:skill", () => registerSkillTransform(ctx.skill, skills));
92178
92461
  if (result)
@@ -92304,6 +92587,7 @@ async function setupDomains(ctx) {
92304
92587
  return {
92305
92588
  directory: pctx.directory,
92306
92589
  worktree: pctx.worktree,
92590
+ pluginConfig,
92307
92591
  hooks: assembled
92308
92592
  };
92309
92593
  }
@@ -92314,8 +92598,8 @@ async function setup(ctx) {
92314
92598
  directory: ctx.location?.directory,
92315
92599
  opencode: ctx.app?.version
92316
92600
  });
92317
- const { hooks, directory, worktree } = await setupDomains(ctx);
92318
- const bridge = await registerDomains({ ctx, hooks, directory, worktree });
92601
+ const { hooks, directory, worktree, pluginConfig } = await setupDomains(ctx);
92602
+ const bridge = await registerDomains({ ctx, hooks, directory, worktree, pluginConfig });
92319
92603
  log("[matrixx] ready", { registrations: bridge.registrations.length });
92320
92604
  return async () => {
92321
92605
  await bridge.dispose();
@@ -252,6 +252,7 @@
252
252
  "knowledge-hub-injector",
253
253
  "knowledge-hub-search-nudge",
254
254
  "dcp-nudge-sanitizer",
255
+ "compress-watchdog",
255
256
  "nudge-loop-breaker",
256
257
  "task-notepad-writer",
257
258
  "session-memory",
@@ -3050,12 +3051,53 @@
3050
3051
  "keepLast"
3051
3052
  ],
3052
3053
  "additionalProperties": false
3054
+ },
3055
+ "watchdog": {
3056
+ "default": {
3057
+ "enabled": true,
3058
+ "abovePercent": 25,
3059
+ "targetPercent": 10,
3060
+ "cooldownTurns": 2
3061
+ },
3062
+ "type": "object",
3063
+ "properties": {
3064
+ "enabled": {
3065
+ "default": true,
3066
+ "type": "boolean"
3067
+ },
3068
+ "abovePercent": {
3069
+ "default": 25,
3070
+ "type": "number",
3071
+ "minimum": 1,
3072
+ "maximum": 95
3073
+ },
3074
+ "targetPercent": {
3075
+ "default": 10,
3076
+ "type": "number",
3077
+ "minimum": 1,
3078
+ "maximum": 94
3079
+ },
3080
+ "cooldownTurns": {
3081
+ "default": 2,
3082
+ "type": "integer",
3083
+ "minimum": 1,
3084
+ "maximum": 9007199254740991
3085
+ }
3086
+ },
3087
+ "required": [
3088
+ "enabled",
3089
+ "abovePercent",
3090
+ "targetPercent",
3091
+ "cooldownTurns"
3092
+ ],
3093
+ "additionalProperties": false
3053
3094
  }
3054
3095
  },
3055
3096
  "required": [
3056
3097
  "enabled",
3057
3098
  "profiles",
3058
- "handoffCompression"
3099
+ "handoffCompression",
3100
+ "watchdog"
3059
3101
  ],
3060
3102
  "additionalProperties": false
3061
3103
  },
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Compress watchdog — native session hook.
3
+ *
4
+ * Registers one `session.hook("context")` callback and, when the configured
5
+ * usage ceiling is crossed, prepends an explicit compress directive to the tail
6
+ * of the last user message. Tail injection preserves the prompt-cache prefix;
7
+ * the system prompt is never touched.
8
+ *
9
+ * Fail-open everywhere: an unknown model window, a missing token figure, a
10
+ * subagent session, or a missing `compress` tool all skip silently instead of
11
+ * injecting an instruction the model cannot satisfy.
12
+ */
13
+ import { COMPRESS_WATCHDOG_MARKER, type CompressWatchdogConfigInput } from "../../shared/policy/compress-watchdog";
14
+ import type { MatrixxContext, V2Registration } from "../tool/context";
15
+ export interface CompressWatchdogOptions {
16
+ config?: CompressWatchdogConfigInput | undefined;
17
+ hookEnabled?: boolean | undefined;
18
+ now?: (() => number) | undefined;
19
+ }
20
+ export interface LastUserMessage {
21
+ index: number;
22
+ text: string;
23
+ }
24
+ /** Newest user message that carries text, or `null` when there is none. */
25
+ export declare function findLastUserMessage(messages: readonly unknown[]): LastUserMessage | null;
26
+ export interface SessionWatchState {
27
+ turns: number;
28
+ lastInjectedTurn: number | null;
29
+ }
30
+ export declare function turnsSinceLastInjection(state: SessionWatchState): number;
31
+ /**
32
+ * Register the watchdog. Returns an empty list when disabled or when the
33
+ * `compress-watchdog` hook name is in `disabled_hooks`.
34
+ */
35
+ export declare function registerCompressWatchdog(ctx: MatrixxContext, options?: CompressWatchdogOptions): Promise<V2Registration[]>;
36
+ export { COMPRESS_WATCHDOG_MARKER };
@@ -1,6 +1,7 @@
1
1
  import type { EvolutionHitlDeps } from "../../hooks/evolution-hitl/flat";
2
2
  import type { SessionMessage } from "../../shared/session/session-message";
3
3
  import type { MatrixxContext, V2Registration } from "../tool/context";
4
+ import { type CompressWatchdogOptions } from "./compress-watchdog";
4
5
  import { type ContextInjectorDeps } from "./context-injector";
5
6
  import { type KnowledgeHubInjectorDeps } from "./knowledge-hub-injector";
6
7
  /** Native session hook kinds that carry messages. */
@@ -39,6 +40,7 @@ export interface MessageHookDeps {
39
40
  readonly evolutionHitl?: EvolutionHitlDeps & {
40
41
  readonly config?: unknown;
41
42
  };
43
+ readonly compressWatchdog?: CompressWatchdogOptions;
42
44
  }
43
45
  /**
44
46
  * Register every mapping row through its per-hook module in
@@ -1,3 +1,4 @@
1
+ import type { MatrixxConfig } from "../../config";
1
2
  import type { MatrixxContext, V2Registration } from "../tool/context";
2
3
  import type { Hooks } from "../tool/legacy-hooks";
3
4
  export interface RegisterDomainsInput {
@@ -5,6 +6,7 @@ export interface RegisterDomainsInput {
5
6
  hooks: Hooks;
6
7
  directory: string;
7
8
  worktree: string;
9
+ pluginConfig?: MatrixxConfig;
8
10
  }
9
11
  export interface DomainRegistration {
10
12
  registrations: V2Registration[];
@@ -18,6 +18,7 @@
18
18
  * `BackgroundManager` talks to `session.create`/`promptAsync` directly and
19
19
  * the tmux pane mirror is gone.
20
20
  */
21
+ import type { MatrixxConfig } from "../config";
21
22
  import type { MatrixxContext } from "./tool/context";
22
23
  import type { Hooks } from "./tool/legacy-hooks";
23
24
  /**
@@ -29,6 +30,7 @@ export declare function setupDomains(ctx: MatrixxContext): Promise<{
29
30
  directory: string;
30
31
  worktree: string;
31
32
  hooks: Hooks;
33
+ pluginConfig: MatrixxConfig;
32
34
  }>;
33
35
  export type { DomainRegistration, RegisterDomainsInput, } from "./setup-domains/assemble";
34
36
  export { hideNativeSubagentTool, hookOutputText, registerDomains, } from "./setup-domains/assemble";
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Compress watchdog policy (pure).
3
+ *
4
+ * DCP's `compress.maxContextLimit` is a *soft* nudge threshold: above it DCP
5
+ * only injects a generic "compress" reminder and the model decides whether,
6
+ * when, and how much to compress. When the model's per-turn growth matches its
7
+ * per-call savings the session settles above the configured ceiling.
8
+ *
9
+ * This module is the deterministic half of the Matrixx counter-measure: given
10
+ * the current usage, the configured ceiling, and the cooldown state, it decides
11
+ * whether an explicit directive should be injected into the tail of the last
12
+ * user message. Every threshold comes from configuration — nothing is
13
+ * hardcoded here except the marker and the wording of the directive.
14
+ *
15
+ * No I/O, no timers: safe to unit test in isolation.
16
+ */
17
+ /** Config consumed by the watchdog (built from `dcp.watchdog`). */
18
+ export interface CompressWatchdogConfig {
19
+ enabled: boolean;
20
+ /** Usage percentage that arms the directive. */
21
+ abovePercent: number;
22
+ /** Usage percentage the directive asks the model to reach. */
23
+ targetPercent: number;
24
+ /** Context builds to wait between directives while still over the ceiling. */
25
+ cooldownTurns: number;
26
+ }
27
+ /** Partial form as it arrives from `matrixx.jsonc` (Zod defaults may be absent). */
28
+ export type CompressWatchdogConfigInput = Partial<CompressWatchdogConfig>;
29
+ /** Sentinel that makes injection idempotent across repeated context builds. */
30
+ export declare const COMPRESS_WATCHDOG_MARKER = "<matrixx-compress-watchdog>";
31
+ export declare const DEFAULT_COMPRESS_WATCHDOG_CONFIG: CompressWatchdogConfig;
32
+ /** Context usage read from the newest assistant message that reports tokens. */
33
+ export interface ContextUsage {
34
+ tokens: number;
35
+ providerID?: string;
36
+ modelID?: string;
37
+ }
38
+ /**
39
+ * Normalize a partial config into a usable one. `targetPercent` is kept strictly
40
+ * below `abovePercent` so a misconfiguration cannot ask for a target the
41
+ * directive will never satisfy.
42
+ */
43
+ export declare function resolveCompressWatchdogConfig(input?: CompressWatchdogConfigInput | undefined): CompressWatchdogConfig;
44
+ /**
45
+ * Read the newest usage figure from a message list.
46
+ *
47
+ * Tolerant by design: the V2 session event has carried both flat messages
48
+ * (`{ type, tokens }`) and `{ info, parts }` projections across runtimes. Both
49
+ * shapes are accepted; anything else is skipped. A message without reported
50
+ * tokens is not evidence of low usage, so it is skipped rather than treated as
51
+ * zero.
52
+ */
53
+ export declare function extractContextUsage(messages: readonly unknown[]): ContextUsage | null;
54
+ /** Unwrap a model-list response (`Array`, `{ models }`, or `{ data }`). */
55
+ export declare function modelListItems(raw: unknown): unknown[];
56
+ /**
57
+ * Resolve the model's context window from the provider model list. Returns
58
+ * `null` when the model cannot be matched — callers must fail open rather than
59
+ * guess a window.
60
+ */
61
+ export declare function resolveContextLimit(models: unknown, providerID?: string | undefined, modelID?: string | undefined): number | null;
62
+ /** Usage as a percentage of the window (0 when the window is unusable). */
63
+ export declare function usagePercent(tokens: number, limit: number): number;
64
+ export interface ShouldInjectCompressDirectiveArgs {
65
+ config: CompressWatchdogConfig;
66
+ currentPercent: number;
67
+ /** Context builds since the last injection (cooldown accounting). */
68
+ turnsSinceLastInjection: number;
69
+ /** Text of the message the directive would be appended to. */
70
+ lastUserText?: string | undefined;
71
+ }
72
+ export declare function shouldInjectCompressDirective(args: ShouldInjectCompressDirectiveArgs): boolean;
73
+ export interface BuildCompressDirectiveArgs {
74
+ currentPercent: number;
75
+ tokens: number;
76
+ limit: number;
77
+ config: CompressWatchdogConfig;
78
+ }
79
+ /**
80
+ * Build the directive appended to the tail of the conversation.
81
+ *
82
+ * It is deliberately explicit about the mechanics the model got wrong in
83
+ * practice: compress a *contiguous closed* span, copy boundary IDs verbatim,
84
+ * preserve durable facts, and repeat until the target is met.
85
+ */
86
+ export declare function buildCompressDirective(args: BuildCompressDirectiveArgs): string;
package/dist/tools.js CHANGED
@@ -8535,6 +8535,12 @@ var DcpManualModeSchema = object({
8535
8535
  enabled: boolean2().optional(),
8536
8536
  automaticStrategies: boolean2().optional()
8537
8537
  });
8538
+ var DcpWatchdogSchema = object({
8539
+ enabled: boolean2().default(true),
8540
+ abovePercent: number2().min(1).max(95).default(25),
8541
+ targetPercent: number2().min(1).max(94).default(10),
8542
+ cooldownTurns: number2().int().min(1).default(2)
8543
+ });
8538
8544
  var DcpProfileDefinitionSchema = object({
8539
8545
  pruneNotification: _enum(["off", "minimal", "detailed"]).optional(),
8540
8546
  pruneNotificationType: _enum(["chat", "toast"]).optional(),
@@ -8563,6 +8569,12 @@ var DcpConfigSchema = object({
8563
8569
  maxMessages: 6,
8564
8570
  keepFirst: 2,
8565
8571
  keepLast: 3
8572
+ }),
8573
+ watchdog: DcpWatchdogSchema.default({
8574
+ enabled: true,
8575
+ abovePercent: 25,
8576
+ targetPercent: 10,
8577
+ cooldownTurns: 2
8566
8578
  })
8567
8579
  });
8568
8580
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-matrixx-v2",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",