@lexwdex-org/opencode-dcp 3.4.14 → 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 +492 -236
- 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 +21 -2
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/prune-service.d.ts +34 -29
- package/dist/lib/prune-service.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/package.json +1 -1
- package/dist/lib/activity.d.ts +0 -22
- package/dist/lib/activity.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,42 +1,3 @@
|
|
|
1
|
-
// lib/activity.ts
|
|
2
|
-
var BUSY_TTL_MS = 10 * 6e4;
|
|
3
|
-
var MAX_TRACKED_SESSIONS = 500;
|
|
4
|
-
var SessionActivityTracker = class {
|
|
5
|
-
sessions = /* @__PURE__ */ new Map();
|
|
6
|
-
now;
|
|
7
|
-
constructor(now) {
|
|
8
|
-
this.now = now ?? Date.now;
|
|
9
|
-
}
|
|
10
|
-
observe(type, properties) {
|
|
11
|
-
const sessionID = properties?.sessionID;
|
|
12
|
-
if (typeof sessionID !== "string" || !sessionID) return;
|
|
13
|
-
if (type === "session.status") {
|
|
14
|
-
const status = properties?.status?.type;
|
|
15
|
-
if (status === "busy" || status === "retry") this.set(sessionID, "busy");
|
|
16
|
-
else if (status === "idle") this.set(sessionID, "idle");
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (type === "session.idle") this.set(sessionID, "idle");
|
|
20
|
-
}
|
|
21
|
-
state(sessionID) {
|
|
22
|
-
const activity = this.sessions.get(sessionID);
|
|
23
|
-
if (!activity) return "unknown";
|
|
24
|
-
if (activity.state === "busy" && this.now() - activity.at > BUSY_TTL_MS) return "unknown";
|
|
25
|
-
return activity.state;
|
|
26
|
-
}
|
|
27
|
-
dropSession(sessionID) {
|
|
28
|
-
this.sessions.delete(sessionID);
|
|
29
|
-
}
|
|
30
|
-
set(sessionID, state) {
|
|
31
|
-
if (this.sessions.has(sessionID)) this.sessions.delete(sessionID);
|
|
32
|
-
this.sessions.set(sessionID, { state, at: this.now() });
|
|
33
|
-
if (this.sessions.size > MAX_TRACKED_SESSIONS) {
|
|
34
|
-
const oldest = this.sessions.keys().next().value;
|
|
35
|
-
if (oldest !== void 0) this.sessions.delete(oldest);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
1
|
// lib/auto-prune.ts
|
|
41
2
|
function tokenize(text) {
|
|
42
3
|
const tokens = /* @__PURE__ */ new Set();
|
|
@@ -121,10 +82,11 @@ var AutoPruner = class {
|
|
|
121
82
|
evaluate(state, text, at) {
|
|
122
83
|
if (state.count + 1 < this.config.minMessages) return [];
|
|
123
84
|
const signals = [];
|
|
124
|
-
|
|
85
|
+
const enabled = this.config.signals;
|
|
86
|
+
if (enabled.idleGap && state.count > 0 && at - state.lastAt >= this.config.idleGapMs) {
|
|
125
87
|
signals.push("idle-gap");
|
|
126
88
|
}
|
|
127
|
-
if (state.count >= DRIFT_BASELINE && text) {
|
|
89
|
+
if (enabled.topicDrift && state.count >= DRIFT_BASELINE && text) {
|
|
128
90
|
const current = tokenize(text);
|
|
129
91
|
if (current.size >= MIN_DRIFT_TOKENS) {
|
|
130
92
|
let max = 0;
|
|
@@ -134,7 +96,7 @@ var AutoPruner = class {
|
|
|
134
96
|
if (max < this.config.driftThreshold) signals.push("topic-drift");
|
|
135
97
|
}
|
|
136
98
|
}
|
|
137
|
-
if (state.count + 1 >= this.config.volumeThreshold) signals.push("volume");
|
|
99
|
+
if (enabled.volume && state.count + 1 >= this.config.volumeThreshold) signals.push("volume");
|
|
138
100
|
return signals;
|
|
139
101
|
}
|
|
140
102
|
state(sessionID) {
|
|
@@ -1019,6 +981,7 @@ var ParseErrorCode;
|
|
|
1019
981
|
var DEFAULT_FAILURE_COOLDOWN_MS = 3e4;
|
|
1020
982
|
var DEFAULT_AUTO_PRUNE = {
|
|
1021
983
|
enabled: true,
|
|
984
|
+
signals: { topicDrift: true, volume: false, idleGap: false },
|
|
1022
985
|
minMessages: 8,
|
|
1023
986
|
volumeThreshold: 30,
|
|
1024
987
|
driftThreshold: 0.18,
|
|
@@ -1039,6 +1002,10 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
1039
1002
|
"summarize.failureCooldownMs",
|
|
1040
1003
|
"autoPrune",
|
|
1041
1004
|
"autoPrune.enabled",
|
|
1005
|
+
"autoPrune.signals",
|
|
1006
|
+
"autoPrune.signals.topicDrift",
|
|
1007
|
+
"autoPrune.signals.volume",
|
|
1008
|
+
"autoPrune.signals.idleGap",
|
|
1042
1009
|
"autoPrune.minMessages",
|
|
1043
1010
|
"autoPrune.volumeThreshold",
|
|
1044
1011
|
"autoPrune.driftThreshold",
|
|
@@ -1213,6 +1180,27 @@ function validateConfigTypes(config) {
|
|
|
1213
1180
|
});
|
|
1214
1181
|
}
|
|
1215
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
|
+
}
|
|
1216
1204
|
}
|
|
1217
1205
|
}
|
|
1218
1206
|
const tool2 = config.tool;
|
|
@@ -1233,15 +1221,25 @@ function validateConfigTypes(config) {
|
|
|
1233
1221
|
}
|
|
1234
1222
|
return errors;
|
|
1235
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
|
+
}
|
|
1236
1228
|
function showConfigWarnings(ctx, configPath, configData, isProject) {
|
|
1237
1229
|
const invalidKeys = getInvalidConfigKeys(configData);
|
|
1238
1230
|
const deprecatedKeys = getDeprecatedConfigKeys(configData);
|
|
1239
1231
|
const typeErrors = validateConfigTypes(configData);
|
|
1240
|
-
|
|
1232
|
+
const signalsHint = needsSignalsMigrationHint(configData);
|
|
1233
|
+
if (!signalsHint && invalidKeys.length === 0 && deprecatedKeys.length === 0 && typeErrors.length === 0) {
|
|
1241
1234
|
return;
|
|
1242
1235
|
}
|
|
1243
1236
|
const configType = isProject ? "project config" : "config";
|
|
1244
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
|
+
}
|
|
1245
1243
|
if (deprecatedKeys.length > 0) {
|
|
1246
1244
|
const keyList = deprecatedKeys.slice(0, 3).join(", ");
|
|
1247
1245
|
const suffix = deprecatedKeys.length > 3 ? ` (+${deprecatedKeys.length - 3} more)` : "";
|
|
@@ -1390,8 +1388,15 @@ function mergeAutoPrune(base, override) {
|
|
|
1390
1388
|
return base;
|
|
1391
1389
|
}
|
|
1392
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;
|
|
1393
1397
|
return {
|
|
1394
1398
|
enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled,
|
|
1399
|
+
signals,
|
|
1395
1400
|
minMessages: number("minMessages", 1),
|
|
1396
1401
|
volumeThreshold: number("volumeThreshold", 2),
|
|
1397
1402
|
driftThreshold: number("driftThreshold", 0, 1),
|
|
@@ -1413,7 +1418,7 @@ function deepCloneConfig(config) {
|
|
|
1413
1418
|
commands: { ...config.commands },
|
|
1414
1419
|
experimental: { ...config.experimental },
|
|
1415
1420
|
summarize: { ...config.summarize },
|
|
1416
|
-
autoPrune: { ...config.autoPrune },
|
|
1421
|
+
autoPrune: { ...config.autoPrune, signals: { ...config.autoPrune.signals } },
|
|
1417
1422
|
tool: { ...config.tool }
|
|
1418
1423
|
};
|
|
1419
1424
|
}
|
|
@@ -1480,164 +1485,232 @@ Using previous/default values`
|
|
|
1480
1485
|
return config;
|
|
1481
1486
|
}
|
|
1482
1487
|
|
|
1483
|
-
// lib/session-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
return { providerID, modelID };
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
return null;
|
|
1496
|
-
}
|
|
1497
|
-
async function resolveSessionModel(client, sessionID) {
|
|
1498
|
-
try {
|
|
1499
|
-
const response = await client.session.messages({ path: { id: sessionID } });
|
|
1500
|
-
return latestUserModel(response.data ?? response);
|
|
1501
|
-
} catch {
|
|
1502
|
-
return null;
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
// lib/prune-service.ts
|
|
1507
|
-
function retrySeconds(retryAfterMs) {
|
|
1508
|
-
return Math.ceil(retryAfterMs / 1e3);
|
|
1509
|
-
}
|
|
1510
|
-
function eventSessionID(properties) {
|
|
1511
|
-
const direct = properties?.sessionID;
|
|
1512
|
-
if (typeof direct === "string" && direct) return direct;
|
|
1513
|
-
const info = properties?.info?.id;
|
|
1514
|
-
if (typeof info === "string" && info) return info;
|
|
1515
|
-
return void 0;
|
|
1516
|
-
}
|
|
1517
|
-
var PruneService = class {
|
|
1518
|
-
deferred = /* @__PURE__ */ new Set();
|
|
1519
|
-
client;
|
|
1520
|
-
summarize;
|
|
1521
|
-
activity;
|
|
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;
|
|
1522
1497
|
logger;
|
|
1498
|
+
now;
|
|
1499
|
+
setTimer;
|
|
1500
|
+
clearTimer;
|
|
1523
1501
|
constructor(deps) {
|
|
1524
|
-
this.
|
|
1525
|
-
this.summarize = deps.summarize;
|
|
1526
|
-
this.activity = deps.activity;
|
|
1502
|
+
this.probeBusy = deps.probeBusy;
|
|
1527
1503
|
this.logger = deps.logger;
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
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) {
|
|
1537
1517
|
if (!sessionID) return;
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
+
);
|
|
1541
1527
|
return;
|
|
1542
1528
|
}
|
|
1543
|
-
if (
|
|
1544
|
-
this.
|
|
1545
|
-
return;
|
|
1529
|
+
if (statusType === "idle") {
|
|
1530
|
+
this.observeIdle(sessionID, entry, "status");
|
|
1546
1531
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1532
|
+
}
|
|
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";
|
|
1550
1573
|
}
|
|
1574
|
+
return entry.phase;
|
|
1551
1575
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
if (
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
const serverBusy = await this.isBusyOnServer(request.sessionID);
|
|
1560
|
-
if (serverBusy === true) {
|
|
1561
|
-
const fallback = await this.outcomeFor(request, { action: "stand-down" });
|
|
1562
|
-
if (fallback) return fallback;
|
|
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;
|
|
1563
1583
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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;
|
|
1567
1594
|
}
|
|
1568
|
-
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
+
});
|
|
1573
1610
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
* boundary. Every other outcome is terminal: it is logged and never
|
|
1579
|
-
* retried — new prune demand arrives through new triggers only.
|
|
1580
|
-
* Execution is re-guarded on every attempt, so re-queueing never violates
|
|
1581
|
-
* the never-interrupt invariant.
|
|
1582
|
-
*/
|
|
1583
|
-
async drainQueued(sessionID) {
|
|
1584
|
-
let outcome;
|
|
1611
|
+
async onWindowExpiry(sessionID, entry) {
|
|
1612
|
+
if (entry.phase !== "pending-rest") return;
|
|
1613
|
+
const generation = entry.generation;
|
|
1614
|
+
let busy;
|
|
1585
1615
|
try {
|
|
1586
|
-
|
|
1587
|
-
} catch
|
|
1588
|
-
|
|
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", {
|
|
1589
1622
|
sessionId: sessionID,
|
|
1590
|
-
|
|
1623
|
+
generation
|
|
1591
1624
|
});
|
|
1592
1625
|
return;
|
|
1593
1626
|
}
|
|
1594
|
-
if (
|
|
1595
|
-
this.
|
|
1627
|
+
if (busy === true) {
|
|
1628
|
+
this.transition(sessionID, entry, "busy", "probe-busy");
|
|
1596
1629
|
return;
|
|
1597
1630
|
}
|
|
1598
|
-
|
|
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", {
|
|
1599
1636
|
sessionId: sessionID,
|
|
1600
|
-
|
|
1637
|
+
from,
|
|
1638
|
+
to: "at-rest",
|
|
1639
|
+
reason
|
|
1601
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
|
+
}
|
|
1602
1678
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
this.
|
|
1612
|
-
|
|
1613
|
-
});
|
|
1614
|
-
return { status: "deferred" };
|
|
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;
|
|
1615
1689
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1690
|
+
refresh(sessionID, entry) {
|
|
1691
|
+
this.sessions.delete(sessionID);
|
|
1692
|
+
this.sessions.set(sessionID, entry);
|
|
1693
|
+
this.evictIfNeeded(sessionID);
|
|
1620
1694
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
const statusFn = this.client.session.status;
|
|
1629
|
-
if (typeof statusFn !== "function") return null;
|
|
1630
|
-
const response = await statusFn.call(
|
|
1631
|
-
this.client.session
|
|
1632
|
-
);
|
|
1633
|
-
const map = response?.data ?? response;
|
|
1634
|
-
const info = map?.[sessionID];
|
|
1635
|
-
return info?.type === "busy" || info?.type === "retry";
|
|
1636
|
-
} catch {
|
|
1637
|
-
return null;
|
|
1638
|
-
}
|
|
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);
|
|
1639
1702
|
}
|
|
1640
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;
|
|
1713
|
+
}
|
|
1641
1714
|
|
|
1642
1715
|
// lib/hooks.ts
|
|
1643
1716
|
function createSessionCompactingHandler(prompts, logger) {
|
|
@@ -1676,57 +1749,12 @@ var SIGNAL_LABELS = {
|
|
|
1676
1749
|
"idle-gap": "\u957F\u65F6\u95F4\u4E2D\u65AD\u540E\u6062\u590D"
|
|
1677
1750
|
};
|
|
1678
1751
|
function createEventHandler(deps) {
|
|
1679
|
-
async function triggerAutoPrune(sessionID, signals) {
|
|
1680
|
-
const reason = signals.map((signal) => SIGNAL_LABELS[signal]).join("\u3001");
|
|
1681
|
-
const result = await deps.prune.request({ sessionID, onBusy: "proceed" });
|
|
1682
|
-
const attempted = result.status === "succeeded" || result.status === "failed" || result.status === "cooldown";
|
|
1683
|
-
if (!attempted) {
|
|
1684
|
-
if (result.status === "busy") {
|
|
1685
|
-
await showToast(
|
|
1686
|
-
deps.client,
|
|
1687
|
-
"DCP \u81EA\u52A8\u538B\u7F29",
|
|
1688
|
-
`\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`,
|
|
1689
|
-
"warning"
|
|
1690
|
-
);
|
|
1691
|
-
}
|
|
1692
|
-
deps.logger.debug("Auto prune skipped", {
|
|
1693
|
-
sessionId: sessionID,
|
|
1694
|
-
status: result.status
|
|
1695
|
-
});
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
deps.autoPruner.markPruned(sessionID);
|
|
1699
|
-
if (result.status === "succeeded") {
|
|
1700
|
-
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`);
|
|
1701
|
-
} else if (result.status === "cooldown") {
|
|
1702
|
-
await showToast(
|
|
1703
|
-
deps.client,
|
|
1704
|
-
"DCP \u81EA\u52A8\u538B\u7F29",
|
|
1705
|
-
`\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF1B${retrySeconds(result.retryAfterMs)} \u79D2\u540E\u53EF\u91CD\u8BD5\u3002`,
|
|
1706
|
-
"warning"
|
|
1707
|
-
);
|
|
1708
|
-
} else {
|
|
1709
|
-
await showToast(
|
|
1710
|
-
deps.client,
|
|
1711
|
-
"DCP \u81EA\u52A8\u538B\u7F29",
|
|
1712
|
-
`\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u538B\u7F29\u5931\u8D25\uFF1B\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`,
|
|
1713
|
-
"warning"
|
|
1714
|
-
);
|
|
1715
|
-
}
|
|
1716
|
-
deps.logger.debug("Auto prune finished", { sessionId: sessionID, status: result.status });
|
|
1717
|
-
}
|
|
1718
1752
|
return async (input) => {
|
|
1719
1753
|
const event = input.event;
|
|
1720
|
-
const sessionID = eventSessionID(event.properties);
|
|
1721
|
-
if (!sessionID) return;
|
|
1722
1754
|
try {
|
|
1723
1755
|
deps.prune.observeEvent(event.type, event.properties);
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
const signals = deps.autoPruner.consumePending(sessionID);
|
|
1727
|
-
if (signals) await triggerAutoPrune(sessionID, signals);
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1756
|
+
const sessionID = eventSessionID(event.properties);
|
|
1757
|
+
if (!sessionID) return;
|
|
1730
1758
|
if (event.type === "session.compacted") {
|
|
1731
1759
|
deps.autoPruner.markPruned(sessionID);
|
|
1732
1760
|
return;
|
|
@@ -1737,12 +1765,57 @@ function createEventHandler(deps) {
|
|
|
1737
1765
|
} catch (error) {
|
|
1738
1766
|
deps.logger.warn("Event handler failed", {
|
|
1739
1767
|
type: event.type,
|
|
1740
|
-
sessionId: sessionID,
|
|
1741
1768
|
error: error instanceof Error ? error.message : String(error)
|
|
1742
1769
|
});
|
|
1743
1770
|
}
|
|
1744
1771
|
};
|
|
1745
1772
|
}
|
|
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
|
+
}
|
|
1746
1819
|
function createCommandExecuteHandler(client, prune, logger) {
|
|
1747
1820
|
return async (input, _output) => {
|
|
1748
1821
|
if (input.command !== "dcp") return;
|
|
@@ -2140,6 +2213,184 @@ var PromptStore = class {
|
|
|
2140
2213
|
|
|
2141
2214
|
// lib/prune-tool.ts
|
|
2142
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
|
|
2143
2394
|
var PRUNE_TOOL_NAME = "dcp_prune";
|
|
2144
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
|
|
2145
2396
|
|
|
@@ -2147,7 +2398,7 @@ var PRUNE_TOOL_DESCRIPTION = `\u628A\u5F53\u524D\u4F1A\u8BDD\u7684\u65E7\u5BF9\u
|
|
|
2147
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
|
|
2148
2399
|
- \u7528\u6237\u660E\u786E\u8981\u6C42\u538B\u7F29\u4E0A\u4E0B\u6587\u3002
|
|
2149
2400
|
|
|
2150
|
-
\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\
|
|
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`;
|
|
2151
2402
|
function createPruneTool(deps) {
|
|
2152
2403
|
return tool({
|
|
2153
2404
|
description: PRUNE_TOOL_DESCRIPTION,
|
|
@@ -2162,7 +2413,7 @@ function createPruneTool(deps) {
|
|
|
2162
2413
|
return "DCP\uFF1A\u8BED\u4E49\u538B\u7F29\u5B8C\u6210\uFF0C\u65E7\u4E0A\u4E0B\u6587\u5DF2\u6298\u53E0\u4E3A\u65B0\u68C0\u67E5\u70B9\u3002";
|
|
2163
2414
|
}
|
|
2164
2415
|
if (result.status === "deferred") {
|
|
2165
|
-
return "DCP\uFF1A\u4F1A\u8BDD\u4ECD\u5728\u5DE5\u4F5C\u4E2D\uFF0C\u538B\u7F29\u5DF2\u6392\u961F\uFF0C\u5C06\u5728\u4E0B\u4E00\u4E2A\
|
|
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";
|
|
2166
2417
|
}
|
|
2167
2418
|
if (result.status === "busy") {
|
|
2168
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";
|
|
@@ -2406,13 +2657,19 @@ var server = (async (ctx) => {
|
|
|
2406
2657
|
const summarize = new SummarizeCoordinator(ctx.client, logger, {
|
|
2407
2658
|
failureCooldownMs: config.summarize.failureCooldownMs
|
|
2408
2659
|
});
|
|
2409
|
-
const prune = new PruneService({
|
|
2410
|
-
client: ctx.client,
|
|
2411
|
-
summarize,
|
|
2412
|
-
activity: new SessionActivityTracker(),
|
|
2413
|
-
logger
|
|
2414
|
-
});
|
|
2660
|
+
const prune = new PruneService({ client: ctx.client, summarize, logger });
|
|
2415
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
|
+
}
|
|
2416
2673
|
logger.info("DCP initialized", {
|
|
2417
2674
|
commands: config.commands.enabled,
|
|
2418
2675
|
autoPrune: config.autoPrune.enabled,
|
|
@@ -2425,14 +2682,13 @@ var server = (async (ctx) => {
|
|
|
2425
2682
|
...config.autoPrune.enabled && {
|
|
2426
2683
|
"chat.message": createChatMessageHandler(autoPruner)
|
|
2427
2684
|
},
|
|
2428
|
-
// The event feed drives both
|
|
2429
|
-
//
|
|
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.
|
|
2430
2688
|
...(config.autoPrune.enabled || config.tool.enabled) && {
|
|
2431
2689
|
event: createEventHandler({
|
|
2432
|
-
client: ctx.client,
|
|
2433
2690
|
prune,
|
|
2434
2691
|
autoPruner,
|
|
2435
|
-
config: config.autoPrune,
|
|
2436
2692
|
logger
|
|
2437
2693
|
})
|
|
2438
2694
|
},
|