dsh-deeppilot 0.9.14 → 0.9.16
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 +21 -6
- package/README.zh-CN.md +13 -4
- package/bin/SHA256SUMS +6 -6
- package/bin/darwin-amd64/dsh-deeppilot-tunnel +0 -0
- package/bin/darwin-arm64/dsh-deeppilot-tunnel +0 -0
- package/bin/linux-amd64/dsh-deeppilot-tunnel +0 -0
- package/bin/linux-arm64/dsh-deeppilot-tunnel +0 -0
- package/bin/windows-amd64/dsh-deeppilot-tunnel.exe +0 -0
- package/bin/windows-arm64/dsh-deeppilot-tunnel.exe +0 -0
- package/docs/DSH_RELEASE_MEMORY.md +33 -0
- package/docs/NPM_PUBLISHING.md +36 -13
- package/lib/client.js +494 -447
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +89 -1
- package/lib/index.js +563 -26
- package/lib/index.js.map +1 -1
- package/package.json +10 -2
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomB
|
|
|
3
3
|
import { access, appendFile, chmod, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { WebSocketServer } from "ws";
|
|
6
|
+
import { deflateRawSync } from "node:zlib";
|
|
6
7
|
import { homedir, networkInterfaces } from "node:os";
|
|
7
8
|
import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
8
9
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -292,6 +293,23 @@ function sanitizeImageName(value) {
|
|
|
292
293
|
function sanitizeDocumentField(value, maxChars) {
|
|
293
294
|
return value.replace(/[\u0000-\u001F\u007F]/g, " ").trim().slice(0, maxChars);
|
|
294
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* 解析关注集合:字符串数组、去重、去空、超长 ID 丢弃,最多取前 16 个。
|
|
298
|
+
* 不合法时返回 undefined。auth.prove 上宁可退回全量广播也不因优化字段拒绝认证;
|
|
299
|
+
* c2s.session.watch 由行校验先拒绝非法载荷。
|
|
300
|
+
*/
|
|
301
|
+
function parseWatchSessions(value) {
|
|
302
|
+
if (!Array.isArray(value)) return void 0;
|
|
303
|
+
const ids = /* @__PURE__ */ new Set();
|
|
304
|
+
for (const item of value) {
|
|
305
|
+
if (typeof item !== "string") continue;
|
|
306
|
+
const id = item.trim();
|
|
307
|
+
if (id.length === 0 || id.length > 256) continue;
|
|
308
|
+
ids.add(id);
|
|
309
|
+
if (ids.size >= 16) break;
|
|
310
|
+
}
|
|
311
|
+
return [...ids];
|
|
312
|
+
}
|
|
295
313
|
/** 二进制模式下导出与附件的单块字节数。连接在发送缓冲降到 256 KiB 以下才发
|
|
296
314
|
* 下一块,缓冲峰值约 512 KiB,远低于 4 MiB 的慢客户端断开线。 */
|
|
297
315
|
const BINARY_CHUNK_BYTES = 262144;
|
|
@@ -305,6 +323,13 @@ function encodeBinaryFrame(header, body) {
|
|
|
305
323
|
frame.set(body, 5 + headerBytes.length);
|
|
306
324
|
return frame;
|
|
307
325
|
}
|
|
326
|
+
function encodeCompressedFrame(json) {
|
|
327
|
+
const deflated = deflateRawSync(Buffer.from(json, "utf8"), { level: 1 });
|
|
328
|
+
const frame = Buffer.allocUnsafe(1 + deflated.length);
|
|
329
|
+
frame.writeUInt8(2, 0);
|
|
330
|
+
deflated.copy(frame, 1);
|
|
331
|
+
return frame;
|
|
332
|
+
}
|
|
308
333
|
// src/auth-rate-limit.ts
|
|
309
334
|
const DEFAULT_AUTH_RATE_POLICY = {
|
|
310
335
|
windowMs: 6e4,
|
|
@@ -485,7 +510,7 @@ function defaultLevel(kind) {
|
|
|
485
510
|
function sanitizeText(value, maxChars) {
|
|
486
511
|
return value.replace(/[\u0000-\u001f\u007f-\u009f]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, maxChars);
|
|
487
512
|
}
|
|
488
|
-
function normalizeReason(reason) {
|
|
513
|
+
function normalizeReason$1(reason) {
|
|
489
514
|
return sanitizeText(reason, MAX_REASON_CHARS).replace(/\s+/g, "-");
|
|
490
515
|
}
|
|
491
516
|
/**
|
|
@@ -566,7 +591,7 @@ var DiagnosticsStore = class {
|
|
|
566
591
|
const event = {
|
|
567
592
|
ts,
|
|
568
593
|
kind,
|
|
569
|
-
reason: normalizeReason(reason),
|
|
594
|
+
reason: normalizeReason$1(reason),
|
|
570
595
|
level: extra.level ?? defaultLevel(kind),
|
|
571
596
|
...attemptId !== void 0 ? { attemptId } : {},
|
|
572
597
|
...source !== void 0 ? { source } : {},
|
|
@@ -1991,6 +2016,12 @@ function userMessageSource(data) {
|
|
|
1991
2016
|
if (obj.source && typeof obj.source === "object") return obj.source;
|
|
1992
2017
|
if (obj.message && typeof obj.message === "object" && obj.message.source && typeof obj.message.source === "object") return obj.message.source;
|
|
1993
2018
|
}
|
|
2019
|
+
/** Source kind of one user/message payload (`user`, `schedule`, injected
|
|
2020
|
+
* context kinds, …); undefined on legacy hosts that record no source. */
|
|
2021
|
+
function userMessageSourceKind(data) {
|
|
2022
|
+
const kind = userMessageSource(data)?.kind;
|
|
2023
|
+
return typeof kind === "string" ? kind : void 0;
|
|
2024
|
+
}
|
|
1994
2025
|
/** Wire role for one user/message payload. A payload without any readable
|
|
1995
2026
|
* source degrades to 'user' so history written by older hosts stays visible;
|
|
1996
2027
|
* a present source follows the host's own trajectory rule — anything whose
|
|
@@ -2223,7 +2254,12 @@ function withoutEditBodies(edit) {
|
|
|
2223
2254
|
function truncate(text, max) {
|
|
2224
2255
|
return text.length <= max ? text : text.slice(0, max - 1) + "…";
|
|
2225
2256
|
}
|
|
2226
|
-
|
|
2257
|
+
/**
|
|
2258
|
+
* `touched`(可选,增量 tail 用):记录每一行最后一次被哪个事件改动,键为行的 seq。
|
|
2259
|
+
* 行由自身事件生成时等于它的 seq;工具结果合并回较早的工具调用行时更新为结果事件的
|
|
2260
|
+
* seq。增量 tail 据此找出「sinceSeq 之后改动过的行」,而不只是 seq 更大的行。
|
|
2261
|
+
*/
|
|
2262
|
+
function projectHistory(events, touched) {
|
|
2227
2263
|
const messages = [];
|
|
2228
2264
|
const toolByCall = /* @__PURE__ */ new Map();
|
|
2229
2265
|
for (const entry of events) {
|
|
@@ -2232,6 +2268,7 @@ function projectHistory(events) {
|
|
|
2232
2268
|
seq: event.seq,
|
|
2233
2269
|
ts: tsOf(event)
|
|
2234
2270
|
};
|
|
2271
|
+
touched?.set(event.seq, Math.max(touched.get(event.seq) ?? event.seq, event.seq));
|
|
2235
2272
|
switch (event.type) {
|
|
2236
2273
|
case "system/message": break;
|
|
2237
2274
|
case "user/message":
|
|
@@ -2285,6 +2322,7 @@ function projectHistory(events) {
|
|
|
2285
2322
|
summary
|
|
2286
2323
|
};
|
|
2287
2324
|
if (attachments.attachments) Object.assign(target, attachments);
|
|
2325
|
+
touched?.set(target.seq, Math.max(touched.get(target.seq) ?? target.seq, event.seq));
|
|
2288
2326
|
} else messages.push({
|
|
2289
2327
|
...base,
|
|
2290
2328
|
role: "tool",
|
|
@@ -2605,12 +2643,13 @@ const sessionRows = [
|
|
|
2605
2643
|
const invalid = requireSessionId(checked.value);
|
|
2606
2644
|
if (invalid) return invalid;
|
|
2607
2645
|
if (!isOptionalField(checked.value, "tailCount", (v) => isInteger(v, 1, 1e4))) return reject$1("E_PROTOCOL", "invalid tailCount");
|
|
2646
|
+
if (!isOptionalField(checked.value, "sinceSeq", (v) => typeof v === "number" && Number.isFinite(v) && v >= 0)) return reject$1("E_PROTOCOL", "invalid sinceSeq");
|
|
2608
2647
|
return checked;
|
|
2609
2648
|
},
|
|
2610
2649
|
handle: async (ctx, payload) => {
|
|
2611
2650
|
const sessionId = payload.sessionId;
|
|
2612
2651
|
const buffered = ctx.bufferOpenEvents(sessionId);
|
|
2613
|
-
if (!await ctx.bridge.openSession(ctx, sessionId, payload.tailCount ?? 100)) {
|
|
2652
|
+
if (!await ctx.bridge.openSession(ctx, sessionId, payload.tailCount ?? 100, payload.sinceSeq)) {
|
|
2614
2653
|
ctx.discardOpenBuffer(sessionId, buffered);
|
|
2615
2654
|
return ctx.fail("E_NOT_FOUND", "session history unavailable");
|
|
2616
2655
|
}
|
|
@@ -2632,6 +2671,24 @@ const sessionRows = [
|
|
|
2632
2671
|
ctx.send("s2c.ack", {});
|
|
2633
2672
|
}
|
|
2634
2673
|
},
|
|
2674
|
+
{
|
|
2675
|
+
type: "c2s.session.watch",
|
|
2676
|
+
stage: "authenticated",
|
|
2677
|
+
scopes: READ,
|
|
2678
|
+
capability: "sessionInterest",
|
|
2679
|
+
doc: "PROTOCOL.md c2s.session.watch",
|
|
2680
|
+
validate: (payload) => {
|
|
2681
|
+
const checked = payloadObject(payload);
|
|
2682
|
+
if (!checked.ok) return checked;
|
|
2683
|
+
const ids = checked.value.sessionIds;
|
|
2684
|
+
if (!Array.isArray(ids) || ids.length > 16 || !ids.every((id) => isText(id, 256))) return reject$1("E_PROTOCOL", `sessionIds must be an array of at most 16 session ids`);
|
|
2685
|
+
return checked;
|
|
2686
|
+
},
|
|
2687
|
+
handle: (ctx, payload) => {
|
|
2688
|
+
ctx.setWatchedSessions(parseWatchSessions(payload.sessionIds) ?? []);
|
|
2689
|
+
ctx.send("s2c.ack", {});
|
|
2690
|
+
}
|
|
2691
|
+
},
|
|
2635
2692
|
{
|
|
2636
2693
|
type: "c2s.session.create",
|
|
2637
2694
|
stage: "authenticated",
|
|
@@ -4070,6 +4127,8 @@ var ConnectionGate = class {
|
|
|
4070
4127
|
scopes: this.scopes,
|
|
4071
4128
|
widgetClient: this.widgetClient,
|
|
4072
4129
|
resumeCursor: this.widgetClient ? void 0 : resumeCursor,
|
|
4130
|
+
watchSessions: this.widgetClient ? void 0 : parseWatchSessions(p.watchSessions),
|
|
4131
|
+
acceptsDeflate: Array.isArray(p.acceptEncoding) && p.acceptEncoding.includes("deflate-raw"),
|
|
4073
4132
|
deviceName,
|
|
4074
4133
|
appVersion
|
|
4075
4134
|
});
|
|
@@ -4124,6 +4183,7 @@ var ConnectionGate = class {
|
|
|
4124
4183
|
fail: (code, message) => host.fail(env.id, code, message),
|
|
4125
4184
|
close: (code, reason) => host.close(code, reason),
|
|
4126
4185
|
canReceive: (scope) => host.canReceive(scope),
|
|
4186
|
+
accepts: (type, payload) => host.accepts(type, payload),
|
|
4127
4187
|
push: (type, payload, seq) => host.push(type, payload, seq),
|
|
4128
4188
|
lastCursor: () => host.lastCursor(),
|
|
4129
4189
|
replay: (entries) => host.replay(entries),
|
|
@@ -4133,6 +4193,7 @@ var ConnectionGate = class {
|
|
|
4133
4193
|
discardOpenBuffer: (sessionId, buffer) => host.discardOpenBuffer(sessionId, buffer),
|
|
4134
4194
|
flushOpenBuffer: (sessionId, buffer) => host.flushOpenBuffer(sessionId, buffer),
|
|
4135
4195
|
closeOpenSession: (sessionId) => host.closeOpenSession(sessionId),
|
|
4196
|
+
setWatchedSessions: (sessionIds) => host.setWatchedSessions(sessionIds),
|
|
4136
4197
|
nextLiveActivityGeneration: () => host.nextLiveActivityGeneration(),
|
|
4137
4198
|
get liveActivityGeneration() {
|
|
4138
4199
|
return host.liveActivityGeneration;
|
|
@@ -4169,6 +4230,14 @@ var BridgeConnection = class {
|
|
|
4169
4230
|
*/
|
|
4170
4231
|
openingSessionEvents = /* @__PURE__ */ new Map();
|
|
4171
4232
|
openSessions = /* @__PURE__ */ new Set();
|
|
4233
|
+
/**
|
|
4234
|
+
* 关注集合(能力 `sessionInterest`):客户端本机有缓存、需要保持最新的会话。
|
|
4235
|
+
* undefined = 旧客户端从未声明,照旧收全部会话事件。来源:auth.prove 的
|
|
4236
|
+
* `watchSessions`(须在重放前生效)与之后的 c2s.session.watch(整体替换)。
|
|
4237
|
+
*/
|
|
4238
|
+
watchedSessions;
|
|
4239
|
+
/** 客户端认证时声明接受 deflate-raw(能力 `compressedFrames`);认证前一律文本帧。 */
|
|
4240
|
+
compressDownlink = false;
|
|
4172
4241
|
liveActivityRegistrationGeneration = 0;
|
|
4173
4242
|
pendingLiveActivityId;
|
|
4174
4243
|
lastActivity = Date.now();
|
|
@@ -4224,6 +4293,8 @@ var BridgeConnection = class {
|
|
|
4224
4293
|
fail: (id, code, message) => connection.fail(id, code, message),
|
|
4225
4294
|
close: (code, reason) => connection.close(code, reason),
|
|
4226
4295
|
canReceive: (scope) => connection.canReceive(scope),
|
|
4296
|
+
accepts: (type, payload) => connection.accepts(type, payload),
|
|
4297
|
+
setWatchedSessions: (sessionIds) => connection.setWatchedSessions(sessionIds),
|
|
4227
4298
|
push: (type, payload, seq) => connection.push(type, payload, seq),
|
|
4228
4299
|
lastCursor: () => connection.lastCursor(),
|
|
4229
4300
|
replay: (entries) => connection.replay(entries),
|
|
@@ -4309,7 +4380,23 @@ var BridgeConnection = class {
|
|
|
4309
4380
|
canReceive(scope) {
|
|
4310
4381
|
return this.identity?.scopes.has(scope) ?? false;
|
|
4311
4382
|
}
|
|
4383
|
+
/**
|
|
4384
|
+
* 关注过滤:只管按会话投递的两类帧。正在打开(tail 未发出、事件进缓冲)、
|
|
4385
|
+
* 已打开或在关注集合里的会话放行;未声明关注集合的连接全部放行。
|
|
4386
|
+
*/
|
|
4387
|
+
accepts(type, payload) {
|
|
4388
|
+
if (this.watchedSessions === void 0) return true;
|
|
4389
|
+
if (type !== "s2c.session.event" && type !== "s2c.session.queue") return true;
|
|
4390
|
+
const sessionId = payload?.sessionId;
|
|
4391
|
+
if (typeof sessionId !== "string") return true;
|
|
4392
|
+
return this.openSessions.has(sessionId) || this.openingSessionEvents.has(sessionId) || this.watchedSessions.has(sessionId);
|
|
4393
|
+
}
|
|
4394
|
+
/** c2s.session.watch:整体替换关注集合;也让从未声明的连接切到按关注投递。 */
|
|
4395
|
+
setWatchedSessions(sessionIds) {
|
|
4396
|
+
this.watchedSessions = new Set(sessionIds);
|
|
4397
|
+
}
|
|
4312
4398
|
push(type, payload, seq) {
|
|
4399
|
+
if (!this.accepts(type, payload)) return;
|
|
4313
4400
|
if (type === "s2c.notify") payload = {
|
|
4314
4401
|
...payload,
|
|
4315
4402
|
hostAudience: this.deps.audience
|
|
@@ -4346,8 +4433,11 @@ var BridgeConnection = class {
|
|
|
4346
4433
|
/** 认证成功:门只做安全判定,欢迎与重放归连接(数据面)。 */
|
|
4347
4434
|
onAuthenticated(identity) {
|
|
4348
4435
|
this.identity = identity;
|
|
4436
|
+
if (identity.watchSessions !== void 0) this.watchedSessions = new Set(identity.watchSessions);
|
|
4437
|
+
this.compressDownlink = identity.acceptsDeflate;
|
|
4349
4438
|
const cursor = identity.resumeCursor;
|
|
4350
4439
|
const canResume = cursor !== void 0 && this.deps.bridge.canResumeFrom(cursor);
|
|
4440
|
+
const routes = identity.widgetClient ? void 0 : this.deps.routes?.();
|
|
4351
4441
|
this.send("s2c.welcome", {
|
|
4352
4442
|
protocolVersion: 2,
|
|
4353
4443
|
serverVersion: this.deps.serverVersion,
|
|
@@ -4355,7 +4445,8 @@ var BridgeConnection = class {
|
|
|
4355
4445
|
scopes: [...identity.scopes],
|
|
4356
4446
|
capabilities: this.deps.bridge.capabilities,
|
|
4357
4447
|
cursor: this.deps.bridge.currentCursor(),
|
|
4358
|
-
resumed: canResume
|
|
4448
|
+
resumed: canResume,
|
|
4449
|
+
...routes !== void 0 ? { routes } : {}
|
|
4359
4450
|
});
|
|
4360
4451
|
if (!identity.widgetClient) this.deps.bridge.addSink(this);
|
|
4361
4452
|
if (cursor !== void 0) {
|
|
@@ -4405,7 +4496,16 @@ var BridgeConnection = class {
|
|
|
4405
4496
|
this.close(1013, "client too slow");
|
|
4406
4497
|
return;
|
|
4407
4498
|
}
|
|
4408
|
-
|
|
4499
|
+
if (body !== void 0) {
|
|
4500
|
+
this.ws.send(encodeBinaryFrame(envelope, body));
|
|
4501
|
+
return;
|
|
4502
|
+
}
|
|
4503
|
+
const json = JSON.stringify(envelope);
|
|
4504
|
+
if (this.compressDownlink && Buffer.byteLength(json) > 8192) {
|
|
4505
|
+
this.ws.send(encodeCompressedFrame(json));
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
this.ws.send(json);
|
|
4409
4509
|
}
|
|
4410
4510
|
fail(id, code, message) {
|
|
4411
4511
|
this.send("s2c.error", {
|
|
@@ -5489,7 +5589,9 @@ function pushContent(notification, mode) {
|
|
|
5489
5589
|
* Relay connectivity self-test used by the settings page "测试中继" button.
|
|
5490
5590
|
*
|
|
5491
5591
|
* Two steps mirror what zero-touch enrollment actually does:
|
|
5492
|
-
* 1. health — GET {url}/healthz → is the relay reachable?
|
|
5592
|
+
* 1. health — GET {url}/healthz → is the relay reachable? The
|
|
5593
|
+
* relay's `version` (relay 0.2.0+) is appended to the message for
|
|
5594
|
+
* troubleshooting; it never affects the verdict.
|
|
5493
5595
|
* 2. enroll — POST {url}/v1/enroll → does the shared key grant a token?
|
|
5494
5596
|
*
|
|
5495
5597
|
* The enroll step doubles as a repair path: a token it issues is handed back
|
|
@@ -5499,6 +5601,16 @@ function pushContent(notification, mode) {
|
|
|
5499
5601
|
* Transport is injectable (`fetchImpl`) so tests never touch the network.
|
|
5500
5602
|
*/
|
|
5501
5603
|
const DEFAULT_TIMEOUT_MS = 6e3;
|
|
5604
|
+
/**
|
|
5605
|
+
* semver-shaped characters only, bounded: the value comes from a remote
|
|
5606
|
+
* server and is rendered verbatim in the settings page.
|
|
5607
|
+
*/
|
|
5608
|
+
const RELAY_VERSION_PATTERN = /^[0-9A-Za-z.+-]{1,32}$/;
|
|
5609
|
+
/** Display-only relay version from a /healthz body; undefined for older relays or unsafe values. */
|
|
5610
|
+
function relayVersionOf(body) {
|
|
5611
|
+
const version = body?.version;
|
|
5612
|
+
return typeof version === "string" && RELAY_VERSION_PATTERN.test(version) ? version : void 0;
|
|
5613
|
+
}
|
|
5502
5614
|
async function requestJson(fetchImpl, url, init, timeoutMs) {
|
|
5503
5615
|
const response = await fetchImpl(url, {
|
|
5504
5616
|
...init,
|
|
@@ -5523,13 +5635,16 @@ async function runRelayProbe(options) {
|
|
|
5523
5635
|
const startedAt = Date.now();
|
|
5524
5636
|
const { status, body } = await requestJson(fetchImpl, `${base}/healthz`, { method: "GET" }, timeoutMs);
|
|
5525
5637
|
const latencyMs = Date.now() - startedAt;
|
|
5526
|
-
if (status === 200 && body?.ok === true)
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5638
|
+
if (status === 200 && body?.ok === true) {
|
|
5639
|
+
const version = relayVersionOf(body);
|
|
5640
|
+
const message = version ? `中继服务可达(版本 ${version})` : "中继服务可达";
|
|
5641
|
+
steps.push({
|
|
5642
|
+
id: "health",
|
|
5643
|
+
ok: true,
|
|
5644
|
+
message,
|
|
5645
|
+
latencyMs
|
|
5646
|
+
});
|
|
5647
|
+
} else steps.push({
|
|
5533
5648
|
id: "health",
|
|
5534
5649
|
ok: false,
|
|
5535
5650
|
message: `中继响应异常(HTTP ${status})`,
|
|
@@ -6273,7 +6388,7 @@ var TransportReconciler = class {
|
|
|
6273
6388
|
function createLocalTransport(deps) {
|
|
6274
6389
|
return new TransportReconciler({
|
|
6275
6390
|
keyOf: (config) => ({
|
|
6276
|
-
enabled: config.enabled === true
|
|
6391
|
+
enabled: config.enabled === true,
|
|
6277
6392
|
port: normalizeLocalPort(config.local?.port)
|
|
6278
6393
|
}),
|
|
6279
6394
|
skipWhenDisabled: true,
|
|
@@ -6341,7 +6456,7 @@ function createLocalTransport(deps) {
|
|
|
6341
6456
|
};
|
|
6342
6457
|
}
|
|
6343
6458
|
}, deps.config, deps.log, {
|
|
6344
|
-
phase: deps.config().enabled === true
|
|
6459
|
+
phase: deps.config().enabled === true ? "starting" : "disabled",
|
|
6345
6460
|
port: normalizeLocalPort(deps.config().local?.port),
|
|
6346
6461
|
endpoints: [],
|
|
6347
6462
|
updatedAt: Date.now()
|
|
@@ -6905,6 +7020,230 @@ function unwrapStreamItem(item) {
|
|
|
6905
7020
|
} : nested;
|
|
6906
7021
|
return item;
|
|
6907
7022
|
}
|
|
7023
|
+
// src/quiet-completion.ts
|
|
7024
|
+
/**
|
|
7025
|
+
* 计划任务按需完成通知(#35)。
|
|
7026
|
+
*
|
|
7027
|
+
* heartbeat 式计划任务大多数轮次"一切正常",每轮都推「任务完成」是噪音。插件
|
|
7028
|
+
* 给计划任务会话提供一个 Agent 工具:Agent 按任务提示词里的规则判断本轮不需要
|
|
7029
|
+
* 提醒时调用它,本轮成功结束就不发完成通知(不写 s2c.notify、不推 APNs)。
|
|
7030
|
+
* 失败、审批、提问照常通知;模型忘了调用时照常提醒,宁可多一次打扰也不漏报。
|
|
7031
|
+
*
|
|
7032
|
+
* 职责划分:
|
|
7033
|
+
* - scheduledTurnOf:同步读宿主会话日志,判定当前这一轮是否由计划任务投递驱动。
|
|
7034
|
+
* 工具的 `when` 与 `execute` 都在宿主进程内同步调用它,不依赖桥接层异步收到的
|
|
7035
|
+
* 事件(实测 `when` 求值时本轮 user/message 尚未落盘,桥接层也还没收到)。
|
|
7036
|
+
* - QuietCompletionTracker:计划任务会话的粘性集合,以及每个会话"本轮已请求静默"
|
|
7037
|
+
* 的记录;HostBridge 在 user/message 与 turn.end 处调用它。
|
|
7038
|
+
* - quietCompletionTool:面向模型的工具定义,由 index.ts 注册到宿主 `ctx.tools`。
|
|
7039
|
+
*
|
|
7040
|
+
* 只在宿主级计划任务(`schedulesHost`,DSH 0.2.2 起)上注册:0.2.1 的
|
|
7041
|
+
* `tools.register` 不支持 `when`,多余参数会被静默忽略,工具会变成全局可见。
|
|
7042
|
+
*/
|
|
7043
|
+
const QUIET_COMPLETION_TOOL_NAME = "deeppilot_skip_completion_notice";
|
|
7044
|
+
/** 往回扫描会话日志的上限:一轮内的事件远少于此,防止异常长轮次拖慢步骤准备。 */
|
|
7045
|
+
const MAX_TURN_SCAN_EVENTS = 2e3;
|
|
7046
|
+
function sessionLogOf(agent) {
|
|
7047
|
+
const session = agent?.session;
|
|
7048
|
+
if (!session || typeof session.id !== "string" || typeof session.seq !== "number" || typeof session.eventAt !== "function") return;
|
|
7049
|
+
return session;
|
|
7050
|
+
}
|
|
7051
|
+
function sourceKindOf(value) {
|
|
7052
|
+
const kind = value?.source?.kind;
|
|
7053
|
+
return typeof kind === "string" ? kind : void 0;
|
|
7054
|
+
}
|
|
7055
|
+
/** 一次 inbox 拼接里插入的条目(按插入顺序)及其目标队列。 */
|
|
7056
|
+
function insertedOf(event) {
|
|
7057
|
+
if (event.type !== "agent/inbox/spliced") return void 0;
|
|
7058
|
+
const data = event.data;
|
|
7059
|
+
const items = Array.isArray(data?.inserted) ? data.inserted : [];
|
|
7060
|
+
return items.length > 0 ? {
|
|
7061
|
+
target: data?.target,
|
|
7062
|
+
items
|
|
7063
|
+
} : void 0;
|
|
7064
|
+
}
|
|
7065
|
+
/**
|
|
7066
|
+
* 判定当前这一轮是否由计划任务投递驱动(同步读宿主会话日志,DSH 0.2.2-rc.1 实测形状)。
|
|
7067
|
+
*
|
|
7068
|
+
* 宿主把待处理消息先拼进 inbox(`agent/inbox/spliced`,带条目本体与目标队列),
|
|
7069
|
+
* `turn/start` 之后的拼接只是取走(`removedCount`,不带内容),条目随后才落成
|
|
7070
|
+
* user/message。因此按 inbox 插入判定:
|
|
7071
|
+
* - 本轮开始之后插入 `next-step` 的用户条目 = 用户中途插话,不算;
|
|
7072
|
+
* - 本轮开始时取走的是上一轮开始之后插入 `next-turn` 的条目;其中最后插入的一条
|
|
7073
|
+
* 来源为 `schedule` 才算(计划任务投递);来源为 `user` 不算;
|
|
7074
|
+
* - 同为 `schedule` 来源的任务变更提示插入的是 `next-step`(宿主注明"不是投递"),不参与判定;
|
|
7075
|
+
* - 扫不到本轮 `turn/start` 时按普通轮次处理,宁可照常提醒。
|
|
7076
|
+
*/
|
|
7077
|
+
function scheduledTurnOf(session) {
|
|
7078
|
+
const floor = Math.max(0, session.seq - MAX_TURN_SCAN_EVENTS);
|
|
7079
|
+
let turn;
|
|
7080
|
+
let seenTurnStart = false;
|
|
7081
|
+
for (let seq = session.seq - 1; seq >= floor; seq--) {
|
|
7082
|
+
const event = session.eventAt(seq);
|
|
7083
|
+
if (!event) continue;
|
|
7084
|
+
if (event.type === "turn/start") {
|
|
7085
|
+
if (seenTurnStart) break;
|
|
7086
|
+
seenTurnStart = true;
|
|
7087
|
+
const value = event.data?.turn;
|
|
7088
|
+
if (typeof value === "number") turn = value;
|
|
7089
|
+
continue;
|
|
7090
|
+
}
|
|
7091
|
+
const splice = insertedOf(event);
|
|
7092
|
+
if (splice === void 0) continue;
|
|
7093
|
+
if (!seenTurnStart) {
|
|
7094
|
+
if (splice.target === "next-step" && splice.items.some((item) => sourceKindOf(item) === "user")) return {
|
|
7095
|
+
scheduled: false,
|
|
7096
|
+
...turn !== void 0 ? { turn } : {}
|
|
7097
|
+
};
|
|
7098
|
+
continue;
|
|
7099
|
+
}
|
|
7100
|
+
if (splice.target !== "next-turn") continue;
|
|
7101
|
+
for (let index = splice.items.length - 1; index >= 0; index--) {
|
|
7102
|
+
const kind = sourceKindOf(splice.items[index]);
|
|
7103
|
+
if (kind === "user") return {
|
|
7104
|
+
scheduled: false,
|
|
7105
|
+
...turn !== void 0 ? { turn } : {}
|
|
7106
|
+
};
|
|
7107
|
+
if (kind === "schedule") return {
|
|
7108
|
+
scheduled: true,
|
|
7109
|
+
...turn !== void 0 ? { turn } : {}
|
|
7110
|
+
};
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7113
|
+
return {
|
|
7114
|
+
scheduled: false,
|
|
7115
|
+
...turn !== void 0 ? { turn } : {}
|
|
7116
|
+
};
|
|
7117
|
+
}
|
|
7118
|
+
var QuietCompletionTracker = class {
|
|
7119
|
+
/**
|
|
7120
|
+
* 判定过计划任务轮次的会话。粘性集合:加入后不再移除,工具不按轮出现或消失。
|
|
7121
|
+
* 宿主每次工具列表变化都会往会话追加一条工具增减消息并让提示词缓存失效,
|
|
7122
|
+
* 按轮开关会让同一会话反复产生这类消息。
|
|
7123
|
+
*/
|
|
7124
|
+
scheduleSessions = /* @__PURE__ */ new Set();
|
|
7125
|
+
quiet = /* @__PURE__ */ new Map();
|
|
7126
|
+
/** 启动时用宿主任务列表里的投递目标会话预热,避免重启后工具先消失再出现。 */
|
|
7127
|
+
seed(sessionIds) {
|
|
7128
|
+
for (const sessionId of sessionIds) if (sessionId) this.scheduleSessions.add(sessionId);
|
|
7129
|
+
}
|
|
7130
|
+
isScheduleSession(sessionId) {
|
|
7131
|
+
return this.scheduleSessions.has(sessionId);
|
|
7132
|
+
}
|
|
7133
|
+
/** 加入粘性集合;返回是否为新加入(调用方据此记一次日志)。 */
|
|
7134
|
+
markScheduleSession(sessionId) {
|
|
7135
|
+
if (this.scheduleSessions.has(sessionId)) return false;
|
|
7136
|
+
this.scheduleSessions.add(sessionId);
|
|
7137
|
+
return true;
|
|
7138
|
+
}
|
|
7139
|
+
/** 工具执行:只在计划任务轮次内生效。 */
|
|
7140
|
+
requestQuiet(sessionId, verdict, seq) {
|
|
7141
|
+
if (sessionId === void 0 || !verdict.scheduled) return {
|
|
7142
|
+
suppressed: false,
|
|
7143
|
+
note: "This run was not started by a scheduled task; the completion notification will be sent as usual."
|
|
7144
|
+
};
|
|
7145
|
+
this.quiet.set(sessionId, {
|
|
7146
|
+
...verdict.turn !== void 0 ? { turn: verdict.turn } : {},
|
|
7147
|
+
seq
|
|
7148
|
+
});
|
|
7149
|
+
return { suppressed: true };
|
|
7150
|
+
}
|
|
7151
|
+
/**
|
|
7152
|
+
* 桥接层观察到 user/message:用户在请求静默之后插话(日志位置更晚)就撤销静默,
|
|
7153
|
+
* 本轮照常提醒。计划任务与注入上下文不影响。
|
|
7154
|
+
*/
|
|
7155
|
+
noteUserMessage(sessionId, sourceKind, seq) {
|
|
7156
|
+
const request = this.quiet.get(sessionId);
|
|
7157
|
+
if (request === void 0 || sourceKind !== "user") return;
|
|
7158
|
+
if (seq === void 0 || seq > request.seq) this.quiet.delete(sessionId);
|
|
7159
|
+
}
|
|
7160
|
+
/**
|
|
7161
|
+
* turn.end 时调用。返回 true 表示本轮完成通知应被抑制:成功结束、且本轮请求过静默。
|
|
7162
|
+
* 轮次号能对上时按轮次号判定;迟到的上一轮 turn.end 保留本轮的请求。
|
|
7163
|
+
*/
|
|
7164
|
+
consumeTurnEnd(sessionId, ok, turn, seq) {
|
|
7165
|
+
const request = this.quiet.get(sessionId);
|
|
7166
|
+
if (request === void 0) return false;
|
|
7167
|
+
if (turn !== void 0 && request.turn !== void 0) {
|
|
7168
|
+
if (turn < request.turn) return false;
|
|
7169
|
+
this.quiet.delete(sessionId);
|
|
7170
|
+
return ok && turn === request.turn;
|
|
7171
|
+
}
|
|
7172
|
+
if (seq !== void 0 && seq < request.seq) return false;
|
|
7173
|
+
this.quiet.delete(sessionId);
|
|
7174
|
+
return ok;
|
|
7175
|
+
}
|
|
7176
|
+
};
|
|
7177
|
+
const DESCRIPTION = [
|
|
7178
|
+
"Call this during a scheduled run only when the task prompt says the user does not need to be notified",
|
|
7179
|
+
"for results like this one. It suppresses only this run's \"task completed\" notification; approvals,",
|
|
7180
|
+
"questions and errors still notify. Do not call it if anything needs the user's attention, or if the",
|
|
7181
|
+
"prompt gives no such rule."
|
|
7182
|
+
].join(" ");
|
|
7183
|
+
/** reason 归一化:非字符串按空串处理,去首尾空白并截断。 */
|
|
7184
|
+
function normalizeReason(raw) {
|
|
7185
|
+
const text = typeof raw === "string" ? raw.trim() : "";
|
|
7186
|
+
return text.length > 200 ? text.slice(0, 200) : text;
|
|
7187
|
+
}
|
|
7188
|
+
/**
|
|
7189
|
+
* 面向模型的工具定义(宿主 ToolDefinition 原始形状:JSON Schema 参数、输出 schema
|
|
7190
|
+
* 与 render、execute)。`log` 只记会话 ID、结果与 reason 长度:reason 由模型撰写,
|
|
7191
|
+
* 可能引用结果内容,按插件日志约定不打正文。
|
|
7192
|
+
*/
|
|
7193
|
+
function quietCompletionTool(tracker, log = () => {}) {
|
|
7194
|
+
return {
|
|
7195
|
+
name: QUIET_COMPLETION_TOOL_NAME,
|
|
7196
|
+
description: DESCRIPTION,
|
|
7197
|
+
parameters: {
|
|
7198
|
+
type: "object",
|
|
7199
|
+
additionalProperties: false,
|
|
7200
|
+
required: ["reason"],
|
|
7201
|
+
properties: { reason: {
|
|
7202
|
+
type: "string",
|
|
7203
|
+
description: `Why this run does not need the user's attention (at most 200 characters).`
|
|
7204
|
+
} }
|
|
7205
|
+
},
|
|
7206
|
+
output: {
|
|
7207
|
+
schema: {
|
|
7208
|
+
type: "object",
|
|
7209
|
+
additionalProperties: false,
|
|
7210
|
+
required: ["suppressed"],
|
|
7211
|
+
properties: {
|
|
7212
|
+
suppressed: { type: "boolean" },
|
|
7213
|
+
note: { type: "string" }
|
|
7214
|
+
}
|
|
7215
|
+
},
|
|
7216
|
+
render(_args, value) {
|
|
7217
|
+
return [{
|
|
7218
|
+
type: "text",
|
|
7219
|
+
text: value.suppressed ? "The completion notification for this run will not be sent." : value.note ?? "The completion notification will be sent as usual."
|
|
7220
|
+
}];
|
|
7221
|
+
}
|
|
7222
|
+
},
|
|
7223
|
+
async execute(args, exec) {
|
|
7224
|
+
const session = sessionLogOf(exec.agent);
|
|
7225
|
+
const reason = normalizeReason(args?.reason);
|
|
7226
|
+
const result = session === void 0 ? tracker.requestQuiet(void 0, { scheduled: false }, 0) : tracker.requestQuiet(session.id, scheduledTurnOf(session), session.seq);
|
|
7227
|
+
log(`quiet completion requested session=${session?.id ?? "?"} suppressed=${result.suppressed} reasonChars=${reason.length}`);
|
|
7228
|
+
return result;
|
|
7229
|
+
}
|
|
7230
|
+
};
|
|
7231
|
+
}
|
|
7232
|
+
/**
|
|
7233
|
+
* 注册工具。`when`:粘性集合内的会话直接放行;否则同步判定本轮是否计划任务轮次,
|
|
7234
|
+
* 是则加入集合(首轮即可见,`new` 目标每次新建的会话同样可见)。普通会话看不到。
|
|
7235
|
+
* 返回宿主给的注销函数。
|
|
7236
|
+
*/
|
|
7237
|
+
function registerQuietCompletionTool(tools, tracker, log = () => {}) {
|
|
7238
|
+
return tools.register(quietCompletionTool(tracker, log), { when: (context) => {
|
|
7239
|
+
const session = sessionLogOf(context.agent);
|
|
7240
|
+
if (session === void 0) return false;
|
|
7241
|
+
if (tracker.isScheduleSession(session.id)) return true;
|
|
7242
|
+
if (!scheduledTurnOf(session).scheduled) return false;
|
|
7243
|
+
if (tracker.markScheduleSession(session.id)) log(`quiet completion: session joined scheduled set session=${session.id}`);
|
|
7244
|
+
return true;
|
|
7245
|
+
} });
|
|
7246
|
+
}
|
|
6908
7247
|
// src/host-capabilities.ts
|
|
6909
7248
|
const pluginUpdateProbes = /* @__PURE__ */ new WeakMap();
|
|
6910
7249
|
function setPluginUpdateProbe(proxy, probe) {
|
|
@@ -6936,6 +7275,10 @@ function capabilityBits(proxy, pushOutlet) {
|
|
|
6936
7275
|
historyPaging: true,
|
|
6937
7276
|
replay: true,
|
|
6938
7277
|
binaryFrames: true,
|
|
7278
|
+
sessionInterest: true,
|
|
7279
|
+
compressedFrames: true,
|
|
7280
|
+
incrementalTail: true,
|
|
7281
|
+
routeHints: true,
|
|
6939
7282
|
approvals: true,
|
|
6940
7283
|
questions: true,
|
|
6941
7284
|
pendingSnapshot: true,
|
|
@@ -7213,6 +7556,10 @@ var AssistantStream = class {
|
|
|
7213
7556
|
};
|
|
7214
7557
|
// src/host-bridge.ts
|
|
7215
7558
|
const MAX_RING_DEFAULT = 2e3;
|
|
7559
|
+
/** 补发量超过它就改发 s2c.resync(经 Funnel 实测吞吐 40~80 KB/s,约 6~12 秒)。 */
|
|
7560
|
+
const REPLAY_RESYNC_BYTES = 524288;
|
|
7561
|
+
/** 只改了用量统计的列表推送,同一会话这段时间内最多推一次(第二批 P5)。 */
|
|
7562
|
+
const SUMMARY_STATS_THROTTLE_MS = 5e3;
|
|
7216
7563
|
/**
|
|
7217
7564
|
* Process-wide bridge state: session mirror, pending approvals/questions,
|
|
7218
7565
|
* and the per-device replay ring. Consumes the in-process mux/host streams
|
|
@@ -7225,6 +7572,13 @@ var HostBridge = class {
|
|
|
7225
7572
|
log;
|
|
7226
7573
|
id = ++BRIDGE_SEQ;
|
|
7227
7574
|
summaries = /* @__PURE__ */ new Map();
|
|
7575
|
+
/**
|
|
7576
|
+
* 列表推送节流(连接传输第二批 P5)。每个会话最近一次推出去的摘要指纹,
|
|
7577
|
+
* 用来丢掉内容完全相同的重复推送;只改了 `stats` 的变化按 SUMMARY_STATS_THROTTLE_MS
|
|
7578
|
+
* 合并,状态、标题、任务清单、待处理标记、活动描述的变化照旧立即推。
|
|
7579
|
+
*/
|
|
7580
|
+
summaryPushed = /* @__PURE__ */ new Map();
|
|
7581
|
+
summaryStatsTimers = /* @__PURE__ */ new Map();
|
|
7228
7582
|
activeTools = /* @__PURE__ */ new Map();
|
|
7229
7583
|
approvals = /* @__PURE__ */ new Map();
|
|
7230
7584
|
questions = /* @__PURE__ */ new Map();
|
|
@@ -7233,6 +7587,8 @@ var HostBridge = class {
|
|
|
7233
7587
|
* and restore them. Excluded from `summaries` and from the live broadcast. */
|
|
7234
7588
|
archivedSummaries = /* @__PURE__ */ new Map();
|
|
7235
7589
|
subagentSessionIds = /* @__PURE__ */ new Set();
|
|
7590
|
+
/** 计划任务按需完成通知(quiet-completion.ts):index.ts 用它注册 Agent 工具。 */
|
|
7591
|
+
quietCompletion = new QuietCompletionTracker();
|
|
7236
7592
|
/**
|
|
7237
7593
|
* 每个会话最近一次已知的队列(inbox 投影的折叠结果)及其指纹。来源是实时
|
|
7238
7594
|
* 投影通知或打开会话时读到的基线;指纹用来丢掉内容没变的重复通知。
|
|
@@ -7415,16 +7771,35 @@ var HostBridge = class {
|
|
|
7415
7771
|
* reader without interactions.respond never gets the missed approval
|
|
7416
7772
|
* frames back (R1/P2).
|
|
7417
7773
|
*/
|
|
7774
|
+
/**
|
|
7775
|
+
* 按序补发游标之后的帧。先按权限与关注集合筛出该 sink 真正会收到的帧,
|
|
7776
|
+
* 估算字节数超过 REPLAY_RESYNC_BYTES 时改发 `s2c.resync`:补发是同步一次性
|
|
7777
|
+
* 写出的,经 Funnel 时插件这侧看不到积压,大补发会把之后所有请求响应堵在
|
|
7778
|
+
* 链路上(实测 450 帧堵 37 秒)。重新同步让客户端只拉列表和正在看的会话。
|
|
7779
|
+
*/
|
|
7418
7780
|
resumeFrom(cursor, target) {
|
|
7419
7781
|
const oldest = this.ring.length > 0 ? this.ring[0].seq : this.cursor + 1;
|
|
7420
7782
|
if (cursor + 1 < oldest) return false;
|
|
7421
7783
|
const receivers = target !== void 0 ? [target] : [...this.sinks];
|
|
7422
|
-
for (const
|
|
7423
|
-
const
|
|
7424
|
-
|
|
7425
|
-
|
|
7784
|
+
for (const sink of receivers) {
|
|
7785
|
+
const window = [];
|
|
7786
|
+
let bytes = 0;
|
|
7787
|
+
for (const entry of this.ring) {
|
|
7788
|
+
if (entry.seq <= cursor) continue;
|
|
7789
|
+
const need = pushScopeFor(entry.type, entry.payload);
|
|
7790
|
+
if (need === void 0 || !sink.canReceive(need)) continue;
|
|
7791
|
+
if (sink.accepts && !sink.accepts(entry.type, entry.payload)) continue;
|
|
7792
|
+
window.push(entry);
|
|
7793
|
+
bytes += Buffer.byteLength(JSON.stringify(entry.payload) ?? "");
|
|
7794
|
+
}
|
|
7795
|
+
if (bytes > 524288) {
|
|
7796
|
+
this.log?.(`resume window ${bytes}B over ${REPLAY_RESYNC_BYTES}B; sending resync`);
|
|
7797
|
+
sink.resync();
|
|
7798
|
+
continue;
|
|
7799
|
+
}
|
|
7800
|
+
for (const entry of window) sink.replay([entry]);
|
|
7801
|
+
sink.replayDone();
|
|
7426
7802
|
}
|
|
7427
|
-
for (const sink of receivers) sink.replayDone();
|
|
7428
7803
|
return true;
|
|
7429
7804
|
}
|
|
7430
7805
|
refreshLiveActivities() {
|
|
@@ -7480,10 +7855,41 @@ var HostBridge = class {
|
|
|
7480
7855
|
this.runMuxStream();
|
|
7481
7856
|
this.refreshSummaries();
|
|
7482
7857
|
}
|
|
7858
|
+
/**
|
|
7859
|
+
* 用宿主任务列表里的投递目标会话预热计划任务会话集合,避免插件重启后工具
|
|
7860
|
+
* 先消失、等下一次投递再出现(每次变化都会给会话追加工具增减消息)。
|
|
7861
|
+
* `new` 目标没有固定会话,只能从最近一次投递回执里取。失败只记日志:集合仍会
|
|
7862
|
+
* 在下一次投递时补上。由 index.ts 在 schedule 服务就绪、注册工具时调用。
|
|
7863
|
+
*/
|
|
7864
|
+
async seedQuietCompletionSessions() {
|
|
7865
|
+
if (this.hostSchedule === void 0) return;
|
|
7866
|
+
const sessionIds = [];
|
|
7867
|
+
let offset = 0;
|
|
7868
|
+
for (;;) {
|
|
7869
|
+
const result = await this.listHostSchedules({
|
|
7870
|
+
limit: HOST_SCHEDULE_PAGE_SIZE,
|
|
7871
|
+
offset
|
|
7872
|
+
});
|
|
7873
|
+
if (!result.ok) {
|
|
7874
|
+
this.log?.(`quiet completion seed skipped: ${result.code}`);
|
|
7875
|
+
return;
|
|
7876
|
+
}
|
|
7877
|
+
for (const task of result.value.tasks) {
|
|
7878
|
+
if (task.target.kind !== "new") sessionIds.push(task.target.sessionId);
|
|
7879
|
+
if (task.lastDelivery) sessionIds.push(task.lastDelivery.targetSessionId);
|
|
7880
|
+
}
|
|
7881
|
+
offset += result.value.tasks.length;
|
|
7882
|
+
if (result.value.tasks.length === 0 || offset >= result.value.total) break;
|
|
7883
|
+
}
|
|
7884
|
+
if (this.disposed) return;
|
|
7885
|
+
this.quietCompletion.seed(sessionIds);
|
|
7886
|
+
}
|
|
7483
7887
|
dispose() {
|
|
7484
7888
|
if (this.disposed) return;
|
|
7485
7889
|
this.disposed = true;
|
|
7486
7890
|
this.abort.abort();
|
|
7891
|
+
for (const timer of this.summaryStatsTimers.values()) clearTimeout(timer);
|
|
7892
|
+
this.summaryStatsTimers.clear();
|
|
7487
7893
|
this.assistantStream.dispose();
|
|
7488
7894
|
this.sinks.clear();
|
|
7489
7895
|
this.sinkSessions.clear();
|
|
@@ -7570,6 +7976,7 @@ var HostBridge = class {
|
|
|
7570
7976
|
if (event.type === "turn/end") this.assistantStream.abandon(sessionId);
|
|
7571
7977
|
if (event.type === "agent-preset/selected") this.record("s2c.skills.changed", { sessionId });
|
|
7572
7978
|
this.noteActivity(sessionId, event);
|
|
7979
|
+
if (event.type === "user/message") this.quietCompletion.noteUserMessage(sessionId, userMessageSourceKind(event.data), event.seq);
|
|
7573
7980
|
this.captureAssistantText(sessionId, event);
|
|
7574
7981
|
const projection = projectEvent(sessionId, event);
|
|
7575
7982
|
this.captureActivity(sessionId, event, projection?.data.tool);
|
|
@@ -7582,7 +7989,12 @@ var HostBridge = class {
|
|
|
7582
7989
|
...streamId && projection.kind === "message.final" ? { streamId } : {}
|
|
7583
7990
|
}
|
|
7584
7991
|
});
|
|
7585
|
-
if (projection?.kind === "turn.end")
|
|
7992
|
+
if (projection?.kind === "turn.end") {
|
|
7993
|
+
const ok = projection.data.ok === true;
|
|
7994
|
+
const turn = event.data?.turn;
|
|
7995
|
+
if (this.quietCompletion.consumeTurnEnd(sessionId, ok, typeof turn === "number" ? turn : void 0, event.seq)) this.log?.(`completion notice skipped by scheduled run session=${sessionId}`);
|
|
7996
|
+
else this.emitTurnCompletedNotify(sessionId, ok);
|
|
7997
|
+
}
|
|
7586
7998
|
break;
|
|
7587
7999
|
}
|
|
7588
8000
|
case "session/projection": {
|
|
@@ -7732,6 +8144,8 @@ var HostBridge = class {
|
|
|
7732
8144
|
this.archivedSummaries = nextArchived;
|
|
7733
8145
|
const removedIds = [...previousIds].filter((id) => !next.has(id));
|
|
7734
8146
|
for (const id of this.lastAssistantText.keys()) if (!next.has(id)) this.lastAssistantText.delete(id);
|
|
8147
|
+
for (const id of removedIds) this.forgetSummaryPush(id);
|
|
8148
|
+
for (const row of next.values()) this.noteSummaryPushed(row);
|
|
7735
8149
|
this.record("s2c.sessions.delta", {
|
|
7736
8150
|
upserted: [...next.values()],
|
|
7737
8151
|
removedIds
|
|
@@ -7851,11 +8265,62 @@ var HostBridge = class {
|
|
|
7851
8265
|
this.pushSummary(row);
|
|
7852
8266
|
}
|
|
7853
8267
|
pushSummary(row) {
|
|
8268
|
+
const full = JSON.stringify(row);
|
|
8269
|
+
const previous = this.summaryPushed.get(row.id);
|
|
8270
|
+
if (previous?.full === full) return;
|
|
8271
|
+
const withoutStats = JSON.stringify({
|
|
8272
|
+
...row,
|
|
8273
|
+
stats: void 0
|
|
8274
|
+
});
|
|
8275
|
+
const now = Date.now();
|
|
8276
|
+
if (previous !== void 0 && previous.withoutStats === withoutStats && now - previous.at < 5e3) {
|
|
8277
|
+
if (!this.summaryStatsTimers.has(row.id)) {
|
|
8278
|
+
const timer = setTimeout(() => {
|
|
8279
|
+
this.summaryStatsTimers.delete(row.id);
|
|
8280
|
+
const latest = this.summaries.get(row.id);
|
|
8281
|
+
if (latest && !this.disposed) this.pushSummary(latest);
|
|
8282
|
+
}, SUMMARY_STATS_THROTTLE_MS - (now - previous.at));
|
|
8283
|
+
timer.unref?.();
|
|
8284
|
+
this.summaryStatsTimers.set(row.id, timer);
|
|
8285
|
+
}
|
|
8286
|
+
return;
|
|
8287
|
+
}
|
|
8288
|
+
const pending = this.summaryStatsTimers.get(row.id);
|
|
8289
|
+
if (pending !== void 0) {
|
|
8290
|
+
clearTimeout(pending);
|
|
8291
|
+
this.summaryStatsTimers.delete(row.id);
|
|
8292
|
+
}
|
|
8293
|
+
this.summaryPushed.set(row.id, {
|
|
8294
|
+
full,
|
|
8295
|
+
withoutStats,
|
|
8296
|
+
at: now
|
|
8297
|
+
});
|
|
7854
8298
|
this.record("s2c.sessions.delta", {
|
|
7855
8299
|
upserted: [row],
|
|
7856
8300
|
removedIds: []
|
|
7857
8301
|
});
|
|
7858
8302
|
}
|
|
8303
|
+
noteSummaryPushed(row) {
|
|
8304
|
+
const pending = this.summaryStatsTimers.get(row.id);
|
|
8305
|
+
if (pending !== void 0) {
|
|
8306
|
+
clearTimeout(pending);
|
|
8307
|
+
this.summaryStatsTimers.delete(row.id);
|
|
8308
|
+
}
|
|
8309
|
+
this.summaryPushed.set(row.id, {
|
|
8310
|
+
full: JSON.stringify(row),
|
|
8311
|
+
withoutStats: JSON.stringify({
|
|
8312
|
+
...row,
|
|
8313
|
+
stats: void 0
|
|
8314
|
+
}),
|
|
8315
|
+
at: Date.now()
|
|
8316
|
+
});
|
|
8317
|
+
}
|
|
8318
|
+
forgetSummaryPush(sessionId) {
|
|
8319
|
+
const pending = this.summaryStatsTimers.get(sessionId);
|
|
8320
|
+
if (pending !== void 0) clearTimeout(pending);
|
|
8321
|
+
this.summaryStatsTimers.delete(sessionId);
|
|
8322
|
+
this.summaryPushed.delete(sessionId);
|
|
8323
|
+
}
|
|
7859
8324
|
listSessions() {
|
|
7860
8325
|
return [...this.summaries.values()].sort((a, b) => b.lastActivityTs - a.lastActivityTs);
|
|
7861
8326
|
}
|
|
@@ -7919,7 +8384,7 @@ var HostBridge = class {
|
|
|
7919
8384
|
} catch {}
|
|
7920
8385
|
}
|
|
7921
8386
|
/** Tail history for an opened session; pushes s2c.session.tail to the sink. */
|
|
7922
|
-
async openSession(sink, sessionId, tailCount) {
|
|
8387
|
+
async openSession(sink, sessionId, tailCount, sinceSeq) {
|
|
7923
8388
|
try {
|
|
7924
8389
|
const response = await this.apiProxy.sessions.history({
|
|
7925
8390
|
rpcId: randomUUID(),
|
|
@@ -7930,14 +8395,26 @@ var HostBridge = class {
|
|
|
7930
8395
|
});
|
|
7931
8396
|
if (!response.result || !response.result.ok) return false;
|
|
7932
8397
|
const result = response.result.value;
|
|
7933
|
-
const
|
|
8398
|
+
const events = result.events ?? [];
|
|
8399
|
+
const touched = /* @__PURE__ */ new Map();
|
|
8400
|
+
const projected = projectHistory(events, touched);
|
|
8401
|
+
const page = limitSessionPageMessages(projected);
|
|
7934
8402
|
const messages = page.messages;
|
|
7935
8403
|
const oldestSeq = messages.length > 0 ? messages[0].seq : 0;
|
|
7936
|
-
|
|
8404
|
+
const hasMore = Boolean(result.hasMore) || page.dropped > 0;
|
|
8405
|
+
const delta = sinceSeq === void 0 ? void 0 : incrementalTail(events, projected, touched, sinceSeq);
|
|
8406
|
+
sink.push("s2c.session.tail", delta === void 0 ? {
|
|
7937
8407
|
sessionId,
|
|
7938
8408
|
messages,
|
|
7939
8409
|
oldestSeq,
|
|
7940
|
-
hasMore
|
|
8410
|
+
hasMore
|
|
8411
|
+
} : {
|
|
8412
|
+
sessionId,
|
|
8413
|
+
messages: delta,
|
|
8414
|
+
oldestSeq,
|
|
8415
|
+
hasMore,
|
|
8416
|
+
incremental: true,
|
|
8417
|
+
sinceSeq
|
|
7941
8418
|
});
|
|
7942
8419
|
this.deriveTitleFallback(sessionId, messages);
|
|
7943
8420
|
this.refreshProjections(sessionId).then(() => this.pushQueueSnapshot(sink, sessionId));
|
|
@@ -8220,6 +8697,7 @@ var HostBridge = class {
|
|
|
8220
8697
|
this.archivedSessionIds = new Set((response.result.value.archivedSessionIds ?? []).map(String));
|
|
8221
8698
|
this.summaries.delete(sessionId);
|
|
8222
8699
|
this.lastAssistantText.delete(sessionId);
|
|
8700
|
+
this.forgetSummaryPush(sessionId);
|
|
8223
8701
|
this.record("s2c.sessions.delta", {
|
|
8224
8702
|
upserted: [],
|
|
8225
8703
|
removedIds: [sessionId]
|
|
@@ -9502,6 +9980,25 @@ function receiptFailureReason(receipt) {
|
|
|
9502
9980
|
* 区间与 wire 行校验(tailCount 1..10000、limit 1..500)共用投影 module 的
|
|
9503
9981
|
* 常量——「一页能要多少条消息」本是一条规则,原先散在三处。
|
|
9504
9982
|
*/
|
|
9983
|
+
/**
|
|
9984
|
+
* 增量 tail(能力 `incrementalTail`):返回 sinceSeq 之后改动过的行;不能保证完整时
|
|
9985
|
+
* 返回 undefined,调用方回退全量。三种回退:
|
|
9986
|
+
* - 窗口没覆盖到 sinceSeq(最旧事件比它新):中间可能有事件没取到;
|
|
9987
|
+
* - sinceSeq 比窗口里最新事件还新:客户端的视图和主机对不上(例如历史被改写);
|
|
9988
|
+
* - 增量本身超过单页 900 KB 预算。
|
|
9989
|
+
*/
|
|
9990
|
+
function incrementalTail(events, projected, touched, sinceSeq) {
|
|
9991
|
+
if (events.length === 0) return void 0;
|
|
9992
|
+
let oldest = Infinity;
|
|
9993
|
+
let newest = -Infinity;
|
|
9994
|
+
for (const { event } of events) {
|
|
9995
|
+
if (event.seq < oldest) oldest = event.seq;
|
|
9996
|
+
if (event.seq > newest) newest = event.seq;
|
|
9997
|
+
}
|
|
9998
|
+
if (oldest > sinceSeq || sinceSeq > newest) return void 0;
|
|
9999
|
+
const limited = limitSessionPageMessages(projected.filter((message) => (touched.get(message.seq) ?? message.seq) > sinceSeq));
|
|
10000
|
+
return limited.dropped > 0 ? void 0 : limited.messages;
|
|
10001
|
+
}
|
|
9505
10002
|
function clampTail(n) {
|
|
9506
10003
|
if (!Number.isFinite(n)) return 100;
|
|
9507
10004
|
return Math.max(10, Math.min(500, Math.floor(n)));
|
|
@@ -11824,6 +12321,25 @@ function requestClientIdentity(req) {
|
|
|
11824
12321
|
}
|
|
11825
12322
|
return normalizedAddress(req.socket.remoteAddress) ?? "unknown";
|
|
11826
12323
|
}
|
|
12324
|
+
// src/welcome-routes.ts
|
|
12325
|
+
/**
|
|
12326
|
+
* welcome.routes(能力 routeHints)的纯计算:局域网监听在线且有指纹时给 RFC1918 地址
|
|
12327
|
+
* (顺序沿用 localLANIPv4Addresses 的物理网卡优先),Funnel 在线时给公网地址;两者都没有
|
|
12328
|
+
* 时返回 undefined,welcome 不带该字段。
|
|
12329
|
+
*/
|
|
12330
|
+
function welcomeRoutes(local, remote, addresses) {
|
|
12331
|
+
const lanAddresses = addresses.filter(isPrivateIPv4);
|
|
12332
|
+
const lan = local.phase === "online" && local.tlsFingerprint && lanAddresses.length > 0 ? {
|
|
12333
|
+
urls: localEndpointURLs(lanAddresses, local.port),
|
|
12334
|
+
tlsFingerprint: local.tlsFingerprint
|
|
12335
|
+
} : void 0;
|
|
12336
|
+
const remoteURL = remote.phase === "online" ? remote.publicURL : void 0;
|
|
12337
|
+
if (lan === void 0 && remoteURL === void 0) return void 0;
|
|
12338
|
+
return {
|
|
12339
|
+
...lan ? { lan } : {},
|
|
12340
|
+
...remoteURL ? { remote: { url: remoteURL } } : {}
|
|
12341
|
+
};
|
|
12342
|
+
}
|
|
11827
12343
|
// src/lan-tls.ts
|
|
11828
12344
|
const LAN_TLS_COMMON_NAME = "dsh-deeppilot";
|
|
11829
12345
|
const LAN_TLS_KEY_FILE = "key.pem";
|
|
@@ -12466,6 +12982,7 @@ function apply(ctx, options) {
|
|
|
12466
12982
|
try {
|
|
12467
12983
|
const connection = new BridgeConnection(ws, {
|
|
12468
12984
|
bridge,
|
|
12985
|
+
routes: phoneRoutes,
|
|
12469
12986
|
devices,
|
|
12470
12987
|
serverVersion: SERVER_VERSION,
|
|
12471
12988
|
audience,
|
|
@@ -12554,6 +13071,12 @@ function apply(ctx, options) {
|
|
|
12554
13071
|
dataDir,
|
|
12555
13072
|
log
|
|
12556
13073
|
});
|
|
13074
|
+
/**
|
|
13075
|
+
* welcome.routes(能力 routeHints):手机据此在同一 Wi-Fi 下先走局域网、在外走 Funnel。
|
|
13076
|
+
* 局域网只给 RFC1918 地址(不含 tailnet 的 100.64/10:手机未入 tailnet 时只会白等超时),
|
|
13077
|
+
* 物理网卡优先;Funnel 只在 online 时给。每次 welcome 现算:地址与 Funnel 状态都会变。
|
|
13078
|
+
*/
|
|
13079
|
+
const phoneRoutes = () => welcomeRoutes(localTransport.status(), remoteTransport.status(), localLANIPv4Addresses());
|
|
12557
13080
|
const localStatus = (addresses) => {
|
|
12558
13081
|
const state = localTransport.status();
|
|
12559
13082
|
return {
|
|
@@ -12634,6 +13157,20 @@ function apply(ctx, options) {
|
|
|
12634
13157
|
if (state.bridge === bridge) state.bridge = void 0;
|
|
12635
13158
|
bridge.dispose();
|
|
12636
13159
|
}, "deeppilot: host streams");
|
|
13160
|
+
apiCtx.inject(["tools", "schedule"], (sub) => {
|
|
13161
|
+
if (bridge.capabilities.schedulesHost !== true) {
|
|
13162
|
+
log("quiet completion tool not registered: host-level schedules unavailable");
|
|
13163
|
+
return;
|
|
13164
|
+
}
|
|
13165
|
+
const tools = sub.tools;
|
|
13166
|
+
if (typeof tools?.register !== "function") {
|
|
13167
|
+
log("quiet completion tool not registered: tools service unavailable", "warn");
|
|
13168
|
+
return;
|
|
13169
|
+
}
|
|
13170
|
+
sub.effect(() => registerQuietCompletionTool(tools, bridge.quietCompletion, (message) => log(message)), "deeppilot: quiet completion tool");
|
|
13171
|
+
log("quiet completion tool registered");
|
|
13172
|
+
bridge.seedQuietCompletionSessions();
|
|
13173
|
+
});
|
|
12637
13174
|
});
|
|
12638
13175
|
ctx.effect(() => async () => {
|
|
12639
13176
|
closeAllConnections();
|