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 +13 -0
- package/dist/config/schema/dcp.d.ts +13 -0
- package/dist/config/schema/hooks.d.ts +1 -0
- package/dist/config/schema/matrixx-config.d.ts +7 -0
- package/dist/index.js +292 -8
- package/dist/matrixx.schema.json +43 -1
- package/dist/plugin/message-hooks/compress-watchdog.d.ts +36 -0
- package/dist/plugin/message-hooks/index.d.ts +2 -0
- package/dist/plugin/setup-domains/assemble.d.ts +2 -0
- package/dist/plugin/setup-domains.d.ts +2 -0
- package/dist/shared/policy/compress-watchdog.d.ts +86 -0
- package/dist/tools.js +12 -0
- package/package.json +1 -1
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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();
|
package/dist/matrixx.schema.json
CHANGED
|
@@ -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.
|
|
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",
|