@lexwdex-org/opencode-dcp 3.4.13 → 3.5.0
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/README.md +23 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +573 -81
- package/dist/index.js.map +1 -1
- package/dist/lib/auto-prune.d.ts.map +1 -1
- package/dist/lib/config.d.ts +14 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts +24 -5
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/prune-service.d.ts +104 -0
- package/dist/lib/prune-service.d.ts.map +1 -0
- package/dist/lib/prune-tool.d.ts +2 -4
- package/dist/lib/prune-tool.d.ts.map +1 -1
- package/dist/lib/session-boundary.d.ts +90 -0
- package/dist/lib/session-boundary.d.ts.map +1 -0
- package/dist/lib/summarize.d.ts +3 -0
- package/dist/lib/summarize.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ function jaccard(a, b) {
|
|
|
27
27
|
}
|
|
28
28
|
var WINDOW_SIZE = 4;
|
|
29
29
|
var DRIFT_BASELINE = 3;
|
|
30
|
+
var MIN_DRIFT_TOKENS = 6;
|
|
30
31
|
function extractText(parts) {
|
|
31
32
|
const texts = [];
|
|
32
33
|
for (const part of parts) {
|
|
@@ -81,18 +82,21 @@ var AutoPruner = class {
|
|
|
81
82
|
evaluate(state, text, at) {
|
|
82
83
|
if (state.count + 1 < this.config.minMessages) return [];
|
|
83
84
|
const signals = [];
|
|
84
|
-
|
|
85
|
+
const enabled = this.config.signals;
|
|
86
|
+
if (enabled.idleGap && state.count > 0 && at - state.lastAt >= this.config.idleGapMs) {
|
|
85
87
|
signals.push("idle-gap");
|
|
86
88
|
}
|
|
87
|
-
if (state.count >= DRIFT_BASELINE && text) {
|
|
89
|
+
if (enabled.topicDrift && state.count >= DRIFT_BASELINE && text) {
|
|
88
90
|
const current = tokenize(text);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
if (current.size >= MIN_DRIFT_TOKENS) {
|
|
92
|
+
let max = 0;
|
|
93
|
+
for (let index = Math.max(0, state.window.length - DRIFT_BASELINE); index < state.window.length; index++) {
|
|
94
|
+
max = Math.max(max, jaccard(current, tokenize(state.window[index])));
|
|
95
|
+
}
|
|
96
|
+
if (max < this.config.driftThreshold) signals.push("topic-drift");
|
|
92
97
|
}
|
|
93
|
-
if (max < this.config.driftThreshold) signals.push("topic-drift");
|
|
94
98
|
}
|
|
95
|
-
if (state.count + 1 >= this.config.volumeThreshold) signals.push("volume");
|
|
99
|
+
if (enabled.volume && state.count + 1 >= this.config.volumeThreshold) signals.push("volume");
|
|
96
100
|
return signals;
|
|
97
101
|
}
|
|
98
102
|
state(sessionID) {
|
|
@@ -977,6 +981,7 @@ var ParseErrorCode;
|
|
|
977
981
|
var DEFAULT_FAILURE_COOLDOWN_MS = 3e4;
|
|
978
982
|
var DEFAULT_AUTO_PRUNE = {
|
|
979
983
|
enabled: true,
|
|
984
|
+
signals: { topicDrift: true, volume: false, idleGap: false },
|
|
980
985
|
minMessages: 8,
|
|
981
986
|
volumeThreshold: 30,
|
|
982
987
|
driftThreshold: 0.18,
|
|
@@ -997,6 +1002,10 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
997
1002
|
"summarize.failureCooldownMs",
|
|
998
1003
|
"autoPrune",
|
|
999
1004
|
"autoPrune.enabled",
|
|
1005
|
+
"autoPrune.signals",
|
|
1006
|
+
"autoPrune.signals.topicDrift",
|
|
1007
|
+
"autoPrune.signals.volume",
|
|
1008
|
+
"autoPrune.signals.idleGap",
|
|
1000
1009
|
"autoPrune.minMessages",
|
|
1001
1010
|
"autoPrune.volumeThreshold",
|
|
1002
1011
|
"autoPrune.driftThreshold",
|
|
@@ -1171,6 +1180,27 @@ function validateConfigTypes(config) {
|
|
|
1171
1180
|
});
|
|
1172
1181
|
}
|
|
1173
1182
|
}
|
|
1183
|
+
const signals = autoPrune.signals;
|
|
1184
|
+
if (signals !== void 0) {
|
|
1185
|
+
if (typeof signals !== "object" || signals === null || Array.isArray(signals)) {
|
|
1186
|
+
errors.push({
|
|
1187
|
+
key: "autoPrune.signals",
|
|
1188
|
+
expected: "object",
|
|
1189
|
+
actual: typeof signals
|
|
1190
|
+
});
|
|
1191
|
+
} else {
|
|
1192
|
+
for (const key of ["topicDrift", "volume", "idleGap"]) {
|
|
1193
|
+
const value = signals[key];
|
|
1194
|
+
if (value !== void 0 && typeof value !== "boolean") {
|
|
1195
|
+
errors.push({
|
|
1196
|
+
key: `autoPrune.signals.${key}`,
|
|
1197
|
+
expected: "boolean",
|
|
1198
|
+
actual: typeof value
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1174
1204
|
}
|
|
1175
1205
|
}
|
|
1176
1206
|
const tool2 = config.tool;
|
|
@@ -1191,15 +1221,25 @@ function validateConfigTypes(config) {
|
|
|
1191
1221
|
}
|
|
1192
1222
|
return errors;
|
|
1193
1223
|
}
|
|
1224
|
+
function needsSignalsMigrationHint(configData) {
|
|
1225
|
+
const autoPrune = configData.autoPrune;
|
|
1226
|
+
return autoPrune !== null && typeof autoPrune === "object" && !Array.isArray(autoPrune) && autoPrune.enabled === true && autoPrune.signals === void 0;
|
|
1227
|
+
}
|
|
1194
1228
|
function showConfigWarnings(ctx, configPath, configData, isProject) {
|
|
1195
1229
|
const invalidKeys = getInvalidConfigKeys(configData);
|
|
1196
1230
|
const deprecatedKeys = getDeprecatedConfigKeys(configData);
|
|
1197
1231
|
const typeErrors = validateConfigTypes(configData);
|
|
1198
|
-
|
|
1232
|
+
const signalsHint = needsSignalsMigrationHint(configData);
|
|
1233
|
+
if (!signalsHint && invalidKeys.length === 0 && deprecatedKeys.length === 0 && typeErrors.length === 0) {
|
|
1199
1234
|
return;
|
|
1200
1235
|
}
|
|
1201
1236
|
const configType = isProject ? "project config" : "config";
|
|
1202
1237
|
const messages = [];
|
|
1238
|
+
if (signalsHint) {
|
|
1239
|
+
messages.push(
|
|
1240
|
+
"auto-prune signals `volume` and `idleGap` are now disabled by default; re-enable via autoPrune.signals.*"
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1203
1243
|
if (deprecatedKeys.length > 0) {
|
|
1204
1244
|
const keyList = deprecatedKeys.slice(0, 3).join(", ");
|
|
1205
1245
|
const suffix = deprecatedKeys.length > 3 ? ` (+${deprecatedKeys.length - 3} more)` : "";
|
|
@@ -1348,8 +1388,15 @@ function mergeAutoPrune(base, override) {
|
|
|
1348
1388
|
return base;
|
|
1349
1389
|
}
|
|
1350
1390
|
const number = (key, min, max = Number.POSITIVE_INFINITY) => typeof override[key] === "number" && Number.isFinite(override[key]) && override[key] >= min && override[key] <= max ? override[key] : base[key];
|
|
1391
|
+
const signalsOverride = override.signals;
|
|
1392
|
+
const signals = signalsOverride && typeof signalsOverride === "object" && !Array.isArray(signalsOverride) ? {
|
|
1393
|
+
topicDrift: typeof signalsOverride.topicDrift === "boolean" ? signalsOverride.topicDrift : base.signals.topicDrift,
|
|
1394
|
+
volume: typeof signalsOverride.volume === "boolean" ? signalsOverride.volume : base.signals.volume,
|
|
1395
|
+
idleGap: typeof signalsOverride.idleGap === "boolean" ? signalsOverride.idleGap : base.signals.idleGap
|
|
1396
|
+
} : base.signals;
|
|
1351
1397
|
return {
|
|
1352
1398
|
enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled,
|
|
1399
|
+
signals,
|
|
1353
1400
|
minMessages: number("minMessages", 1),
|
|
1354
1401
|
volumeThreshold: number("volumeThreshold", 2),
|
|
1355
1402
|
driftThreshold: number("driftThreshold", 0, 1),
|
|
@@ -1371,7 +1418,7 @@ function deepCloneConfig(config) {
|
|
|
1371
1418
|
commands: { ...config.commands },
|
|
1372
1419
|
experimental: { ...config.experimental },
|
|
1373
1420
|
summarize: { ...config.summarize },
|
|
1374
|
-
autoPrune: { ...config.autoPrune },
|
|
1421
|
+
autoPrune: { ...config.autoPrune, signals: { ...config.autoPrune.signals } },
|
|
1375
1422
|
tool: { ...config.tool }
|
|
1376
1423
|
};
|
|
1377
1424
|
}
|
|
@@ -1438,27 +1485,231 @@ Using previous/default values`
|
|
|
1438
1485
|
return config;
|
|
1439
1486
|
}
|
|
1440
1487
|
|
|
1441
|
-
// lib/session-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1488
|
+
// lib/session-boundary.ts
|
|
1489
|
+
var BOUNDARY_QUIET_MS = 2e3;
|
|
1490
|
+
var PROBE_TIMEOUT_MS = 2e3;
|
|
1491
|
+
var BUSY_EVIDENCE_TTL_MS = 10 * 6e4;
|
|
1492
|
+
var MAX_TRACKED_SESSIONS = 500;
|
|
1493
|
+
var SessionBoundaryTracker = class {
|
|
1494
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1495
|
+
listeners = [];
|
|
1496
|
+
probeBusy;
|
|
1497
|
+
logger;
|
|
1498
|
+
now;
|
|
1499
|
+
setTimer;
|
|
1500
|
+
clearTimer;
|
|
1501
|
+
constructor(deps) {
|
|
1502
|
+
this.probeBusy = deps.probeBusy;
|
|
1503
|
+
this.logger = deps.logger;
|
|
1504
|
+
this.now = deps.now ?? Date.now;
|
|
1505
|
+
this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
1506
|
+
this.clearTimer = deps.clearTimer ?? ((t) => clearTimeout(t));
|
|
1507
|
+
}
|
|
1508
|
+
/** Register an at-rest listener; listeners fire in registration order
|
|
1509
|
+
* (1. deferred drain, 2. heuristic auto prune) and are individually
|
|
1510
|
+
* error-isolated: a throw or rejection is warn-logged and never blocks
|
|
1511
|
+
* later listeners nor escapes into the timer callback. */
|
|
1512
|
+
onAtRest(listener) {
|
|
1513
|
+
this.listeners.push(listener);
|
|
1514
|
+
}
|
|
1515
|
+
/** Primary input: `session.status` event payload status type. */
|
|
1516
|
+
observeStatus(sessionID, statusType) {
|
|
1517
|
+
if (!sessionID) return;
|
|
1518
|
+
const entry = this.entry(sessionID);
|
|
1519
|
+
entry.sawStatus = true;
|
|
1520
|
+
if (statusType === "busy" || statusType === "retry") {
|
|
1521
|
+
this.transition(
|
|
1522
|
+
sessionID,
|
|
1523
|
+
entry,
|
|
1524
|
+
"busy",
|
|
1525
|
+
statusType === "retry" ? "retry-observed" : "busy-observed"
|
|
1526
|
+
);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (statusType === "idle") {
|
|
1530
|
+
this.observeIdle(sessionID, entry, "status");
|
|
1451
1531
|
}
|
|
1452
1532
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1533
|
+
/** First-class input: legacy `session.idle`. Absorbed by dedup on hosts
|
|
1534
|
+
* that dual-publish; the only boundary signal on status-less hosts (T3). */
|
|
1535
|
+
observeLegacyIdle(sessionID) {
|
|
1536
|
+
if (!sessionID) return;
|
|
1537
|
+
this.observeIdle(sessionID, this.entry(sessionID), "legacy-idle");
|
|
1538
|
+
}
|
|
1539
|
+
/** Lifecycle: cancel any pending window and forget the session. */
|
|
1540
|
+
observeDeleted(sessionID) {
|
|
1541
|
+
if (!sessionID) return;
|
|
1542
|
+
const entry = this.sessions.get(sessionID);
|
|
1543
|
+
if (!entry) return;
|
|
1544
|
+
this.cancelWindow(entry);
|
|
1545
|
+
this.sessions.delete(sessionID);
|
|
1546
|
+
this.logger.debug("Boundary session dropped", { sessionId: sessionID, reason: "deleted" });
|
|
1547
|
+
}
|
|
1548
|
+
/** Lifecycle: compaction ended; reset to unknown so the next idle opens a
|
|
1549
|
+
* fresh window. */
|
|
1550
|
+
observeCompacted(sessionID) {
|
|
1551
|
+
if (!sessionID) return;
|
|
1552
|
+
const entry = this.sessions.get(sessionID);
|
|
1553
|
+
if (!entry) return;
|
|
1554
|
+
const from = entry.phase;
|
|
1555
|
+
this.cancelWindow(entry);
|
|
1556
|
+
entry.phase = "unknown";
|
|
1557
|
+
delete entry.busyAt;
|
|
1558
|
+
this.logger.debug("Boundary transition", {
|
|
1559
|
+
sessionId: sessionID,
|
|
1560
|
+
from,
|
|
1561
|
+
to: "unknown",
|
|
1562
|
+
reason: "compacted"
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
/** Absorbed busy-evidence cache: `busy` decays to `unknown` after the TTL;
|
|
1566
|
+
* the table is LRU-bounded (pending-rest/at-rest entries have no TTL exit
|
|
1567
|
+
* and rely on the cap). */
|
|
1568
|
+
state(sessionID) {
|
|
1569
|
+
const entry = this.sessions.get(sessionID);
|
|
1570
|
+
if (!entry) return "unknown";
|
|
1571
|
+
if (entry.phase === "busy" && entry.busyAt !== void 0 && this.now() - entry.busyAt > BUSY_EVIDENCE_TTL_MS) {
|
|
1572
|
+
return "unknown";
|
|
1573
|
+
}
|
|
1574
|
+
return entry.phase;
|
|
1575
|
+
}
|
|
1576
|
+
dispose(sessionID) {
|
|
1577
|
+
this.observeDeleted(sessionID);
|
|
1578
|
+
}
|
|
1579
|
+
observeIdle(sessionID, entry, source) {
|
|
1580
|
+
if (entry.phase === "pending-rest") {
|
|
1581
|
+
this.logger.debug("Boundary duplicate idle ignored", { sessionId: sessionID, source });
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (entry.phase === "at-rest") {
|
|
1585
|
+
if (source === "legacy-idle" && !entry.sawStatus) {
|
|
1586
|
+
this.armWindow(sessionID, entry, "degrade-rearm");
|
|
1587
|
+
} else {
|
|
1588
|
+
this.logger.debug("Boundary duplicate idle ignored", {
|
|
1589
|
+
sessionId: sessionID,
|
|
1590
|
+
source
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
this.armWindow(sessionID, entry, "idle-observed");
|
|
1596
|
+
}
|
|
1597
|
+
armWindow(sessionID, entry, reason) {
|
|
1598
|
+
this.cancelWindow(entry);
|
|
1599
|
+
entry.phase = "pending-rest";
|
|
1600
|
+
entry.timer = this.setTimer(() => {
|
|
1601
|
+
entry.timer = void 0;
|
|
1602
|
+
void this.onWindowExpiry(sessionID, entry);
|
|
1603
|
+
}, BOUNDARY_QUIET_MS);
|
|
1604
|
+
this.refresh(sessionID, entry);
|
|
1605
|
+
this.logger.debug("Boundary quiet window armed", {
|
|
1606
|
+
sessionId: sessionID,
|
|
1607
|
+
reason,
|
|
1608
|
+
generation: entry.generation
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
async onWindowExpiry(sessionID, entry) {
|
|
1612
|
+
if (entry.phase !== "pending-rest") return;
|
|
1613
|
+
const generation = entry.generation;
|
|
1614
|
+
let busy;
|
|
1615
|
+
try {
|
|
1616
|
+
busy = await this.probeBusy(sessionID);
|
|
1617
|
+
} catch {
|
|
1618
|
+
busy = null;
|
|
1619
|
+
}
|
|
1620
|
+
if (entry.phase !== "pending-rest" || entry.generation !== generation || this.sessions.get(sessionID) !== entry) {
|
|
1621
|
+
this.logger.debug("Boundary probe result discarded", {
|
|
1622
|
+
sessionId: sessionID,
|
|
1623
|
+
generation
|
|
1624
|
+
});
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (busy === true) {
|
|
1628
|
+
this.transition(sessionID, entry, "busy", "probe-busy");
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const reason = busy === false ? "window-expired" : "probe-failopen";
|
|
1632
|
+
const from = entry.phase;
|
|
1633
|
+
entry.phase = "at-rest";
|
|
1634
|
+
this.refresh(sessionID, entry);
|
|
1635
|
+
this.logger.debug("Boundary transition", {
|
|
1636
|
+
sessionId: sessionID,
|
|
1637
|
+
from,
|
|
1638
|
+
to: "at-rest",
|
|
1639
|
+
reason
|
|
1640
|
+
});
|
|
1641
|
+
this.fireAtRest(sessionID, reason);
|
|
1642
|
+
}
|
|
1643
|
+
transition(sessionID, entry, phase, reason) {
|
|
1644
|
+
const from = entry.phase;
|
|
1645
|
+
this.cancelWindow(entry);
|
|
1646
|
+
entry.phase = phase;
|
|
1647
|
+
if (phase === "busy") entry.busyAt = this.now();
|
|
1648
|
+
else delete entry.busyAt;
|
|
1649
|
+
this.refresh(sessionID, entry);
|
|
1650
|
+
this.logger.debug("Boundary transition", { sessionId: sessionID, from, to: phase, reason });
|
|
1651
|
+
}
|
|
1652
|
+
cancelWindow(entry) {
|
|
1653
|
+
if (entry.timer !== void 0) {
|
|
1654
|
+
this.clearTimer(entry.timer);
|
|
1655
|
+
entry.timer = void 0;
|
|
1656
|
+
}
|
|
1657
|
+
entry.generation += 1;
|
|
1658
|
+
}
|
|
1659
|
+
fireAtRest(sessionID, reason) {
|
|
1660
|
+
for (const listener of this.listeners) {
|
|
1661
|
+
try {
|
|
1662
|
+
const result = listener(sessionID, reason);
|
|
1663
|
+
if (result instanceof Promise) {
|
|
1664
|
+
result.catch((error) => {
|
|
1665
|
+
this.logger.warn("At-rest listener rejected", {
|
|
1666
|
+
sessionId: sessionID,
|
|
1667
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1668
|
+
});
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
this.logger.warn("At-rest listener failed", {
|
|
1673
|
+
sessionId: sessionID,
|
|
1674
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1461
1678
|
}
|
|
1679
|
+
entry(sessionID) {
|
|
1680
|
+
let entry = this.sessions.get(sessionID);
|
|
1681
|
+
if (!entry) {
|
|
1682
|
+
entry = { phase: "unknown", generation: 0, sawStatus: false };
|
|
1683
|
+
this.sessions.set(sessionID, entry);
|
|
1684
|
+
this.evictIfNeeded(sessionID);
|
|
1685
|
+
return entry;
|
|
1686
|
+
}
|
|
1687
|
+
this.refresh(sessionID, entry);
|
|
1688
|
+
return entry;
|
|
1689
|
+
}
|
|
1690
|
+
refresh(sessionID, entry) {
|
|
1691
|
+
this.sessions.delete(sessionID);
|
|
1692
|
+
this.sessions.set(sessionID, entry);
|
|
1693
|
+
this.evictIfNeeded(sessionID);
|
|
1694
|
+
}
|
|
1695
|
+
evictIfNeeded(kept) {
|
|
1696
|
+
if (this.sessions.size <= MAX_TRACKED_SESSIONS) return;
|
|
1697
|
+
const oldest = this.sessions.keys().next().value;
|
|
1698
|
+
if (oldest === void 0 || oldest === kept) return;
|
|
1699
|
+
const victim = this.sessions.get(oldest);
|
|
1700
|
+
if (victim?.timer !== void 0) this.clearTimer(victim.timer);
|
|
1701
|
+
this.sessions.delete(oldest);
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
function retrySeconds(retryAfterMs) {
|
|
1705
|
+
return Math.ceil(retryAfterMs / 1e3);
|
|
1706
|
+
}
|
|
1707
|
+
function eventSessionID(properties) {
|
|
1708
|
+
const direct = properties?.sessionID;
|
|
1709
|
+
if (typeof direct === "string" && direct) return direct;
|
|
1710
|
+
const info = properties?.info?.id;
|
|
1711
|
+
if (typeof info === "string" && info) return info;
|
|
1712
|
+
return void 0;
|
|
1462
1713
|
}
|
|
1463
1714
|
|
|
1464
1715
|
// lib/hooks.ts
|
|
@@ -1498,38 +1749,12 @@ var SIGNAL_LABELS = {
|
|
|
1498
1749
|
"idle-gap": "\u957F\u65F6\u95F4\u4E2D\u65AD\u540E\u6062\u590D"
|
|
1499
1750
|
};
|
|
1500
1751
|
function createEventHandler(deps) {
|
|
1501
|
-
async function triggerAutoPrune(sessionID, signals) {
|
|
1502
|
-
const reason = signals.map((signal) => SIGNAL_LABELS[signal]).join("\u3001");
|
|
1503
|
-
const model = await resolveSessionModel(deps.client, sessionID);
|
|
1504
|
-
if (!model) {
|
|
1505
|
-
deps.logger.debug("Auto prune skipped; no session model yet", { sessionId: sessionID });
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
const result = await deps.summarize.summarize({ sessionID, model });
|
|
1509
|
-
deps.autoPruner.markPruned(sessionID);
|
|
1510
|
-
if (result.status === "succeeded") {
|
|
1511
|
-
await showToast(deps.client, "DCP \u81EA\u52A8\u538B\u7F29", `\u68C0\u6D4B\u5230${reason}\uFF0C\u5DF2\u751F\u6210\u65B0\u7684\u8BED\u4E49\u68C0\u67E5\u70B9\u3002`);
|
|
1512
|
-
} else {
|
|
1513
|
-
await showToast(
|
|
1514
|
-
deps.client,
|
|
1515
|
-
"DCP \u81EA\u52A8\u538B\u7F29",
|
|
1516
|
-
`\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u538B\u7F29\u5931\u8D25\uFF1B\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`,
|
|
1517
|
-
"warning"
|
|
1518
|
-
);
|
|
1519
|
-
}
|
|
1520
|
-
deps.logger.debug("Auto prune finished", { sessionId: sessionID, status: result.status });
|
|
1521
|
-
}
|
|
1522
1752
|
return async (input) => {
|
|
1523
1753
|
const event = input.event;
|
|
1524
|
-
const sessionID = event.properties?.sessionID;
|
|
1525
|
-
if (typeof sessionID !== "string" || !sessionID) return;
|
|
1526
1754
|
try {
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
if (signals) await triggerAutoPrune(sessionID, signals);
|
|
1531
|
-
return;
|
|
1532
|
-
}
|
|
1755
|
+
deps.prune.observeEvent(event.type, event.properties);
|
|
1756
|
+
const sessionID = eventSessionID(event.properties);
|
|
1757
|
+
if (!sessionID) return;
|
|
1533
1758
|
if (event.type === "session.compacted") {
|
|
1534
1759
|
deps.autoPruner.markPruned(sessionID);
|
|
1535
1760
|
return;
|
|
@@ -1540,13 +1765,58 @@ function createEventHandler(deps) {
|
|
|
1540
1765
|
} catch (error) {
|
|
1541
1766
|
deps.logger.warn("Event handler failed", {
|
|
1542
1767
|
type: event.type,
|
|
1543
|
-
sessionId: sessionID,
|
|
1544
1768
|
error: error instanceof Error ? error.message : String(error)
|
|
1545
1769
|
});
|
|
1546
1770
|
}
|
|
1547
1771
|
};
|
|
1548
1772
|
}
|
|
1549
|
-
function
|
|
1773
|
+
async function triggerAutoPrune(deps, sessionID, signals) {
|
|
1774
|
+
const reason = signals.map((signal) => SIGNAL_LABELS[signal]).join("\u3001");
|
|
1775
|
+
const result = await deps.prune.request({ sessionID, onBusy: "proceed" });
|
|
1776
|
+
const attempted = result.status === "succeeded" || result.status === "failed" || result.status === "cooldown";
|
|
1777
|
+
if (!attempted) {
|
|
1778
|
+
if (result.status === "busy") {
|
|
1779
|
+
await showToast(
|
|
1780
|
+
deps.client,
|
|
1781
|
+
"DCP \u81EA\u52A8\u538B\u7F29",
|
|
1782
|
+
`\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u4F1A\u8BDD\u6B63\u5FD9\uFF0C\u672C\u6B21\u5DF2\u8DF3\u8FC7\uFF1B\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`,
|
|
1783
|
+
"warning"
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
deps.logger.debug("Auto prune skipped", {
|
|
1787
|
+
sessionId: sessionID,
|
|
1788
|
+
status: result.status
|
|
1789
|
+
});
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
deps.autoPruner.markPruned(sessionID);
|
|
1793
|
+
if (result.status === "succeeded") {
|
|
1794
|
+
await showToast(deps.client, "DCP \u81EA\u52A8\u538B\u7F29", `\u68C0\u6D4B\u5230${reason}\uFF0C\u5DF2\u751F\u6210\u65B0\u7684\u8BED\u4E49\u68C0\u67E5\u70B9\u3002`);
|
|
1795
|
+
} else if (result.status === "cooldown") {
|
|
1796
|
+
await showToast(
|
|
1797
|
+
deps.client,
|
|
1798
|
+
"DCP \u81EA\u52A8\u538B\u7F29",
|
|
1799
|
+
`\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF1B${retrySeconds(result.retryAfterMs)} \u79D2\u540E\u53EF\u91CD\u8BD5\u3002`,
|
|
1800
|
+
"warning"
|
|
1801
|
+
);
|
|
1802
|
+
} else {
|
|
1803
|
+
await showToast(
|
|
1804
|
+
deps.client,
|
|
1805
|
+
"DCP \u81EA\u52A8\u538B\u7F29",
|
|
1806
|
+
`\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u538B\u7F29\u5931\u8D25\uFF1B\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`,
|
|
1807
|
+
"warning"
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
deps.logger.debug("Auto prune finished", { sessionId: sessionID, status: result.status });
|
|
1811
|
+
}
|
|
1812
|
+
function createAtRestAutoPruneListener(deps) {
|
|
1813
|
+
return async (sessionID, _reason) => {
|
|
1814
|
+
if (!deps.config.enabled) return;
|
|
1815
|
+
const signals = deps.autoPruner.consumePending(sessionID);
|
|
1816
|
+
if (signals) await triggerAutoPrune(deps, sessionID, signals);
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
function createCommandExecuteHandler(client, prune, logger) {
|
|
1550
1820
|
return async (input, _output) => {
|
|
1551
1821
|
if (input.command !== "dcp") return;
|
|
1552
1822
|
const subcommand = (input.arguments ?? "").trim().split(/\s+/, 1)[0]?.toLowerCase();
|
|
@@ -1558,8 +1828,17 @@ function createCommandExecuteHandler(client, summarize, logger) {
|
|
|
1558
1828
|
);
|
|
1559
1829
|
throw new Error("__DCP_HELP_HANDLED__");
|
|
1560
1830
|
}
|
|
1561
|
-
const
|
|
1562
|
-
if (
|
|
1831
|
+
const result = await prune.request({ sessionID: input.sessionID, onBusy: "proceed" });
|
|
1832
|
+
if (result.status === "busy") {
|
|
1833
|
+
await showToast(
|
|
1834
|
+
client,
|
|
1835
|
+
"DCP summarize",
|
|
1836
|
+
"Session is busy; the prune will not interrupt the current turn. Try again once it finishes.",
|
|
1837
|
+
"warning"
|
|
1838
|
+
);
|
|
1839
|
+
throw new Error("__DCP_SUMMARIZE_HANDLED__");
|
|
1840
|
+
}
|
|
1841
|
+
if (result.status === "no-model") {
|
|
1563
1842
|
await showToast(
|
|
1564
1843
|
client,
|
|
1565
1844
|
"DCP summarize",
|
|
@@ -1568,14 +1847,13 @@ function createCommandExecuteHandler(client, summarize, logger) {
|
|
|
1568
1847
|
);
|
|
1569
1848
|
throw new Error("__DCP_SUMMARIZE_NO_MODEL__");
|
|
1570
1849
|
}
|
|
1571
|
-
const result = await summarize.summarize({ sessionID: input.sessionID, model });
|
|
1572
1850
|
if (result.status === "succeeded") {
|
|
1573
1851
|
await showToast(client, "DCP summarize", "Semantic pruning checkpoint created.");
|
|
1574
1852
|
} else if (result.status === "cooldown") {
|
|
1575
1853
|
await showToast(
|
|
1576
1854
|
client,
|
|
1577
1855
|
"DCP summarize",
|
|
1578
|
-
`Previous attempt failed; retry in ${
|
|
1856
|
+
`Previous attempt failed; retry in ${retrySeconds(result.retryAfterMs)}s.`,
|
|
1579
1857
|
"warning"
|
|
1580
1858
|
);
|
|
1581
1859
|
} else {
|
|
@@ -1935,34 +2213,216 @@ var PromptStore = class {
|
|
|
1935
2213
|
|
|
1936
2214
|
// lib/prune-tool.ts
|
|
1937
2215
|
import { tool } from "@opencode-ai/plugin";
|
|
2216
|
+
|
|
2217
|
+
// lib/session-model.ts
|
|
2218
|
+
function latestUserModel(messages) {
|
|
2219
|
+
if (!Array.isArray(messages)) return null;
|
|
2220
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
2221
|
+
const info = messages[index]?.info;
|
|
2222
|
+
if (info?.role !== "user") continue;
|
|
2223
|
+
const providerID = info.model?.providerID;
|
|
2224
|
+
const modelID = info.model?.modelID;
|
|
2225
|
+
if (typeof providerID === "string" && typeof modelID === "string") {
|
|
2226
|
+
return { providerID, modelID };
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
return null;
|
|
2230
|
+
}
|
|
2231
|
+
async function resolveSessionModel(client, sessionID) {
|
|
2232
|
+
try {
|
|
2233
|
+
const response = await client.session.messages({ path: { id: sessionID } });
|
|
2234
|
+
return latestUserModel(response.data ?? response);
|
|
2235
|
+
} catch {
|
|
2236
|
+
return null;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// lib/prune-service.ts
|
|
2241
|
+
var PruneService = class {
|
|
2242
|
+
deferred = /* @__PURE__ */ new Set();
|
|
2243
|
+
client;
|
|
2244
|
+
summarize;
|
|
2245
|
+
logger;
|
|
2246
|
+
probeTimeoutMs;
|
|
2247
|
+
/** THE single busy/idle event state machine (absorbed the former
|
|
2248
|
+
* SessionActivityTracker — no second busy cache may exist). */
|
|
2249
|
+
boundary;
|
|
2250
|
+
constructor(deps) {
|
|
2251
|
+
this.client = deps.client;
|
|
2252
|
+
this.summarize = deps.summarize;
|
|
2253
|
+
this.logger = deps.logger;
|
|
2254
|
+
this.probeTimeoutMs = deps.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
|
|
2255
|
+
this.boundary = new SessionBoundaryTracker({
|
|
2256
|
+
probeBusy: (sessionID) => this.probeBusy(sessionID),
|
|
2257
|
+
logger: deps.logger,
|
|
2258
|
+
now: deps.now,
|
|
2259
|
+
setTimer: deps.setTimer,
|
|
2260
|
+
clearTimer: deps.clearTimer
|
|
2261
|
+
});
|
|
2262
|
+
this.boundary.onAtRest((sessionID) => {
|
|
2263
|
+
if (!this.deferred.delete(sessionID)) return;
|
|
2264
|
+
void this.drainQueued(sessionID);
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Feed every host event through here. `session.status` and legacy
|
|
2269
|
+
* `session.idle` feed the boundary tracker (quiet window + expiry probe);
|
|
2270
|
+
* the deferral queue drains at the confirmed at-rest classification, and
|
|
2271
|
+
* queued prunes are forgotten once a compaction (or the session itself)
|
|
2272
|
+
* is gone.
|
|
2273
|
+
*/
|
|
2274
|
+
observeEvent(type, properties) {
|
|
2275
|
+
const sessionID = eventSessionID(properties);
|
|
2276
|
+
if (type === "session.status") {
|
|
2277
|
+
this.boundary.observeStatus(sessionID, properties?.status?.type);
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (type === "session.idle") {
|
|
2281
|
+
this.boundary.observeLegacyIdle(sessionID);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
if (!sessionID) return;
|
|
2285
|
+
if (type === "session.compacted") {
|
|
2286
|
+
this.deferred.delete(sessionID);
|
|
2287
|
+
this.boundary.observeCompacted(sessionID);
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
if (type === "session.deleted") {
|
|
2291
|
+
this.deferred.delete(sessionID);
|
|
2292
|
+
this.boundary.observeDeleted(sessionID);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
async request(request) {
|
|
2296
|
+
const beforeModel = await this.gate(request);
|
|
2297
|
+
if (beforeModel) return beforeModel;
|
|
2298
|
+
const model = await resolveSessionModel(this.client, request.sessionID);
|
|
2299
|
+
if (!model) return { status: "no-model" };
|
|
2300
|
+
const afterModel = await this.gate(request);
|
|
2301
|
+
if (afterModel) return afterModel;
|
|
2302
|
+
const serverBusy = await this.probeBusy(request.sessionID);
|
|
2303
|
+
if (serverBusy === true) {
|
|
2304
|
+
const fallback = await this.outcomeFor(request, { action: "stand-down" });
|
|
2305
|
+
if (fallback) return fallback;
|
|
2306
|
+
}
|
|
2307
|
+
const result = await this.summarize.summarize({ sessionID: request.sessionID, model });
|
|
2308
|
+
if (result.status === "rejected") {
|
|
2309
|
+
return { status: "busy" };
|
|
2310
|
+
}
|
|
2311
|
+
return result;
|
|
2312
|
+
}
|
|
2313
|
+
/** One admission check; `null` means the request may proceed. */
|
|
2314
|
+
gate(request) {
|
|
2315
|
+
return this.outcomeFor(request, this.admit(request));
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Executes a queued prune at the confirmed at-rest boundary. The tool
|
|
2319
|
+
* already promised its caller this prune would run; losing the busy race
|
|
2320
|
+
* must not silently break that promise, so a busy outcome re-queues for
|
|
2321
|
+
* the next at-rest boundary. Every other outcome is terminal: it is
|
|
2322
|
+
* logged and never retried — new prune demand arrives through new
|
|
2323
|
+
* triggers only. Execution is re-guarded on every attempt, so re-queueing
|
|
2324
|
+
* never violates the never-interrupt invariant.
|
|
2325
|
+
*/
|
|
2326
|
+
async drainQueued(sessionID) {
|
|
2327
|
+
let outcome;
|
|
2328
|
+
try {
|
|
2329
|
+
outcome = await this.request({ sessionID, onBusy: "proceed" });
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
this.logger.warn("Queued prune drain failed; the prune was not retried", {
|
|
2332
|
+
sessionId: sessionID,
|
|
2333
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2334
|
+
});
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
if (outcome.status === "busy") {
|
|
2338
|
+
this.deferred.add(sessionID);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
this.logger.debug("Queued prune drain finished", {
|
|
2342
|
+
sessionId: sessionID,
|
|
2343
|
+
status: outcome.status
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Turns an admission decision into a terminal outcome, or `null` when the
|
|
2348
|
+
* request may proceed. Deferral additionally enqueues the session.
|
|
2349
|
+
*/
|
|
2350
|
+
async outcomeFor(request, admission) {
|
|
2351
|
+
if (admission.action === "go") return null;
|
|
2352
|
+
if (admission.action === "stand-down") return { status: "busy" };
|
|
2353
|
+
this.deferred.add(request.sessionID);
|
|
2354
|
+
this.logger.debug("Prune deferred to the next session at-rest boundary", {
|
|
2355
|
+
sessionId: request.sessionID
|
|
2356
|
+
});
|
|
2357
|
+
return { status: "deferred" };
|
|
2358
|
+
}
|
|
2359
|
+
admit(request) {
|
|
2360
|
+
if (request.onBusy === "defer") return { action: "defer" };
|
|
2361
|
+
if (this.boundary.state(request.sessionID) === "busy") return { action: "stand-down" };
|
|
2362
|
+
return { action: "go" };
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* THE single live busy probe, reused verbatim by the boundary tracker's
|
|
2366
|
+
* expiry check. Bounded by a finite deadline: a never-returning probe
|
|
2367
|
+
* resolves `null` within the timeout (fail-open). Returns `null` when the
|
|
2368
|
+
* answer is unknown (endpoint or SDK method missing, request failed,
|
|
2369
|
+
* timeout) — callers must fail open in that case.
|
|
2370
|
+
*/
|
|
2371
|
+
async probeBusy(sessionID) {
|
|
2372
|
+
const statusFn = this.client.session.status;
|
|
2373
|
+
if (typeof statusFn !== "function") return null;
|
|
2374
|
+
const query = (async () => {
|
|
2375
|
+
try {
|
|
2376
|
+
return await statusFn.call(
|
|
2377
|
+
this.client.session
|
|
2378
|
+
);
|
|
2379
|
+
} catch {
|
|
2380
|
+
return null;
|
|
2381
|
+
}
|
|
2382
|
+
})();
|
|
2383
|
+
const response = await Promise.race([
|
|
2384
|
+
query,
|
|
2385
|
+
new Promise((resolve) => setTimeout(() => resolve(null), this.probeTimeoutMs))
|
|
2386
|
+
]);
|
|
2387
|
+
const map = response?.data ?? response;
|
|
2388
|
+
const info = map?.[sessionID];
|
|
2389
|
+
return info?.type === "busy" || info?.type === "retry";
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
|
|
2393
|
+
// lib/prune-tool.ts
|
|
1938
2394
|
var PRUNE_TOOL_NAME = "dcp_prune";
|
|
1939
|
-
var PRUNE_TOOL_DESCRIPTION = `\
|
|
2395
|
+
var PRUNE_TOOL_DESCRIPTION = `\u628A\u5F53\u524D\u4F1A\u8BDD\u7684\u65E7\u5BF9\u8BDD\u524D\u7F00\u6298\u53E0\u4E3A\u4E00\u4E2A\u6EDA\u52A8\u68C0\u67E5\u70B9\uFF08\u4FDD\u7559\u7CFB\u7EDF\u7EA7\u89C4\u5219\u3001\u538B\u7F29\u4E2D\u90E8\u5386\u53F2\u3001\u8BE6\u8FF0\u8FDB\u884C\u4E2D\u7684\u4EFB\u52A1\uFF09\uFF0C\u8FD1\u671F\u5C3E\u90E8\u4E0D\u53D7\u5F71\u54CD\u3002
|
|
1940
2396
|
|
|
1941
|
-
\
|
|
2397
|
+
\u4EC5\u5728\u8FD9\u4E9B\u60C5\u51B5\u4E0B\u8C03\u7528\uFF1A
|
|
1942
2398
|
- \u5BF9\u8BDD\u8BDD\u9898\u53D1\u751F\u660E\u663E\u53D8\u66F4\uFF1A\u5F00\u59CB\u5904\u7406\u65B0\u7684\u95EE\u9898\u57DF\u3001\u5207\u6362\u5230\u53E6\u4E00\u4E2A\u6A21\u5757/\u4ED3\u5E93/\u4EFB\u52A1\uFF1B
|
|
1943
|
-
- \
|
|
1944
|
-
- \u5BF9\u8BDD\u8F6E\u6570\u6216\u4E0A\u4E0B\u6587\u660E\u663E\u53D8\u957F\uFF0C\u65E9\u671F\u7EC6\u8282\u5DF2\u4E0D\u9700\u8981\u9010\u5B57\u4FDD\u7559\u3002
|
|
2399
|
+
- \u7528\u6237\u660E\u786E\u8981\u6C42\u538B\u7F29\u4E0A\u4E0B\u6587\u3002
|
|
1945
2400
|
|
|
1946
|
-
\u8C03\u7528\
|
|
2401
|
+
\u540C\u4E00\u4EFB\u52A1\u5185\u7684\u591A\u8F6E\u8FFD\u95EE\u3001\u53C2\u6570\u5FAE\u8C03\u3001\u5EF6\u7EED\u5F53\u524D\u5DE5\u4F5C\uFF0C\u90FD\u4E0D\u8981\u8C03\u7528\u3002\u8C03\u7528\u4E0D\u4F1A\u6253\u65AD\u5F53\u524D\u5DE5\u4F5C\uFF1A\u538B\u7F29\u4F1A\u6392\u961F\uFF0C\u5E76\u5728\u4E0B\u4E00\u4E2A\u786E\u8BA4\u7684\u9759\u606F\u8FB9\u754C\uFF08\u77ED\u6682\u9759\u9ED8\u7A97\u53E3\u52A0\u5B9E\u65F6\u72B6\u6001\u68C0\u67E5\uFF09\u5C1D\u8BD5\u6267\u884C\uFF1B\u5E76\u53D1\u8BF7\u6C42\u81EA\u52A8\u5408\u5E76\uFF0C\u5931\u8D25\u4E0D\u4F1A\u7834\u574F\u73B0\u6709\u4E0A\u4E0B\u6587\u3002\u4E0D\u8981\u4E3A\u540C\u4E00\u8BDD\u9898\u53CD\u590D\u8C03\u7528\u3002`;
|
|
1947
2402
|
function createPruneTool(deps) {
|
|
1948
2403
|
return tool({
|
|
1949
2404
|
description: PRUNE_TOOL_DESCRIPTION,
|
|
1950
2405
|
args: {},
|
|
1951
2406
|
execute: async (_args, context) => {
|
|
1952
2407
|
const sessionID = context.sessionID;
|
|
1953
|
-
const
|
|
1954
|
-
if (!model) {
|
|
1955
|
-
return "DCP\uFF1A\u4F1A\u8BDD\u4E2D\u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u4FE1\u606F\uFF0C\u65E0\u6CD5\u6267\u884C\u538B\u7F29\u3002";
|
|
1956
|
-
}
|
|
1957
|
-
const result = await deps.summarize.summarize({ sessionID, model });
|
|
2408
|
+
const result = await deps.prune.request({ sessionID, onBusy: "defer" });
|
|
1958
2409
|
if (result.status === "succeeded") {
|
|
1959
2410
|
deps.logger.debug("Prune tool triggered native compaction", {
|
|
1960
2411
|
sessionId: sessionID
|
|
1961
2412
|
});
|
|
1962
2413
|
return "DCP\uFF1A\u8BED\u4E49\u538B\u7F29\u5B8C\u6210\uFF0C\u65E7\u4E0A\u4E0B\u6587\u5DF2\u6298\u53E0\u4E3A\u65B0\u68C0\u67E5\u70B9\u3002";
|
|
1963
2414
|
}
|
|
2415
|
+
if (result.status === "deferred") {
|
|
2416
|
+
return "DCP\uFF1A\u4F1A\u8BDD\u4ECD\u5728\u5DE5\u4F5C\u4E2D\uFF0C\u538B\u7F29\u5DF2\u6392\u961F\uFF0C\u5C06\u5728\u4E0B\u4E00\u4E2A\u786E\u8BA4\u7684\u9759\u606F\u8FB9\u754C\u5C1D\u8BD5\u81EA\u52A8\u6267\u884C\uFF1B\u5F53\u524D\u4E0A\u4E0B\u6587\u4E0D\u53D7\u5F71\u54CD\u3002";
|
|
2417
|
+
}
|
|
2418
|
+
if (result.status === "busy") {
|
|
2419
|
+
return "DCP\uFF1A\u4F1A\u8BDD\u6B63\u5FD9\uFF0C\u4E3A\u907F\u514D\u6253\u65AD\u5F53\u524D\u5DE5\u4F5C\uFF0C\u672C\u6B21\u672A\u6267\u884C\u538B\u7F29\u3002";
|
|
2420
|
+
}
|
|
1964
2421
|
if (result.status === "cooldown") {
|
|
1965
|
-
return `DCP\uFF1A\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF0C${
|
|
2422
|
+
return `DCP\uFF1A\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF0C${retrySeconds(result.retryAfterMs)} \u79D2\u540E\u624D\u80FD\u91CD\u8BD5\u3002`;
|
|
2423
|
+
}
|
|
2424
|
+
if (result.status === "no-model") {
|
|
2425
|
+
return "DCP\uFF1A\u4F1A\u8BDD\u4E2D\u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u4FE1\u606F\uFF0C\u65E0\u6CD5\u6267\u884C\u538B\u7F29\u3002";
|
|
1966
2426
|
}
|
|
1967
2427
|
return `DCP\uFF1A\u538B\u7F29\u5931\u8D25\uFF08${result.error}\uFF09\uFF0C\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`;
|
|
1968
2428
|
}
|
|
@@ -1979,6 +2439,16 @@ function errorMessage(error) {
|
|
|
1979
2439
|
return "Unknown native compaction error";
|
|
1980
2440
|
}
|
|
1981
2441
|
}
|
|
2442
|
+
function isBusyRejection(error) {
|
|
2443
|
+
if (error instanceof Error && /\bbusy\b/i.test(error.message)) return true;
|
|
2444
|
+
if (typeof error === "string" && /\bbusy\b/i.test(error)) return true;
|
|
2445
|
+
const structured = error;
|
|
2446
|
+
if (!structured || typeof structured !== "object") return false;
|
|
2447
|
+
if (structured.status === 409 || structured.statusCode === 409 || structured.code === 409) {
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
return typeof structured.name === "string" && /busy/i.test(structured.name);
|
|
2451
|
+
}
|
|
1982
2452
|
var SummarizeCoordinator = class {
|
|
1983
2453
|
constructor(client, logger, options) {
|
|
1984
2454
|
this.client = client;
|
|
@@ -2014,12 +2484,19 @@ var SummarizeCoordinator = class {
|
|
|
2014
2484
|
path: { id: request.sessionID },
|
|
2015
2485
|
body: request.model
|
|
2016
2486
|
});
|
|
2017
|
-
|
|
2018
|
-
|
|
2487
|
+
const nativeError = response?.error;
|
|
2488
|
+
if (nativeError && isBusyRejection(nativeError)) {
|
|
2489
|
+
return { status: "rejected", reason: "busy" };
|
|
2490
|
+
}
|
|
2491
|
+
if (nativeError || response?.data !== true) {
|
|
2492
|
+
throw new Error(errorMessage(nativeError ?? "Native summarize returned false"));
|
|
2019
2493
|
}
|
|
2020
2494
|
this.failedAt.delete(request.sessionID);
|
|
2021
2495
|
return { status: "succeeded" };
|
|
2022
2496
|
} catch (error) {
|
|
2497
|
+
if (isBusyRejection(error)) {
|
|
2498
|
+
return { status: "rejected", reason: "busy" };
|
|
2499
|
+
}
|
|
2023
2500
|
this.failedAt.set(request.sessionID, this.now());
|
|
2024
2501
|
const message = errorMessage(error);
|
|
2025
2502
|
await this.logger.warn("Native summarize failed; context remains unchanged", {
|
|
@@ -2180,7 +2657,19 @@ var server = (async (ctx) => {
|
|
|
2180
2657
|
const summarize = new SummarizeCoordinator(ctx.client, logger, {
|
|
2181
2658
|
failureCooldownMs: config.summarize.failureCooldownMs
|
|
2182
2659
|
});
|
|
2660
|
+
const prune = new PruneService({ client: ctx.client, summarize, logger });
|
|
2183
2661
|
const autoPruner = new AutoPruner(config.autoPrune);
|
|
2662
|
+
if (config.autoPrune.enabled) {
|
|
2663
|
+
prune.boundary.onAtRest(
|
|
2664
|
+
createAtRestAutoPruneListener({
|
|
2665
|
+
client: ctx.client,
|
|
2666
|
+
prune,
|
|
2667
|
+
autoPruner,
|
|
2668
|
+
config: config.autoPrune,
|
|
2669
|
+
logger
|
|
2670
|
+
})
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2184
2673
|
logger.info("DCP initialized", {
|
|
2185
2674
|
commands: config.commands.enabled,
|
|
2186
2675
|
autoPrune: config.autoPrune.enabled,
|
|
@@ -2191,20 +2680,23 @@ var server = (async (ctx) => {
|
|
|
2191
2680
|
return {
|
|
2192
2681
|
"experimental.session.compacting": createSessionCompactingHandler(prompts, logger),
|
|
2193
2682
|
...config.autoPrune.enabled && {
|
|
2194
|
-
"chat.message": createChatMessageHandler(autoPruner)
|
|
2683
|
+
"chat.message": createChatMessageHandler(autoPruner)
|
|
2684
|
+
},
|
|
2685
|
+
// The event feed drives both the boundary classification (status/idle
|
|
2686
|
+
// observations) and the tool's deferred prunes, so it stays registered
|
|
2687
|
+
// whenever either surface is on.
|
|
2688
|
+
...(config.autoPrune.enabled || config.tool.enabled) && {
|
|
2195
2689
|
event: createEventHandler({
|
|
2196
|
-
|
|
2197
|
-
summarize,
|
|
2690
|
+
prune,
|
|
2198
2691
|
autoPruner,
|
|
2199
|
-
config: config.autoPrune,
|
|
2200
2692
|
logger
|
|
2201
2693
|
})
|
|
2202
2694
|
},
|
|
2203
2695
|
...config.tool.enabled && {
|
|
2204
|
-
tool: { [PRUNE_TOOL_NAME]: createPruneTool({
|
|
2696
|
+
tool: { [PRUNE_TOOL_NAME]: createPruneTool({ prune, logger }) }
|
|
2205
2697
|
},
|
|
2206
2698
|
...config.commands.enabled && {
|
|
2207
|
-
"command.execute.before": createCommandExecuteHandler(ctx.client,
|
|
2699
|
+
"command.execute.before": createCommandExecuteHandler(ctx.client, prune, logger),
|
|
2208
2700
|
config: async (opencodeConfig) => {
|
|
2209
2701
|
opencodeConfig.command ??= {};
|
|
2210
2702
|
opencodeConfig.command.dcp = {
|