dsh-dispatch 0.1.0 → 0.2.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/cordis.patch.yml +8 -0
- package/dist/index.js +272 -20
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
package/cordis.patch.yml
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# config:
|
|
12
12
|
# relay: 'wss://relay.example.com'
|
|
13
13
|
# allowedRoots: ['/Users/me/code']
|
|
14
|
+
# allowFullAccessDispatch: false
|
|
14
15
|
#
|
|
15
16
|
# `relay` is REQUIRED and ships empty, which is the explicit "not configured
|
|
16
17
|
# yet" state: the plugin loads, logs one actionable error, and reports
|
|
@@ -19,9 +20,16 @@
|
|
|
19
20
|
# OFF — approval forwarding still works; dispatch requests are answered with a
|
|
20
21
|
# visible "dispatch not enabled" error until you list the directories you are
|
|
21
22
|
# willing to have an agent run in.
|
|
23
|
+
#
|
|
24
|
+
# `allowFullAccessDispatch` lets the phone ask for dsh's `danger-full-access`
|
|
25
|
+
# permission preset (full file access, no approval prompts). It is off by
|
|
26
|
+
# default and should stay off unless you know exactly which machine this is;
|
|
27
|
+
# a full-access session also raises no approvals, so nothing reaches your phone
|
|
28
|
+
# for review while it runs.
|
|
22
29
|
- insert:
|
|
23
30
|
- id: dsh-dispatch
|
|
24
31
|
name: dsh-dispatch
|
|
25
32
|
config:
|
|
26
33
|
relay: ''
|
|
27
34
|
allowedRoots: []
|
|
35
|
+
allowFullAccessDispatch: false
|
package/dist/index.js
CHANGED
|
@@ -262,7 +262,7 @@ function installCommands(hub, deps) {
|
|
|
262
262
|
handler: () => ({ kind: "success", text: statusText(hub, deps) })
|
|
263
263
|
}), "dsh-dispatch: /dispatch-status");
|
|
264
264
|
}
|
|
265
|
-
function pairingResult(hub,
|
|
265
|
+
function pairingResult(hub, prefix2 = "") {
|
|
266
266
|
if (hub.config.relay === "") {
|
|
267
267
|
return {
|
|
268
268
|
kind: "error",
|
|
@@ -272,7 +272,7 @@ function pairingResult(hub, prefix = "") {
|
|
|
272
272
|
const code = hub.pairing.pairingCode(hub.config.relay, hub.config.machineName);
|
|
273
273
|
return {
|
|
274
274
|
kind: "success",
|
|
275
|
-
text: `${
|
|
275
|
+
text: `${prefix2}\u914D\u5BF9\u7801\uFF08${hub.config.machineName}\uFF09\uFF1A
|
|
276
276
|
${code}
|
|
277
277
|
|
|
278
278
|
\u6216\u5728\u624B\u673A\u4E0A\u6253\u5F00\uFF1A
|
|
@@ -289,7 +289,9 @@ function statusText(hub, deps) {
|
|
|
289
289
|
`\u623F\u95F4: ${status.room.slice(0, 8)}\u2026`,
|
|
290
290
|
`\u5DF2\u914D\u5BF9: ${hub.pairing.everPaired ? "yes" : "no"}${status.phoneOnline ? "\uFF08\u624B\u673A\u5728\u7EBF\uFF09" : "\uFF08\u624B\u673A\u79BB\u7EBF\uFF09"}`,
|
|
291
291
|
`\u5F85\u6279\u5BA1\u6279: ${String(deps.approvals.openCount())}`,
|
|
292
|
+
`\u5F85\u7B54\u63D0\u95EE: ${String(deps.questions.openCount())}`,
|
|
292
293
|
`\u6D3E\u53D1\u4F1A\u8BDD: ${String(deps.dispatch.dispatchedCount())}`,
|
|
294
|
+
`full-access \u6D3E\u53D1: ${hub.config.allowFullAccessDispatch ? "\u5DF2\u5F00\u542F \u26A0\uFE0F" : "\u5DF2\u7981\u7528"}`,
|
|
293
295
|
`\u89E3\u5BC6\u5931\u8D25: ${String(status.decryptFailures)}${status.decryptFailures > 0 ? " \u2014 \u5BC6\u94A5\u4E0D\u5339\u914D\uFF0C\u8BF7\u91CD\u65B0\u914D\u5BF9" : ""}`,
|
|
294
296
|
`allowedRoots: ${hub.config.allowedRoots.length === 0 ? "(\u7A7A \u2014 \u8FDC\u7A0B\u6D3E\u4EFB\u52A1\u5DF2\u7981\u7528)" : hub.config.allowedRoots.join(", ")}`
|
|
295
297
|
].join("\n");
|
|
@@ -456,6 +458,9 @@ function stderrOf(error) {
|
|
|
456
458
|
var SEEN_LIMIT = 256;
|
|
457
459
|
var DISPATCH_DISABLED = "dispatch \u672A\u542F\u7528\uFF1A\u8BF7\u5728\u63D2\u4EF6\u914D\u7F6E\u4E2D\u8BBE\u7F6E allowedRoots";
|
|
458
460
|
var NO_REMOTE_FOLLOWUP = "\u8BE5\u4F1A\u8BDD\u4E0D\u652F\u6301\u8FDC\u7A0B\u8FFD\u52A0";
|
|
461
|
+
var FULL_ACCESS_DISABLED = "full-access \u6D3E\u53D1\u672A\u542F\u7528\uFF1A\u8BF7\u5728\u63D2\u4EF6\u914D\u7F6E\u4E2D\u5F00\u542F allowFullAccessDispatch";
|
|
462
|
+
var FULL_ACCESS_UNAVAILABLE = "full-access \u4E0D\u53EF\u7528\uFF1A\u672C\u673A dsh \u672A\u7EC4\u5408 permission-presets \u63D2\u4EF6";
|
|
463
|
+
var FULL_ACCESS_PRESET = "danger-full-access";
|
|
459
464
|
function installDispatch(hub) {
|
|
460
465
|
const state = {
|
|
461
466
|
handles: /* @__PURE__ */ new Map(),
|
|
@@ -480,6 +485,11 @@ function installDispatch(hub) {
|
|
|
480
485
|
};
|
|
481
486
|
}
|
|
482
487
|
async function handleRequest(hub, state, message) {
|
|
488
|
+
const rejected = validate(hub, message);
|
|
489
|
+
if (rejected !== void 0) {
|
|
490
|
+
hub.relay.send(failure(message.requestId, rejected));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
483
493
|
const replay = state.seen.get(message.requestId);
|
|
484
494
|
if (replay !== void 0) {
|
|
485
495
|
if (replay !== null) hub.relay.send(replay);
|
|
@@ -497,10 +507,9 @@ async function handleRequest(hub, state, message) {
|
|
|
497
507
|
}
|
|
498
508
|
async function startSession(hub, state, message) {
|
|
499
509
|
const prompt = message.prompt;
|
|
500
|
-
const oversized = overLimit("prompt", prompt);
|
|
501
|
-
if (oversized !== void 0) return failure(message.requestId, oversized);
|
|
502
510
|
const target = resolveCwd(hub, message.cwd);
|
|
503
511
|
if (typeof target !== "string") return failure(message.requestId, target.error);
|
|
512
|
+
const full = message.access === "full";
|
|
504
513
|
try {
|
|
505
514
|
const workspace = await prepareWorkspace({
|
|
506
515
|
cwd: target,
|
|
@@ -509,11 +518,15 @@ async function startSession(hub, state, message) {
|
|
|
509
518
|
});
|
|
510
519
|
state.loaderReady ??= Promise.resolve(hub.ctx.get("loader")?.await()).then(() => void 0);
|
|
511
520
|
await state.loaderReady;
|
|
512
|
-
const
|
|
513
|
-
state.handles.set(handle.agent.id, handle);
|
|
514
|
-
hub.sessions.markDispatched(handle.agent.id, prompt);
|
|
515
|
-
|
|
516
|
-
|
|
521
|
+
const created = await createSession(hub.ctx, workspace.cwd, prompt, full);
|
|
522
|
+
state.handles.set(created.handle.agent.id, created.handle);
|
|
523
|
+
hub.sessions.markDispatched(created.handle.agent.id, prompt, full ? "full" : "standard");
|
|
524
|
+
if (created.fullAccessError !== void 0) {
|
|
525
|
+
hub.report(`dispatch ${message.requestId}`, `\u5B8C\u5168\u8BBF\u95EE\u6743\u9650\u672A\u80FD\u5E94\u7528\uFF1A${created.fullAccessError}`);
|
|
526
|
+
}
|
|
527
|
+
const result = ok(message.requestId, created.handle.agent.id);
|
|
528
|
+
const advisory = created.fullAccessError === void 0 ? workspace.note : `\u5B8C\u5168\u8BBF\u95EE\u6743\u9650\u672A\u80FD\u5E94\u7528\uFF0C\u5DF2\u5728\u6807\u51C6\u6743\u9650\u4E0B\u8FD0\u884C\uFF1A${created.fullAccessError}`;
|
|
529
|
+
return advisory === void 0 ? result : { ...result, note: advisory };
|
|
517
530
|
} catch (error) {
|
|
518
531
|
const detail = error instanceof WorktreeError ? error.message : String(error);
|
|
519
532
|
hub.log.error("dispatch %s failed: %s", message.requestId, detail);
|
|
@@ -570,7 +583,18 @@ async function composeAgent(ctx, selection) {
|
|
|
570
583
|
}
|
|
571
584
|
};
|
|
572
585
|
}
|
|
573
|
-
|
|
586
|
+
function validate(hub, message) {
|
|
587
|
+
const oversized = overLimit("prompt", message.prompt);
|
|
588
|
+
if (oversized !== void 0) return oversized;
|
|
589
|
+
if (message.access !== void 0 && message.access !== "standard" && message.access !== "full") {
|
|
590
|
+
return `access \u53D6\u503C\u65E0\u6548\uFF1A${String(message.access)}`;
|
|
591
|
+
}
|
|
592
|
+
if (message.access !== "full") return void 0;
|
|
593
|
+
if (!hub.config.allowFullAccessDispatch) return FULL_ACCESS_DISABLED;
|
|
594
|
+
if (hub.ctx.get("permissionPresets") === void 0) return FULL_ACCESS_UNAVAILABLE;
|
|
595
|
+
return void 0;
|
|
596
|
+
}
|
|
597
|
+
async function createSession(ctx, cwd, prompt, fullAccess) {
|
|
574
598
|
const selection = ctx.get("agentDefaultModel")?.currentSelection();
|
|
575
599
|
const composition = await composeAgent(ctx, selection);
|
|
576
600
|
const handle = await ctx.agents.create({
|
|
@@ -582,11 +606,22 @@ async function createSession(ctx, cwd, prompt) {
|
|
|
582
606
|
setup: composition.setup
|
|
583
607
|
});
|
|
584
608
|
await handle.agent.whenIdle();
|
|
609
|
+
const fullAccessError = fullAccess ? applyFullAccess(ctx, handle) : void 0;
|
|
585
610
|
handle.agent.followup(createUserMessage({
|
|
586
611
|
content: [{ type: "text", text: prompt }],
|
|
587
612
|
source: { kind: "user" }
|
|
588
613
|
}));
|
|
589
|
-
return handle;
|
|
614
|
+
return fullAccessError === void 0 ? { handle } : { handle, fullAccessError };
|
|
615
|
+
}
|
|
616
|
+
function applyFullAccess(ctx, handle) {
|
|
617
|
+
try {
|
|
618
|
+
const presets = ctx.get("permissionPresets");
|
|
619
|
+
if (presets === void 0) return "permission-presets \u63D2\u4EF6\u672A\u7EC4\u5408";
|
|
620
|
+
presets.set(handle.agent.session, FULL_ACCESS_PRESET);
|
|
621
|
+
return void 0;
|
|
622
|
+
} catch (error) {
|
|
623
|
+
return error instanceof Error ? error.message : String(error);
|
|
624
|
+
}
|
|
590
625
|
}
|
|
591
626
|
function resolveCwd(hub, requested) {
|
|
592
627
|
const roots = hub.config.allowedRoots.map((root) => resolve2(expandHome(root)));
|
|
@@ -669,6 +704,195 @@ function createHub(parts) {
|
|
|
669
704
|
};
|
|
670
705
|
}
|
|
671
706
|
|
|
707
|
+
// src/questions.ts
|
|
708
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
709
|
+
var ASK_USER_QUESTION = "ask_user_question";
|
|
710
|
+
var PROMPT_CHARS = 2e3;
|
|
711
|
+
var PROMPT_BYTES = 4e3;
|
|
712
|
+
var LABEL_CHARS = 300;
|
|
713
|
+
var LABEL_BYTES = 600;
|
|
714
|
+
var REMINDER_MS2 = 5 * 6e4;
|
|
715
|
+
var MAX_REMINDERS2 = 6;
|
|
716
|
+
function installQuestions(hub) {
|
|
717
|
+
const pending = /* @__PURE__ */ new Map();
|
|
718
|
+
hub.ctx.on("tools/execute", (exec, next) => {
|
|
719
|
+
if (exec.name !== ASK_USER_QUESTION || exec.agent === void 0) return next();
|
|
720
|
+
if (!hub.pairing.everPaired || !hub.relay.connected) return next();
|
|
721
|
+
if (exec.signal.aborted) return next();
|
|
722
|
+
return forward2(hub, pending, exec, next);
|
|
723
|
+
});
|
|
724
|
+
return {
|
|
725
|
+
respond(message) {
|
|
726
|
+
const entry = pending.get(message.questionId);
|
|
727
|
+
if (entry === void 0) {
|
|
728
|
+
hub.relay.send(closedMsg2(message.questionId, "expired"));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const value = toAnswerValue(entry, message.answers);
|
|
732
|
+
if (typeof value === "string") {
|
|
733
|
+
hub.report(`question ${entry.questionId}`, value);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
entry.settle(successResult(value), "phone");
|
|
737
|
+
},
|
|
738
|
+
resendPending() {
|
|
739
|
+
for (const entry of pending.values()) hub.relay.send(entry.frame, entry.push);
|
|
740
|
+
},
|
|
741
|
+
openCount: () => pending.size
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function forward2(hub, pending, exec, next) {
|
|
745
|
+
const normalized = normalize(exec.arguments);
|
|
746
|
+
if (normalized === null) {
|
|
747
|
+
hub.report("question", "ask_user_question \u53C2\u6570\u65E0\u6CD5\u89E3\u6790\uFF0C\u5DF2\u4EA4\u56DE\u672C\u673A\u5904\u7406");
|
|
748
|
+
return next();
|
|
749
|
+
}
|
|
750
|
+
const questionId = randomUUID3();
|
|
751
|
+
const sessionId = exec.agent?.id ?? "";
|
|
752
|
+
const frame = {
|
|
753
|
+
v: 1,
|
|
754
|
+
ts: Date.now(),
|
|
755
|
+
type: "question.request",
|
|
756
|
+
questionId,
|
|
757
|
+
sessionId,
|
|
758
|
+
items: normalized.items,
|
|
759
|
+
createdAt: Date.now()
|
|
760
|
+
};
|
|
761
|
+
const push = {
|
|
762
|
+
v: 1,
|
|
763
|
+
ts: Date.now(),
|
|
764
|
+
type: "push",
|
|
765
|
+
kind: "question",
|
|
766
|
+
sessionId,
|
|
767
|
+
title: normalized.items[0]?.prompt.slice(0, 120) ?? "\u9700\u8981\u4F60\u56DE\u7B54\u4E00\u4E2A\u95EE\u9898"
|
|
768
|
+
};
|
|
769
|
+
if (!hub.relay.send(frame, push)) {
|
|
770
|
+
hub.report("question", "\u63D0\u95EE\u672A\u80FD\u63A8\u9001\u5230\u624B\u673A\uFF08relay \u672A\u8FDE\u63A5\u6216\u5185\u5BB9\u8FC7\u5927\uFF09\uFF0C\u5DF2\u4EA4\u56DE\u672C\u673A\u5904\u7406");
|
|
771
|
+
return next();
|
|
772
|
+
}
|
|
773
|
+
const forwarded = { questionId, sessionId, frame, push, labels: normalized.labels };
|
|
774
|
+
return race2(hub, pending, forwarded, exec, next);
|
|
775
|
+
}
|
|
776
|
+
function race2(hub, pending, forwarded, exec, next) {
|
|
777
|
+
const { questionId } = forwarded;
|
|
778
|
+
return new Promise((resolve3) => {
|
|
779
|
+
let settled = false;
|
|
780
|
+
let localFailure;
|
|
781
|
+
const settle = (result, resolution) => {
|
|
782
|
+
if (settled) return;
|
|
783
|
+
settled = true;
|
|
784
|
+
const entry2 = pending.get(questionId);
|
|
785
|
+
if (entry2?.timer !== void 0) clearTimeout(entry2.timer);
|
|
786
|
+
pending.delete(questionId);
|
|
787
|
+
exec.signal.removeEventListener("abort", onAbort);
|
|
788
|
+
hub.relay.send(closedMsg2(questionId, resolution));
|
|
789
|
+
resolve3(result);
|
|
790
|
+
};
|
|
791
|
+
function onAbort() {
|
|
792
|
+
settle(localFailure ?? abortedResult(), "cancelled");
|
|
793
|
+
}
|
|
794
|
+
exec.signal.addEventListener("abort", onAbort, { once: true });
|
|
795
|
+
const entry = { ...forwarded, settle, reminders: 0, timer: void 0 };
|
|
796
|
+
pending.set(questionId, entry);
|
|
797
|
+
armReminder2(hub, entry);
|
|
798
|
+
void next().then((result) => {
|
|
799
|
+
if (!result.isError) settle(result, "local");
|
|
800
|
+
else localFailure = result;
|
|
801
|
+
}, (error) => {
|
|
802
|
+
hub.fail(`question ${questionId}: local provider failed`, error);
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
function armReminder2(hub, entry) {
|
|
807
|
+
entry.timer = setTimeout(() => {
|
|
808
|
+
entry.timer = void 0;
|
|
809
|
+
entry.reminders += 1;
|
|
810
|
+
hub.relay.send(entry.frame, entry.push);
|
|
811
|
+
if (entry.reminders >= MAX_REMINDERS2) {
|
|
812
|
+
hub.log.warn(
|
|
813
|
+
"question %s still unanswered after %d reminders; it stays open (never auto-answered)",
|
|
814
|
+
entry.questionId,
|
|
815
|
+
entry.reminders
|
|
816
|
+
);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
armReminder2(hub, entry);
|
|
820
|
+
}, REMINDER_MS2);
|
|
821
|
+
entry.timer.unref?.();
|
|
822
|
+
}
|
|
823
|
+
function normalize(args) {
|
|
824
|
+
const questions = args?.questions;
|
|
825
|
+
if (!Array.isArray(questions) || questions.length === 0) return null;
|
|
826
|
+
const items = [];
|
|
827
|
+
const labels = /* @__PURE__ */ new Map();
|
|
828
|
+
for (const raw of questions) {
|
|
829
|
+
if (typeof raw?.id !== "string" || typeof raw.question !== "string") return null;
|
|
830
|
+
const options = [];
|
|
831
|
+
const byId = /* @__PURE__ */ new Map();
|
|
832
|
+
(raw.options ?? []).forEach((option, index) => {
|
|
833
|
+
if (typeof option?.label !== "string") return;
|
|
834
|
+
const id = `o${String(index)}`;
|
|
835
|
+
byId.set(id, option.label);
|
|
836
|
+
options.push({ id, label: clamp(decorate(option.label, option.description), LABEL_CHARS, LABEL_BYTES) });
|
|
837
|
+
});
|
|
838
|
+
labels.set(raw.id, byId);
|
|
839
|
+
items.push({
|
|
840
|
+
id: raw.id,
|
|
841
|
+
prompt: clamp(prefix(raw.header, raw.question), PROMPT_CHARS, PROMPT_BYTES),
|
|
842
|
+
options,
|
|
843
|
+
multiSelect: raw.multi_select === true,
|
|
844
|
+
// The tool's output schema always permits `custom`, so free text is
|
|
845
|
+
// always a valid answer — and the only answer when there are no options.
|
|
846
|
+
allowFreeText: true
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return items.length === 0 ? null : { items, labels };
|
|
850
|
+
}
|
|
851
|
+
var decorate = (label, description) => description === void 0 || description === "" ? label : `${label} \u2014 ${description}`;
|
|
852
|
+
var prefix = (header, question) => header === void 0 || header === "" ? question : `${header}
|
|
853
|
+
|
|
854
|
+
${question}`;
|
|
855
|
+
var clamp = (text, chars, bytes) => clampBytes(truncate(text, chars), bytes);
|
|
856
|
+
function toAnswerValue(entry, answers) {
|
|
857
|
+
if (!Array.isArray(answers)) return "\u56DE\u7B54\u683C\u5F0F\u65E0\u6548\uFF1Aanswers \u4E0D\u662F\u6570\u7EC4";
|
|
858
|
+
const seen = /* @__PURE__ */ new Set();
|
|
859
|
+
const projected = [];
|
|
860
|
+
for (const answer of answers) {
|
|
861
|
+
const options = entry.labels.get(answer.itemId);
|
|
862
|
+
if (options === void 0) return `\u56DE\u7B54\u5F15\u7528\u4E86\u672A\u77E5\u7684\u95EE\u9898 id\uFF1A${String(answer.itemId)}`;
|
|
863
|
+
if (seen.has(answer.itemId)) return `\u95EE\u9898 ${answer.itemId} \u88AB\u56DE\u7B54\u4E86\u591A\u6B21`;
|
|
864
|
+
seen.add(answer.itemId);
|
|
865
|
+
const selected = [];
|
|
866
|
+
for (const optionId of answer.optionIds ?? []) {
|
|
867
|
+
const label = options.get(optionId);
|
|
868
|
+
if (label === void 0) return `\u95EE\u9898 ${answer.itemId} \u7684\u9009\u9879 id \u672A\u77E5\uFF1A${String(optionId)}`;
|
|
869
|
+
selected.push(label);
|
|
870
|
+
}
|
|
871
|
+
const item = entry.frame.items.find((candidate) => candidate.id === answer.itemId);
|
|
872
|
+
if (item !== void 0 && !item.multiSelect && selected.length > 1) {
|
|
873
|
+
return `\u95EE\u9898 ${answer.itemId} \u662F\u5355\u9009\uFF0C\u5374\u6536\u5230 ${String(selected.length)} \u4E2A\u9009\u9879`;
|
|
874
|
+
}
|
|
875
|
+
const custom = answer.freeText;
|
|
876
|
+
if (selected.length === 0 && (custom === void 0 || custom === "")) {
|
|
877
|
+
return `\u95EE\u9898 ${answer.itemId} \u65E2\u6CA1\u6709\u9009\u9879\u4E5F\u6CA1\u6709\u6587\u5B57\u56DE\u7B54`;
|
|
878
|
+
}
|
|
879
|
+
projected.push({ id: answer.itemId, selected, ...custom === void 0 || custom === "" ? {} : { custom } });
|
|
880
|
+
}
|
|
881
|
+
const missing = entry.frame.items.filter((item) => !seen.has(item.id)).map((item) => item.id);
|
|
882
|
+
if (missing.length > 0) return `\u8FD8\u6709\u95EE\u9898\u672A\u56DE\u7B54\uFF1A${missing.join(", ")}`;
|
|
883
|
+
return { answers: projected };
|
|
884
|
+
}
|
|
885
|
+
function successResult(value) {
|
|
886
|
+
return { isError: false, value, content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
887
|
+
}
|
|
888
|
+
function abortedResult() {
|
|
889
|
+
const message = "ask_user_question was cancelled before the user answered";
|
|
890
|
+
return { isError: true, error: { message }, content: [{ type: "text", text: `Error: ${message}` }] };
|
|
891
|
+
}
|
|
892
|
+
function closedMsg2(questionId, resolution) {
|
|
893
|
+
return { v: 1, ts: Date.now(), type: "question.closed", questionId, resolution };
|
|
894
|
+
}
|
|
895
|
+
|
|
672
896
|
// src/relay-client.ts
|
|
673
897
|
var BACKOFF_BASE_MS = 500;
|
|
674
898
|
var BACKOFF_MAX_MS = 3e4;
|
|
@@ -904,6 +1128,7 @@ var RelayClient = class {
|
|
|
904
1128
|
var INBOUND_FIELDS = {
|
|
905
1129
|
"sessions.get": [],
|
|
906
1130
|
"approval.respond": ["requestId", "approvalId", "decision"],
|
|
1131
|
+
"question.respond": ["requestId", "questionId"],
|
|
907
1132
|
"dispatch.request": ["requestId", "prompt"],
|
|
908
1133
|
"session.message": ["requestId", "sessionId", "text"]
|
|
909
1134
|
};
|
|
@@ -921,8 +1146,19 @@ function parseInbound(value) {
|
|
|
921
1146
|
return null;
|
|
922
1147
|
}
|
|
923
1148
|
if (type === "dispatch.request" && typeof record["worktree"] !== "boolean") return null;
|
|
1149
|
+
if (type === "question.respond" && !isAnswerList(record["answers"])) return null;
|
|
924
1150
|
return record;
|
|
925
1151
|
}
|
|
1152
|
+
function isAnswerList(value) {
|
|
1153
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
1154
|
+
return value.every((entry) => {
|
|
1155
|
+
const answer = entry;
|
|
1156
|
+
if (typeof answer?.itemId !== "string") return false;
|
|
1157
|
+
if (!Array.isArray(answer.optionIds)) return false;
|
|
1158
|
+
if (!answer.optionIds.every((id) => typeof id === "string")) return false;
|
|
1159
|
+
return answer.freeText === void 0 || typeof answer.freeText === "string";
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
926
1162
|
|
|
927
1163
|
// src/sessions.ts
|
|
928
1164
|
import { SessionId as SessionId2 } from "@deepseek-ai/dsh-session";
|
|
@@ -973,10 +1209,11 @@ var SessionTracker = class {
|
|
|
973
1209
|
});
|
|
974
1210
|
}
|
|
975
1211
|
/** Mark a session as started by us, with the dispatch prompt as its title. */
|
|
976
|
-
markDispatched(sessionId, prompt) {
|
|
1212
|
+
markDispatched(sessionId, prompt, access = "standard") {
|
|
977
1213
|
this.#update(sessionId, (entry) => {
|
|
978
1214
|
entry.dispatched = true;
|
|
979
1215
|
entry.title = truncate(prompt.trim().split("\n", 1)[0] ?? "", TITLE_CHARS2);
|
|
1216
|
+
if (access === "full") entry.access = "full";
|
|
980
1217
|
});
|
|
981
1218
|
}
|
|
982
1219
|
isDispatched(sessionId) {
|
|
@@ -1029,7 +1266,8 @@ var SessionTracker = class {
|
|
|
1029
1266
|
cwd,
|
|
1030
1267
|
state: "idle",
|
|
1031
1268
|
lastActivity: createdAt,
|
|
1032
|
-
dispatched: false
|
|
1269
|
+
dispatched: false,
|
|
1270
|
+
access: "standard"
|
|
1033
1271
|
};
|
|
1034
1272
|
}
|
|
1035
1273
|
#adopt(agent) {
|
|
@@ -1044,6 +1282,7 @@ var SessionTracker = class {
|
|
|
1044
1282
|
openApprovals: 0,
|
|
1045
1283
|
lastActivity: Date.now(),
|
|
1046
1284
|
dispatched: false,
|
|
1285
|
+
access: "standard",
|
|
1047
1286
|
finalized: false
|
|
1048
1287
|
};
|
|
1049
1288
|
this.#tracked.set(agent.id, entry);
|
|
@@ -1061,7 +1300,7 @@ var SessionTracker = class {
|
|
|
1061
1300
|
const entry = this.#tracked.get(sessionId);
|
|
1062
1301
|
if (entry === void 0) return;
|
|
1063
1302
|
const session = toWire(entry);
|
|
1064
|
-
const fingerprint = `${session.state}|${session.title}|${session.cwd}|${String(session.dispatched)}`;
|
|
1303
|
+
const fingerprint = `${session.state}|${session.title}|${session.cwd}|${String(session.dispatched)}|${String(session.access)}`;
|
|
1065
1304
|
if (this.#published.get(sessionId) === fingerprint) return;
|
|
1066
1305
|
this.#published.set(sessionId, fingerprint);
|
|
1067
1306
|
this.#relay.send({ v: 1, ts: Date.now(), type: "session.update", session });
|
|
@@ -1087,7 +1326,8 @@ function toWire(entry) {
|
|
|
1087
1326
|
cwd: entry.cwd,
|
|
1088
1327
|
state: stateOf(entry),
|
|
1089
1328
|
lastActivity: entry.lastActivity,
|
|
1090
|
-
dispatched: entry.dispatched
|
|
1329
|
+
dispatched: entry.dispatched,
|
|
1330
|
+
access: entry.access
|
|
1091
1331
|
};
|
|
1092
1332
|
}
|
|
1093
1333
|
function stateOf(entry) {
|
|
@@ -1104,7 +1344,7 @@ function firstPrompt(agent) {
|
|
|
1104
1344
|
}
|
|
1105
1345
|
|
|
1106
1346
|
// src/version.ts
|
|
1107
|
-
var PLUGIN_VERSION = "0.
|
|
1347
|
+
var PLUGIN_VERSION = "0.2.0";
|
|
1108
1348
|
|
|
1109
1349
|
// src/config.ts
|
|
1110
1350
|
import { hostname } from "os";
|
|
@@ -1113,6 +1353,7 @@ var Config = z.object({
|
|
|
1113
1353
|
relay: z.string().required(),
|
|
1114
1354
|
machineName: z.string().default(hostname()),
|
|
1115
1355
|
allowedRoots: z.array(z.string()).default([]),
|
|
1356
|
+
allowFullAccessDispatch: z.boolean().default(false),
|
|
1116
1357
|
dataDir: z.string().default("~/.dsh-dispatch"),
|
|
1117
1358
|
pwaUrl: z.string().default("http://localhost:5173")
|
|
1118
1359
|
});
|
|
@@ -1150,17 +1391,20 @@ function apply(ctx, config) {
|
|
|
1150
1391
|
const sessions = new SessionTracker(ctx, relay, log);
|
|
1151
1392
|
const hub = createHub({ ctx, config, log, relay, sessions, pairing });
|
|
1152
1393
|
const approvals = installApprovals(hub);
|
|
1394
|
+
const questions = installQuestions(hub);
|
|
1153
1395
|
const dispatch = installDispatch(hub);
|
|
1396
|
+
const routers = { approvals, questions, dispatch };
|
|
1154
1397
|
deferred.message = (message) => {
|
|
1155
|
-
route(hub,
|
|
1398
|
+
route(hub, routers, message);
|
|
1156
1399
|
};
|
|
1157
1400
|
deferred.hello = () => {
|
|
1158
1401
|
publishStatus(hub);
|
|
1159
1402
|
approvals.resendPending();
|
|
1403
|
+
questions.resendPending();
|
|
1160
1404
|
void publishSnapshot(hub);
|
|
1161
1405
|
};
|
|
1162
1406
|
sessions.install();
|
|
1163
|
-
installCommands(hub,
|
|
1407
|
+
installCommands(hub, routers);
|
|
1164
1408
|
startRelay(hub);
|
|
1165
1409
|
}
|
|
1166
1410
|
function startRelay(hub) {
|
|
@@ -1191,7 +1435,8 @@ function startRelay(hub) {
|
|
|
1191
1435
|
};
|
|
1192
1436
|
}, "dsh-dispatch: status heartbeat");
|
|
1193
1437
|
}
|
|
1194
|
-
function route(hub,
|
|
1438
|
+
function route(hub, routers, message) {
|
|
1439
|
+
const { approvals, questions, dispatch } = routers;
|
|
1195
1440
|
switch (message.type) {
|
|
1196
1441
|
case "sessions.get":
|
|
1197
1442
|
void publishSnapshot(hub);
|
|
@@ -1199,6 +1444,9 @@ function route(hub, approvals, dispatch, message) {
|
|
|
1199
1444
|
case "approval.respond":
|
|
1200
1445
|
approvals.respond(message);
|
|
1201
1446
|
return;
|
|
1447
|
+
case "question.respond":
|
|
1448
|
+
questions.respond(message);
|
|
1449
|
+
return;
|
|
1202
1450
|
case "dispatch.request":
|
|
1203
1451
|
void dispatch.request(message).catch((error) => {
|
|
1204
1452
|
hub.fail(`dispatch.request ${message.requestId}`, error);
|
|
@@ -1216,7 +1464,11 @@ function publishStatus(hub) {
|
|
|
1216
1464
|
ts: Date.now(),
|
|
1217
1465
|
type: "machine.status",
|
|
1218
1466
|
machine: hub.config.machineName,
|
|
1219
|
-
pluginVersion: PLUGIN_VERSION
|
|
1467
|
+
pluginVersion: PLUGIN_VERSION,
|
|
1468
|
+
capabilities: {
|
|
1469
|
+
fullAccessDispatch: hub.config.allowFullAccessDispatch,
|
|
1470
|
+
questionForwarding: true
|
|
1471
|
+
}
|
|
1220
1472
|
});
|
|
1221
1473
|
}
|
|
1222
1474
|
async function publishSnapshot(hub) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/approvals.ts","../../shared/src/crypto.ts","../../shared/src/limits.ts","../src/commands.ts","../src/dispatch.ts","../src/pairing.ts","../src/worktree.ts","../src/hub.ts","../src/relay-client.ts","../src/sessions.ts","../src/version.ts","../src/config.ts","../src/index.ts"],"sourcesContent":["/**\n * Approval forwarding — the M1 core.\n *\n * Registers a waterfall answerer on `approval/request` and races three ways:\n * the phone's decision, a local answer arriving through `next()`, and the\n * request's own abort. A timeout is deliberately NOT one of them: docs/PROTOCOL.md\n * invariant 1 forbids auto-allow, so an unanswered question stays pending and\n * is re-pushed instead.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { Agent } from '@deepseek-ai/dsh-agent';\nimport type { CallId } from '@deepseek-ai/dsh-llm';\nimport type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval';\nimport { clampBytes, MAX_DETAIL_CHARS, MAX_FIELD_BYTES, truncate } from '@dsh-dispatch/shared';\nimport type {\n ApprovalRequestMsg,\n ApprovalRespondMsg,\n ApprovalResolution,\n PushCompactMsg,\n} from '@dsh-dispatch/shared';\nimport type { Hub } from './hub.js';\n\nconst TITLE_CHARS = 200;\n/** How often an unanswered approval nudges the phone again. */\nconst REMINDER_MS = 5 * 60_000;\n/** Stop nudging after this many reminders; the question still stays open. */\nconst MAX_REMINDERS = 6;\n\ntype Settle = (outcome: ApprovalOutcome, resolution: ApprovalResolution) => void;\n\ninterface Pending {\n readonly frame: ApprovalRequestMsg;\n readonly push: PushCompactMsg;\n readonly settle: Settle;\n reminders: number;\n timer: ReturnType<typeof setTimeout> | undefined;\n}\n\nexport interface ApprovalRouter {\n /** Apply a phone decision. Unknown ids are answered `expired` so cards clear. */\n respond(message: ApprovalRespondMsg): void;\n /** Re-push every still-open question after a reconnect. */\n resendPending(): void;\n openCount(): number;\n}\n\nexport function installApprovals(hub: Hub): ApprovalRouter {\n const pending = new Map<string, Pending>();\n\n hub.ctx.on('approval/request', (request, next) => {\n // Nothing to forward to: delegate immediately so an unpaired or offline\n // machine pays zero added latency on its local approval UI.\n if (!hub.pairing.everPaired || !hub.relay.connected) return next();\n if (request.signal?.aborted === true) return Promise.resolve<ApprovalOutcome>('cancelled');\n return forward(hub, pending, request, next);\n });\n\n return {\n respond(message: ApprovalRespondMsg): void {\n const entry = pending.get(message.approvalId);\n if (entry === undefined) {\n // Double-tap, or the desktop already answered. Tell the phone to clear.\n hub.relay.send(closedMsg(message.approvalId, 'expired'));\n return;\n }\n entry.settle(message.decision === 'allow' ? 'allowed-once' : 'rejected', message.decision);\n },\n resendPending(): void {\n for (const entry of pending.values()) hub.relay.send(entry.frame, entry.push);\n },\n openCount: () => pending.size,\n };\n}\n\nfunction forward(\n hub: Hub,\n pending: Map<string, Pending>,\n request: ApprovalRequest,\n next: () => Promise<ApprovalOutcome>,\n): Promise<ApprovalOutcome> {\n const approvalId = randomUUID();\n const sessionId = request.agent.id;\n const frame = buildRequestMsg(approvalId, sessionId, request);\n const push = buildPush(sessionId, frame.title);\n if (!hub.relay.send(frame, push)) {\n hub.report('approval', `审批 ${frame.title} 未能推送到手机(relay 未连接),已交回本机处理`);\n return next();\n }\n return race(hub, pending, { approvalId, sessionId, frame, push }, request, next);\n}\n\ninterface Forwarded {\n readonly approvalId: string;\n readonly sessionId: string;\n readonly frame: ApprovalRequestMsg;\n readonly push: PushCompactMsg;\n}\n\nfunction race(\n hub: Hub,\n pending: Map<string, Pending>,\n forwarded: Forwarded,\n request: ApprovalRequest,\n next: () => Promise<ApprovalOutcome>,\n): Promise<ApprovalOutcome> {\n const { approvalId, sessionId } = forwarded;\n return new Promise<ApprovalOutcome>((resolve) => {\n let settled = false;\n const settle: Settle = (outcome, resolution) => {\n if (settled) return;\n settled = true;\n const entry = pending.get(approvalId);\n if (entry?.timer !== undefined) clearTimeout(entry.timer);\n pending.delete(approvalId);\n request.signal?.removeEventListener('abort', onAbort);\n hub.sessions.approvalClosed(sessionId);\n hub.relay.send(closedMsg(approvalId, resolution));\n resolve(outcome);\n };\n function onAbort(): void {\n settle('cancelled', 'superseded');\n }\n request.signal?.addEventListener('abort', onAbort, { once: true });\n const entry: Pending = { ...forwarded, settle, reminders: 0, timer: undefined };\n pending.set(approvalId, entry);\n hub.sessions.approvalOpened(sessionId);\n armReminder(hub, entry);\n // `unavailable` means nobody downstream answered — keep waiting for the\n // phone. Any real decision means a human answered on the desktop first.\n void next().then(\n (outcome) => { if (outcome !== 'unavailable') settle(outcome, 'local'); },\n (error: unknown) => { hub.fail(`approval ${approvalId}: local answerer failed`, error); },\n );\n });\n}\n\n/** Never auto-allow; nudge instead, then go quiet while staying open. */\nfunction armReminder(hub: Hub, entry: Pending): void {\n entry.timer = setTimeout(() => {\n entry.timer = undefined;\n entry.reminders += 1;\n hub.relay.send(entry.frame, entry.push);\n if (entry.reminders >= MAX_REMINDERS) {\n hub.log.warn(\n 'approval %s still unanswered after %d reminders; it stays open (never auto-approved)',\n entry.frame.approvalId, entry.reminders,\n );\n return;\n }\n armReminder(hub, entry);\n }, REMINDER_MS);\n entry.timer.unref?.();\n}\n\nfunction buildRequestMsg(\n approvalId: string,\n sessionId: string,\n request: ApprovalRequest,\n): ApprovalRequestMsg {\n return {\n v: 1,\n ts: Date.now(),\n type: 'approval.request',\n approvalId,\n sessionId,\n title: truncate(request.toolName, TITLE_CHARS),\n // Chars first (protocol conformance), then bytes: 8 192 chars of CJK is\n // ~24 KB of UTF-8, which base64 would expand past the 16 KB envelope.\n detail: clampBytes(truncate(buildDetail(request), MAX_DETAIL_CHARS), MAX_FIELD_BYTES),\n createdAt: Date.now(),\n };\n}\n\n/**\n * The phone must be able to see exactly what it is allowing before allowing it\n * (docs/PRODUCT.md 防呆). `ApprovalRequest` deliberately omits tool arguments —\n * they live on the already-logged `tool/call` this `callId` points at.\n */\nfunction buildDetail(request: ApprovalRequest): string {\n const parts = [`工具: ${request.toolName}`];\n if (request.reason !== undefined && request.reason !== '') parts.push(`原因: ${request.reason}`);\n const args = toolCallArguments(request.agent, request.callId);\n parts.push(args === undefined ? '参数: (调用未记录参数)' : `参数:\\n${args}`);\n return parts.join('\\n\\n');\n}\n\nfunction toolCallArguments(agent: Agent, callId: CallId | undefined): string | undefined {\n if (callId === undefined) return undefined;\n const events = agent.session.events;\n for (let index = events.length - 1; index >= 0; index -= 1) {\n const event = events[index];\n if (event?.type === 'tool/call' && event.data.callId === callId) return event.data.arguments;\n }\n return undefined;\n}\n\nfunction buildPush(sessionId: string, title: string): PushCompactMsg {\n return { v: 1, ts: Date.now(), type: 'push', kind: 'approval', sessionId, title };\n}\n\nfunction closedMsg(approvalId: string, resolution: ApprovalResolution) {\n return { v: 1, ts: Date.now(), type: 'approval.closed', approvalId, resolution } as const;\n}\n","// E2E crypto for dsh-dispatch. Isomorphic (Node + browser) — tweetnacl only.\n// Key/room derivation uses SHA-512 (nacl.hash) truncated; see docs/PROTOCOL.md.\n\nimport nacl from 'tweetnacl';\nimport type { PairingInfo } from './types.js';\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nconst KEY_PREFIX = encoder.encode('dsh-dispatch/key');\nconst ROOM_PREFIX = encoder.encode('dsh-dispatch/room');\n\nconst NONCE_LENGTH = nacl.secretbox.nonceLength;\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n const out = new Uint8Array(a.length + b.length);\n out.set(a, 0);\n out.set(b, a.length);\n return out;\n}\n\nconst B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\nexport function toBase64(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i < bytes.length; i += 3) {\n const b0 = bytes[i]!;\n const b1 = bytes[i + 1];\n const b2 = bytes[i + 2];\n out += B64_ALPHABET[b0 >> 2]!;\n out += B64_ALPHABET[((b0 & 3) << 4) | ((b1 ?? 0) >> 4)]!;\n out += b1 === undefined ? '=' : B64_ALPHABET[((b1 & 15) << 2) | ((b2 ?? 0) >> 6)]!;\n out += b2 === undefined ? '=' : B64_ALPHABET[b2 & 63]!;\n }\n return out;\n}\n\nexport function fromBase64(text: string): Uint8Array | null {\n const clean = text.replace(/=+$/, '');\n if (!/^[A-Za-z0-9+/]*$/.test(clean)) return null;\n const out = new Uint8Array(Math.floor((clean.length * 3) / 4));\n let bits = 0;\n let value = 0;\n let index = 0;\n for (const char of clean) {\n value = (value << 6) | B64_ALPHABET.indexOf(char);\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n out[index++] = (value >> bits) & 0xff;\n }\n }\n return out.slice(0, index);\n}\n\nexport function toBase64Url(bytes: Uint8Array): string {\n return toBase64(bytes).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport function fromBase64Url(text: string): Uint8Array | null {\n return fromBase64(text.replace(/-/g, '+').replace(/_/g, '/'));\n}\n\nexport function generateSecret(): Uint8Array {\n return nacl.randomBytes(32);\n}\n\nexport function deriveKey(secret: Uint8Array): Uint8Array {\n return nacl.hash(concat(KEY_PREFIX, secret)).slice(0, nacl.secretbox.keyLength);\n}\n\nexport function deriveRoomId(secret: Uint8Array): string {\n return toBase64Url(nacl.hash(concat(ROOM_PREFIX, secret)).slice(0, 16));\n}\n\nexport function seal(message: unknown, key: Uint8Array): string {\n const nonce = nacl.randomBytes(NONCE_LENGTH);\n const box = nacl.secretbox(encoder.encode(JSON.stringify(message)), nonce, key);\n return toBase64(concat(nonce, box));\n}\n\n// Returns null on any tamper/garbage/wrong-key input — caller MUST surface it, not drop it.\nexport function open(payload: string, key: Uint8Array): unknown | null {\n const bytes = fromBase64(payload);\n if (bytes === null || bytes.length < NONCE_LENGTH + nacl.secretbox.overheadLength) return null;\n const box = nacl.secretbox.open(bytes.slice(NONCE_LENGTH), bytes.slice(0, NONCE_LENGTH), key);\n if (box === null) return null;\n try {\n return JSON.parse(decoder.decode(box));\n } catch {\n return null;\n }\n}\n\nexport function encodePairing(info: PairingInfo): string {\n const json = JSON.stringify({\n v: 1,\n relay: info.relay,\n secret: toBase64Url(info.secret),\n machine: info.machine,\n });\n return toBase64Url(encoder.encode(json));\n}\n\nexport function decodePairing(code: string): PairingInfo | null {\n const bytes = fromBase64Url(code.trim());\n if (bytes === null) return null;\n try {\n const parsed = JSON.parse(decoder.decode(bytes)) as {\n v?: number;\n relay?: string;\n secret?: string;\n machine?: string;\n };\n if (parsed.v !== 1 || !parsed.relay || !parsed.secret || !parsed.machine) return null;\n const secret = fromBase64Url(parsed.secret);\n if (secret === null || secret.length !== 32) return null;\n return { relay: parsed.relay, secret, machine: parsed.machine };\n } catch {\n return null;\n }\n}\n","// Payload limits from docs/PROTOCOL.md. The sender truncates BEFORE encryption.\n// The relay imports THIS FILE by subpath (src/limits.js) to keep tweetnacl out of its\n// bundle — if this package ever gains a package.json \"exports\" map, add a matching entry.\n\nexport const MAX_ENVELOPE_BYTES = 16 * 1024;\nexport const MAX_DETAIL_CHARS = 8 * 1024;\nexport const MAX_SUMMARY_CHARS = 4 * 1024;\n// Char limits alone don't bound the envelope: CJK is ~3 bytes/char in UTF-8.\n// Free-text fields are clamped by chars first, then by bytes (clampBytes) before seal.\nexport const MAX_PROMPT_CHARS = 8 * 1024;\nexport const MAX_PROMPT_BYTES = 10 * 1024;\nexport const MAX_FIELD_BYTES = 10 * 1024;\nexport const MAX_PUSH_BYTES = 3 * 1024;\nexport const TRUNCATION_SUFFIX = '…[truncated]';\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) return text;\n return text.slice(0, maxChars - TRUNCATION_SUFFIX.length) + TRUNCATION_SUFFIX;\n}\n\nconst byteEncoder = new TextEncoder();\n\nexport function utf8Length(text: string): number {\n return byteEncoder.encode(text).length;\n}\n\n// Clamp to a UTF-8 byte budget without splitting a multi-byte char or surrogate pair.\nexport function clampBytes(text: string, maxBytes: number): string {\n if (utf8Length(text) <= maxBytes) return text;\n const budget = maxBytes - utf8Length(TRUNCATION_SUFFIX);\n let low = 0;\n let high = text.length;\n while (low < high) {\n const mid = Math.ceil((low + high) / 2);\n if (utf8Length(text.slice(0, mid)) <= budget) low = mid;\n else high = mid - 1;\n }\n const tail = text.charCodeAt(low - 1);\n if (tail >= 0xd800 && tail <= 0xdbff) low -= 1;\n return text.slice(0, low) + TRUNCATION_SUFFIX;\n}\n","/**\n * The desktop-side surfaces: pairing, re-pairing, and the status line that\n * makes a broken link visible without leaving dsh (docs/PRODUCT.md 失败可见).\n */\n\nimport type { CommandResult } from '@deepseek-ai/dsh-commands';\nimport type { ApprovalRouter } from './approvals.js';\nimport type { DispatchRouter } from './dispatch.js';\nimport type { Hub } from './hub.js';\n\nexport interface CommandDeps {\n readonly approvals: ApprovalRouter;\n readonly dispatch: DispatchRouter;\n}\n\nexport function installCommands(hub: Hub, deps: CommandDeps): void {\n hub.ctx.effect(() => hub.ctx.commands.register({\n name: 'dispatch-pair',\n description: 'Show the dsh-dispatch pairing code for this machine',\n handler: () => pairingResult(hub),\n }), 'dsh-dispatch: /dispatch-pair');\n\n hub.ctx.effect(() => hub.ctx.commands.register({\n name: 'dispatch-repair',\n description: 'Generate a new dsh-dispatch pairing secret (invalidates every paired phone)',\n handler: () => {\n hub.relay.rekey(hub.pairing.regenerate());\n hub.log.warn('pairing secret regenerated; every previously paired phone is now locked out');\n return pairingResult(hub, '旧配对码已作废,请在每台手机上重新扫码。\\n\\n');\n },\n }), 'dsh-dispatch: /dispatch-repair');\n\n hub.ctx.effect(() => hub.ctx.commands.register({\n name: 'dispatch-status',\n description: 'Show the dsh-dispatch relay connection and remote session state',\n handler: () => ({ kind: 'success', text: statusText(hub, deps) }),\n }), 'dsh-dispatch: /dispatch-status');\n}\n\nfunction pairingResult(hub: Hub, prefix = ''): CommandResult {\n if (hub.config.relay === '') {\n return {\n kind: 'error',\n text: 'relay 未配置:请在 profile 的 cordis.patch.yml 中为 dsh-dispatch 设置 relay: \\'wss://…\\'',\n };\n }\n const code = hub.pairing.pairingCode(hub.config.relay, hub.config.machineName);\n return {\n kind: 'success',\n text: `${prefix}配对码(${hub.config.machineName}):\\n${code}\\n\\n`\n + `或在手机上打开:\\n${hub.config.pwaUrl}/#pair=${code}\\n\\n`\n + '⚠️ 此码等同于本机的控制权:持有者可以批准工具调用并在允许目录中派发任务。'\n + '不要发到群里或截图外传;一旦泄露立即运行 /dispatch-repair。',\n };\n}\n\nfunction statusText(hub: Hub, deps: CommandDeps): string {\n const status = hub.relay.status();\n const link = hub.config.relay === ''\n ? '未配置(请设置 relay)'\n : status.connected\n ? `已连接 ${hub.config.relay}`\n : status.gaveUp\n ? `连接失败 ${String(status.consecutiveFailures)} 次,仍在每 60s 重试 — 请检查 relay`\n : `未连接(重试中,已失败 ${String(status.consecutiveFailures)} 次)`;\n return [\n `relay: ${link}`,\n `房间: ${status.room.slice(0, 8)}…`,\n `已配对: ${hub.pairing.everPaired ? 'yes' : 'no'}`\n + `${status.phoneOnline ? '(手机在线)' : '(手机离线)'}`,\n `待批审批: ${String(deps.approvals.openCount())}`,\n `派发会话: ${String(deps.dispatch.dispatchedCount())}`,\n `解密失败: ${String(status.decryptFailures)}`\n + `${status.decryptFailures > 0 ? ' — 密钥不匹配,请重新配对' : ''}`,\n `allowedRoots: ${hub.config.allowedRoots.length === 0\n ? '(空 — 远程派任务已禁用)'\n : hub.config.allowedRoots.join(', ')}`,\n ].join('\\n');\n}\n","/**\n * Remote dispatch — the M2 core. Starts agent sessions on request from the\n * phone, guards the working directory, and reports the final assistant text\n * when a dispatched turn closes.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { isAbsolute, resolve, sep } from 'node:path';\nimport type { Context } from '@deepseek-ai/cordis';\nimport { installModelSelection } from '@deepseek-ai/dsh-agent';\nimport type { AgentHandle, ModelSelection } from '@deepseek-ai/dsh-agent';\nimport type {} from '@deepseek-ai/dsh-agent-default-model';\n// Carries the `ctx.agentPresets` Context merge; the roster is optional at\n// runtime, so it is read with ctx.get rather than declared in `inject`.\nimport type {} from '@deepseek-ai/dsh-agent-presets';\nimport { createUserMessage } from '@deepseek-ai/dsh-llm';\nimport { SessionId } from '@deepseek-ai/dsh-session';\nimport type { SurfaceEvent } from '@deepseek-ai/dsh-session';\nimport {\n clampBytes,\n MAX_FIELD_BYTES,\n MAX_PROMPT_BYTES,\n MAX_PROMPT_CHARS,\n MAX_SUMMARY_CHARS,\n truncate,\n utf8Length,\n} from '@dsh-dispatch/shared';\nimport type { DispatchRequestMsg, DispatchResultMsg, SessionMessageMsg } from '@dsh-dispatch/shared';\nimport type { Hub } from './hub.js';\nimport { expandHome } from './pairing.js';\nimport { prepareWorkspace, WorktreeError } from './worktree.js';\n\n/** docs/PROTOCOL.md: LRU of seen request ids, for double-tap safety. */\nconst SEEN_LIMIT = 256;\n\nconst DISPATCH_DISABLED = 'dispatch 未启用:请在插件配置中设置 allowedRoots';\nconst NO_REMOTE_FOLLOWUP = '该会话不支持远程追加';\n\nexport interface DispatchRouter {\n request(message: DispatchRequestMsg): Promise<void>;\n followup(message: SessionMessageMsg): Promise<void>;\n dispatchedCount(): number;\n}\n\n/** Per-plugin dispatch state; kept in one object so the handlers stay flat. */\ninterface DispatchState {\n readonly handles: Map<string, AgentHandle>;\n readonly seen: ResultCache;\n readonly inflight: Map<string, Promise<DispatchResultMsg>>;\n loaderReady: Promise<void> | undefined;\n}\n\nexport function installDispatch(hub: Hub): DispatchRouter {\n const state: DispatchState = {\n handles: new Map(),\n seen: new ResultCache(),\n inflight: new Map(),\n loaderReady: undefined,\n };\n hub.ctx.on('agent/turn-stopping', ({ agent }) => {\n if (!hub.sessions.isDispatched(agent.id)) return;\n // Latch the card synchronously: the idle status that closes this turn\n // races the async surface read below and would otherwise land first.\n hub.sessions.markTurnComplete(agent.id);\n void reportTurnFinal(hub, agent.id).catch((error: unknown) => {\n hub.fail(`turn.final for ${agent.id}`, error);\n });\n });\n hub.ctx.on('agent/disposed', ({ agent }) => { state.handles.delete(agent.id); });\n return {\n request: message => handleRequest(hub, state, message),\n followup: message => handleFollowup(hub, state, message),\n dispatchedCount: () => state.handles.size,\n };\n}\n\n/** Answer exactly once per requestId, even under a double-tap. */\nasync function handleRequest(\n hub: Hub,\n state: DispatchState,\n message: DispatchRequestMsg,\n): Promise<void> {\n const replay = state.seen.get(message.requestId);\n if (replay !== undefined) {\n if (replay !== null) hub.relay.send(replay);\n return;\n }\n const pending = state.inflight.get(message.requestId) ?? startSession(hub, state, message);\n state.inflight.set(message.requestId, pending);\n try {\n const result = await pending;\n state.seen.set(message.requestId, result);\n hub.relay.send(result);\n } finally {\n state.inflight.delete(message.requestId);\n }\n}\n\nasync function startSession(\n hub: Hub,\n state: DispatchState,\n message: DispatchRequestMsg,\n): Promise<DispatchResultMsg> {\n const prompt = message.prompt;\n const oversized = overLimit('prompt', prompt);\n if (oversized !== undefined) return failure(message.requestId, oversized);\n const target = resolveCwd(hub, message.cwd);\n if (typeof target !== 'string') return failure(message.requestId, target.error);\n try {\n const workspace = await prepareWorkspace({\n cwd: target,\n dataDir: hub.pairing.dataDir,\n worktree: message.worktree,\n });\n // Loader siblings mount concurrently; never create an agent into a\n // half-composed application (dossier §3.2).\n state.loaderReady ??= Promise.resolve(hub.ctx.get('loader')?.await()).then(() => undefined);\n await state.loaderReady;\n const handle = await createSession(hub.ctx, workspace.cwd, prompt);\n state.handles.set(handle.agent.id, handle);\n hub.sessions.markDispatched(handle.agent.id, prompt);\n const result = ok(message.requestId, handle.agent.id);\n // `note` is advisory on a successful result; `error` never appears with\n // ok: true, so the phone can key its failure state on `ok` alone.\n return workspace.note === undefined ? result : { ...result, note: workspace.note };\n } catch (error) {\n const detail = error instanceof WorktreeError ? error.message : String(error);\n hub.log.error('dispatch %s failed: %s', message.requestId, detail);\n return failure(message.requestId, detail);\n }\n}\n\n/**\n * Append a phone-typed message to a live session. Cold (persisted-only)\n * sessions are refused out loud rather than silently resumed.\n */\nasync function handleFollowup(\n hub: Hub,\n state: DispatchState,\n message: SessionMessageMsg,\n): Promise<void> {\n // Malformed input can never double-apply, so it is answered before the\n // idempotency mark: a retry deserves the same visible complaint.\n const oversized = overLimit('消息', message.text);\n if (oversized !== undefined) {\n hub.report(`session.message ${message.sessionId}`, oversized);\n return;\n }\n if (state.seen.get(message.requestId) !== undefined) return;\n state.seen.set(message.requestId, null);\n const agent = state.handles.get(message.sessionId)?.agent\n ?? hub.sessions.agentOf(message.sessionId);\n if (agent === undefined) {\n hub.report(`session.message ${message.sessionId}`, NO_REMOTE_FOLLOWUP);\n return;\n }\n agent.followup(createUserMessage({\n content: [{ type: 'text', text: message.text }],\n source: { kind: 'user' },\n }));\n}\n\n/**\n * Reject over-limit inbound text instead of clamping it. A well-behaved phone\n * clamps client-side (docs/PROTOCOL.md), so an oversized prompt is malformed\n * input — silently trimming it would run a task the user never wrote.\n * @returns the visible complaint, or undefined when the text is within limits.\n */\nfunction overLimit(field: string, text: string): string | undefined {\n const tail = '请在手机端缩短后重发。';\n if (text.length > MAX_PROMPT_CHARS) {\n return `${field} 超长:${String(text.length)} 字符,上限 ${String(MAX_PROMPT_CHARS)} 字符。${tail}`;\n }\n const bytes = utf8Length(text);\n if (bytes > MAX_PROMPT_BYTES) {\n return `${field} 超长:${String(bytes)} 字节(UTF-8),上限 ${String(MAX_PROMPT_BYTES)} 字节。${tail}`;\n }\n return undefined;\n}\n\n/**\n * Compose the agent's model-facing world: the model selection, and — when the\n * deployment runs a preset roster — the preset that carries its tools.\n *\n * Mirrors `composeAgent()` in packages/host/apiproxy/src/api-proxy.ts:1168,\n * which is what a user-created web session goes through. Skipping the mount is\n * not a SMALLER toolset, it is NO toolset: the web bundle disables every\n * model-facing row in the host plane (`tool-bash`, `tool-fs`, `tool-todo`, …\n * all `disabled: true` in packages/bundle/web-app/cordis.patch.yml) and moves\n * them into each preset's own scope layer, so an agent that joins no preset\n * reaches the model with an empty tool registry and no persona sections.\n */\nasync function composeAgent(ctx: Context, selection: ModelSelection | undefined): Promise<{\n agentPreset?: string;\n setup: (agentCtx: Context) => Promise<void>;\n}> {\n const install = (agentCtx: Context): void => {\n if (selection === undefined) return;\n installModelSelection(agentCtx, { current: selection, assembled: undefined });\n };\n const presets = ctx.get('agentPresets');\n // No roster composed (the headless shape): model-facing rows sit in the host\n // plane and the agent reads them from the global layer.\n if (presets === undefined) {\n return { setup: (agentCtx: Context) => { install(agentCtx); return Promise.resolve(); } };\n }\n // Name no preset, exactly as when a web client creates a session without\n // picking one: the roster's configured default wins.\n const resolvedId = (await presets.resolve()).id;\n return {\n agentPreset: resolvedId,\n setup: async (agentCtx: Context) => {\n install(agentCtx);\n await presets.mount(agentCtx, resolvedId);\n },\n };\n}\n\n/** Create a session in `cwd` with the same composition a web session gets. */\nasync function createSession(ctx: Context, cwd: string, prompt: string): Promise<AgentHandle> {\n const selection = ctx.get('agentDefaultModel')?.currentSelection();\n const composition = await composeAgent(ctx, selection);\n const handle = await ctx.agents.create({\n sessionId: SessionId(`session-${randomUUID()}`),\n // The preset is recorded on the header so a later cold resume rebuilds the\n // composition this session's history was actually produced under.\n meta: composition.agentPreset === undefined\n ? { cwd }\n : { cwd, agentPreset: composition.agentPreset },\n agentOptions: selection === undefined\n ? undefined\n : { provider: selection.provider, model: selection.model },\n setup: composition.setup,\n });\n await handle.agent.whenIdle();\n handle.agent.followup(createUserMessage({\n content: [{ type: 'text', text: prompt }],\n source: { kind: 'user' },\n }));\n return handle;\n}\n\n/**\n * Validate the requested cwd against the configured roots.\n * @returns the absolute directory, or the visible failure to send back.\n */\nfunction resolveCwd(hub: Hub, requested: string | undefined): string | { error: string } {\n const roots = hub.config.allowedRoots.map(root => resolve(expandHome(root)));\n if (roots.length === 0) return { error: DISPATCH_DISABLED };\n const first = roots[0];\n if (requested === undefined) return first === undefined ? { error: DISPATCH_DISABLED } : first;\n const target = resolve(isAbsolute(requested) ? requested : expandHome(requested));\n const inside = roots.some(root => target === root || target.startsWith(root + sep));\n return inside ? target : { error: `cwd 不在允许目录内:${target}` };\n}\n\nasync function reportTurnFinal(hub: Hub, sessionId: string): Promise<void> {\n const snapshot = await hub.ctx.sessionQuery.readSurface(SessionId(sessionId));\n const ok = !hub.sessions.hasError(sessionId);\n const summary = clampBytes(\n truncate(lastAssistantText(snapshot.events), MAX_SUMMARY_CHARS),\n MAX_FIELD_BYTES,\n );\n hub.relay.send(\n { v: 1, ts: Date.now(), type: 'turn.final', sessionId, ok, summary },\n {\n v: 1,\n ts: Date.now(),\n type: 'push',\n kind: ok ? 'done' : 'error',\n sessionId,\n title: truncate(summary.split('\\n', 1)[0] ?? '', 120),\n },\n );\n}\n\nfunction lastAssistantText(events: readonly SurfaceEvent[]): string {\n for (let index = events.length - 1; index >= 0; index -= 1) {\n const event = events[index];\n if (event?.type !== 'assistant/message') continue;\n const text = event.data.message.content\n .filter(block => block.type === 'text')\n .map(block => block.text)\n .join('')\n .trim();\n if (text !== '') return text;\n }\n return '(本轮没有产生助手文本)';\n}\n\nfunction ok(requestId: string, sessionId: string): DispatchResultMsg {\n return { v: 1, ts: Date.now(), type: 'dispatch.result', requestId, ok: true, sessionId };\n}\n\nfunction failure(requestId: string, error: string): DispatchResultMsg {\n return { v: 1, ts: Date.now(), type: 'dispatch.result', requestId, ok: false, error };\n}\n\n/** Bounded replay cache. `null` marks a request that succeeded with no reply frame. */\nclass ResultCache {\n readonly #entries = new Map<string, DispatchResultMsg | null>();\n\n get(requestId: string): DispatchResultMsg | null | undefined {\n return this.#entries.get(requestId);\n }\n\n set(requestId: string, value: DispatchResultMsg | null): void {\n this.#entries.delete(requestId);\n this.#entries.set(requestId, value);\n while (this.#entries.size > SEEN_LIMIT) {\n const oldest = this.#entries.keys().next();\n if (oldest.done === true) break;\n this.#entries.delete(oldest.value);\n }\n }\n}\n","/**\n * Pairing secret lifecycle: the 32 random bytes that are the ONLY thing\n * standing between a stranger and control of this machine. Stored at\n * `<dataDir>/secret` with mode 0600 and never sent to the relay.\n */\n\nimport { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\nimport { encodePairing, fromBase64, generateSecret, toBase64 } from '@dsh-dispatch/shared';\nimport type { Log } from './log.js';\n\nconst SECRET_FILE = 'secret';\nconst PAIRED_FILE = 'paired.json';\nconst SECRET_BYTES = 32;\n\n/** Expand a leading `~` so a configured `~/.dsh-dispatch` resolves to a real path. */\nexport function expandHome(input: string): string {\n if (input === '~') return homedir();\n if (input.startsWith('~/')) return join(homedir(), input.slice(2));\n return resolve(input);\n}\n\n/** Everything the plugin needs to prove a phone is allowed to talk to it. */\nexport class PairingStore {\n readonly dataDir: string;\n #secret: Uint8Array;\n #pairedAt: number | undefined;\n\n private constructor(dataDir: string, secret: Uint8Array, pairedAt: number | undefined) {\n this.dataDir = dataDir;\n this.#secret = secret;\n this.#pairedAt = pairedAt;\n }\n\n /** Load the stored secret, generating and persisting one on first start. */\n static open(dataDir: string, log: Log): PairingStore {\n const dir = expandHome(dataDir);\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n const secretPath = join(dir, SECRET_FILE);\n let secret: Uint8Array | null = null;\n if (existsSync(secretPath)) {\n secret = fromBase64(readFileSync(secretPath, 'utf8').trim());\n if (secret === null || secret.length !== SECRET_BYTES) {\n throw new Error(\n `dsh-dispatch: ${secretPath} is not a valid pairing secret. `\n + 'Delete the file to generate a new one (this invalidates existing pairings).',\n );\n }\n } else {\n secret = generateSecret();\n writeSecret(secretPath, secret);\n }\n return new PairingStore(dir, secret, readPairedAt(join(dir, PAIRED_FILE), log));\n }\n\n get secret(): Uint8Array {\n return this.#secret;\n }\n\n /**\n * Whether a phone has ever completed a pairing with this machine. Approval\n * forwarding stays out of the way entirely until this is true, so an\n * unpaired machine pays zero added approval latency.\n */\n get everPaired(): boolean {\n return this.#pairedAt !== undefined;\n }\n\n get pairedAt(): number | undefined {\n return this.#pairedAt;\n }\n\n /** Record the first sighting of a phone in this room. Idempotent. */\n markPaired(): void {\n if (this.#pairedAt !== undefined) return;\n this.#pairedAt = Date.now();\n writeFileSync(join(this.dataDir, PAIRED_FILE), JSON.stringify({ pairedAt: this.#pairedAt }), {\n mode: 0o600,\n });\n }\n\n /** Replace the secret, invalidating every existing pairing. */\n regenerate(): Uint8Array {\n this.#secret = generateSecret();\n writeSecret(join(this.dataDir, SECRET_FILE), this.#secret);\n this.#pairedAt = undefined;\n rmSync(join(this.dataDir, PAIRED_FILE), { force: true });\n return this.#secret;\n }\n\n /** The out-of-band pairing payload: relay + secret + machine name. */\n pairingCode(relay: string, machine: string): string {\n return encodePairing({ relay, secret: this.#secret, machine });\n }\n}\n\nfunction writeSecret(path: string, secret: Uint8Array): void {\n writeFileSync(path, toBase64(secret), { mode: 0o600 });\n // writeFileSync only applies `mode` when it creates the file; a rewrite of an\n // existing loose-permission file would otherwise stay world-readable.\n chmodSync(path, 0o600);\n}\n\nfunction readPairedAt(path: string, log: Log): number | undefined {\n if (!existsSync(path)) return undefined;\n try {\n const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));\n const value = (parsed as { pairedAt?: unknown }).pairedAt;\n return typeof value === 'number' ? value : undefined;\n } catch (error) {\n // A corrupt marker only costs us the \"have we ever paired\" shortcut; the\n // next phone connection rewrites it. Loud, then treat as never paired.\n log.error('pairing: %s is unreadable, treating this machine as never paired: %s', path, error);\n return undefined;\n }\n}\n","/**\n * Git worktree preparation for dispatched tasks.\n *\n * v0 never deletes a worktree (docs/PRODUCT.md 防呆): cleanup is the user's\n * call, because guessing wrong destroys real work.\n */\n\nimport { execFile } from 'node:child_process';\nimport { randomBytes } from 'node:crypto';\nimport { join } from 'node:path';\nimport { promisify } from 'node:util';\n\nconst run = promisify(execFile);\n\n/** docs/PROTOCOL.md: a failure result carries at most 500 chars of stderr. */\nconst STDERR_TAIL = 500;\n\nexport class WorktreeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'WorktreeError';\n }\n}\n\nexport interface Workspace {\n /** Directory the agent will actually run in. */\n readonly cwd: string;\n /** Branch created for this task, absent when running in `cwd` directly. */\n readonly branch?: string;\n /** Human-readable degradation note the phone must show, when degraded. */\n readonly note?: string;\n}\n\nexport interface PrepareOptions {\n readonly cwd: string;\n readonly dataDir: string;\n readonly worktree: boolean;\n}\n\n/**\n * Resolve the directory a dispatched task runs in.\n * @throws WorktreeError when `git worktree add` fails; the message carries the stderr tail.\n */\nexport async function prepareWorkspace(options: PrepareOptions): Promise<Workspace> {\n if (!options.worktree) return { cwd: options.cwd };\n if (!await isGitRepo(options.cwd)) {\n return {\n cwd: options.cwd,\n note: `已降级:${options.cwd} 不是 git 仓库,任务直接在该目录运行(未创建 worktree)`,\n };\n }\n const root = join(options.dataDir, 'worktrees');\n try {\n return await addWorktree(options.cwd, root, freshSlug());\n } catch (error) {\n const stderr = stderrOf(error);\n if (!isCollision(stderr)) throw new WorktreeError(stderr);\n return await addWorktree(options.cwd, root, freshSlug()).catch((retry: unknown) => {\n throw new WorktreeError(stderrOf(retry));\n });\n }\n}\n\nasync function addWorktree(cwd: string, root: string, slug: string): Promise<Workspace> {\n const target = join(root, slug);\n const branch = `dsh-dispatch/${slug}`;\n await run('git', ['-C', cwd, 'worktree', 'add', target, '-b', branch]);\n return { cwd: target, branch };\n}\n\nasync function isGitRepo(cwd: string): Promise<boolean> {\n try {\n const { stdout } = await run('git', ['-C', cwd, 'rev-parse', '--is-inside-work-tree']);\n return stdout.trim() === 'true';\n } catch {\n // Not a repo, or git is missing. Both mean \"no worktree here\"; the caller\n // reports the degradation to the phone, so this is not a silent swallow.\n return false;\n }\n}\n\n/** `timestamp + short random` — sortable, collision-resistant, readable in `git worktree list`. */\nfunction freshSlug(): string {\n const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\\..+$/, '');\n return `${stamp}-${randomBytes(3).toString('hex')}`;\n}\n\nfunction isCollision(stderr: string): boolean {\n return /already exists|already used by worktree|already checked out/i.test(stderr);\n}\n\nfunction stderrOf(error: unknown): string {\n const raw = (error as { stderr?: unknown }).stderr;\n const text = typeof raw === 'string' && raw.trim() !== ''\n ? raw.trim()\n : error instanceof Error ? error.message : String(error);\n return text.length <= STDERR_TAIL ? text : text.slice(-STDERR_TAIL);\n}\n","/**\n * The shared runtime handle every feature module receives. Also the single\n * place a failure becomes visible: `fail()` logs on the desktop AND pushes an\n * `error` message to the phone, so no catch can quietly swallow anything.\n */\n\nimport type { Context } from '@deepseek-ai/cordis';\nimport { truncate } from '@dsh-dispatch/shared';\nimport type { Config } from './config.js';\nimport type { Log } from './log.js';\nimport type { PairingStore } from './pairing.js';\nimport type { RelayClient } from './relay-client.js';\nimport type { SessionTracker } from './sessions.js';\n\n/** Phone screens are small; a wall of stack trace helps nobody there. */\nconst ERROR_CHARS = 1_000;\n\nexport interface Hub {\n readonly ctx: Context;\n readonly config: Config;\n readonly log: Log;\n readonly relay: RelayClient;\n readonly sessions: SessionTracker;\n readonly pairing: PairingStore;\n /** Report a caught error on both surfaces. */\n fail(context: string, error: unknown): void;\n /** Report an already human-readable failure on both surfaces. */\n report(context: string, message: string): void;\n}\n\nexport function createHub(parts: Omit<Hub, 'fail' | 'report'>): Hub {\n const report = (context: string, message: string): void => {\n parts.log.error('%s: %s', context, message);\n parts.relay.send({\n v: 1,\n ts: Date.now(),\n type: 'error',\n message: truncate(message, ERROR_CHARS),\n context,\n });\n };\n return {\n ...parts,\n report,\n fail(context: string, error: unknown): void {\n report(context, error instanceof Error ? error.message : String(error));\n },\n };\n}\n","/**\n * Outbound wss client to the dsh-dispatch relay. Uses Node's global\n * `WebSocket` (Node >= 22), so the plugin adds no transport dependency.\n *\n * Everything crossing this boundary is sealed with the pairing key: the relay\n * routes ciphertext and learns nothing but room id, role and message size.\n */\n\nimport {\n deriveKey,\n deriveRoomId,\n MAX_ENVELOPE_BYTES,\n open,\n seal,\n} from '@dsh-dispatch/shared';\nimport type {\n MachineToPhoneMsg,\n MsgFrame,\n PhoneToMachineMsg,\n PushCompactMsg,\n} from '@dsh-dispatch/shared';\nimport type { Log } from './log.js';\n\nconst BACKOFF_BASE_MS = 500;\nconst BACKOFF_MAX_MS = 30_000;\n/** Consecutive connect failures after which we surface a visible error state. */\nconst GIVE_UP_AFTER = 10;\n/** Retry interval once we have surfaced the error; we never stop trying. */\nconst GIVE_UP_RETRY_MS = 60_000;\n/** Decryption failures inside this window collapse into one log line. */\nconst TAMPER_LOG_WINDOW_MS = 30_000;\n\n/** Everything `/dispatch-status` needs to describe the link in one line. */\nexport interface RelayStatus {\n readonly connected: boolean;\n readonly phoneOnline: boolean;\n readonly consecutiveFailures: number;\n readonly gaveUp: boolean;\n readonly room: string;\n readonly decryptFailures: number;\n}\n\nexport interface RelayClientOptions {\n readonly url: string;\n readonly secret: Uint8Array;\n readonly log: Log;\n /** A validated inner message arrived from the phone. */\n onMessage(message: PhoneToMachineMsg): void;\n /** The relay accepted our hello — time to publish status and a snapshot. */\n onConnected(): void;\n /** A phone joined or left this room. */\n onPhonePresence(online: boolean): void;\n}\n\nexport class RelayClient {\n readonly #options: RelayClientOptions;\n #key: Uint8Array;\n #room: string;\n #socket: WebSocket | undefined;\n #retry: ReturnType<typeof setTimeout> | undefined;\n #stopped = true;\n #connected = false;\n #phoneOnline = false;\n #failures = 0;\n #gaveUp = false;\n #tamperBurst = 0;\n #tamperWindowStart = 0;\n #tamperTotal = 0;\n\n constructor(options: RelayClientOptions) {\n this.#options = options;\n this.#key = deriveKey(options.secret);\n this.#room = deriveRoomId(options.secret);\n }\n\n get connected(): boolean {\n return this.#connected;\n }\n\n get phoneOnline(): boolean {\n return this.#phoneOnline;\n }\n\n status(): RelayStatus {\n return {\n connected: this.#connected,\n phoneOnline: this.#phoneOnline,\n consecutiveFailures: this.#failures,\n gaveUp: this.#gaveUp,\n room: this.#room,\n decryptFailures: this.#tamperTotal,\n };\n }\n\n start(): void {\n if (!this.#stopped) return;\n this.#stopped = false;\n this.#connect();\n }\n\n stop(): void {\n this.#stopped = true;\n if (this.#retry !== undefined) clearTimeout(this.#retry);\n this.#retry = undefined;\n this.#teardownSocket();\n this.#connected = false;\n this.#phoneOnline = false;\n }\n\n /** Adopt a freshly generated secret: new room, new key, new connection. */\n rekey(secret: Uint8Array): void {\n this.#key = deriveKey(secret);\n this.#room = deriveRoomId(secret);\n this.#failures = 0;\n this.#gaveUp = false;\n if (this.#stopped) return;\n this.stop();\n this.start();\n }\n\n /**\n * Seal and send one inner message. `push` rides along so the relay can wake\n * an offline phone through Web Push without ever seeing the plaintext.\n * @returns whether the frame reached the socket.\n */\n send(message: MachineToPhoneMsg, push?: PushCompactMsg): boolean {\n const socket = this.#socket;\n if (socket === undefined || !this.#connected) return false;\n const frame: MsgFrame = {\n kind: 'msg',\n room: this.#room,\n payload: seal(message, this.#key),\n push: push === undefined ? null : { payload: seal(push, this.#key), tag: push.kind },\n };\n const text = JSON.stringify(frame);\n if (text.length > MAX_ENVELOPE_BYTES) {\n this.#options.log.error(\n 'relay: refusing to send an oversized %s envelope (%d bytes > %d); this is a truncation bug',\n message.type, text.length, MAX_ENVELOPE_BYTES,\n );\n return false;\n }\n socket.send(text);\n return true;\n }\n\n #connect(): void {\n if (this.#stopped) return;\n let socket: WebSocket;\n try {\n socket = new WebSocket(this.#options.url);\n } catch (error) {\n this.#options.log.error('relay: cannot open %s: %s', this.#options.url, error);\n this.#scheduleRetry();\n return;\n }\n this.#socket = socket;\n socket.addEventListener('open', () => {\n socket.send(JSON.stringify({ kind: 'hello', room: this.#room, role: 'machine' }));\n });\n socket.addEventListener('message', (event: MessageEvent) => {\n this.#onFrame(event.data);\n });\n socket.addEventListener('error', () => {\n // 'close' always follows; the event itself carries no useful detail.\n });\n socket.addEventListener('close', (event) => {\n if (this.#socket !== socket) return;\n const code = (event as unknown as { code?: number }).code;\n this.#onDown(`socket closed (${String(code ?? 'no code')})`);\n });\n }\n\n #onFrame(data: unknown): void {\n if (typeof data !== 'string') return;\n let frame: unknown;\n try {\n frame = JSON.parse(data);\n } catch (error) {\n this.#options.log.error('relay: dropped an unparseable frame: %s', error);\n return;\n }\n const kind = (frame as { kind?: unknown }).kind;\n if (kind === 'hello-ok') this.#onHelloOk(frame);\n else if (kind === 'presence') this.#onPresence(frame);\n else if (kind === 'msg') this.#onData(frame);\n else if (kind === 'error') {\n const code = String((frame as { code?: unknown }).code ?? 'unknown');\n this.#options.log.error('relay: rejected our frame with code \"%s\"', code);\n } else {\n this.#options.log.warn('relay: ignoring unknown frame kind \"%s\"', String(kind));\n }\n }\n\n #onHelloOk(frame: unknown): void {\n this.#connected = true;\n this.#failures = 0;\n this.#gaveUp = false;\n const peers = (frame as { peers?: { phone?: unknown } }).peers;\n const phones = typeof peers?.phone === 'number' ? peers.phone : 0;\n this.#options.log.info('relay: connected (room %s…, phones online: %d)', this.#room.slice(0, 8), phones);\n this.#options.onConnected();\n if (phones > 0) this.#setPhoneOnline(true);\n }\n\n #onPresence(frame: unknown): void {\n const { role, online } = frame as { role?: unknown; online?: unknown };\n if (role !== 'phone' || typeof online !== 'boolean') return;\n this.#setPhoneOnline(online);\n }\n\n #setPhoneOnline(online: boolean): void {\n if (this.#phoneOnline === online) return;\n this.#phoneOnline = online;\n this.#options.onPhonePresence(online);\n }\n\n #onData(frame: unknown): void {\n const payload = (frame as { payload?: unknown }).payload;\n if (typeof payload !== 'string') {\n this.#options.log.error('relay: dropped a msg frame with no payload');\n return;\n }\n const plain = open(payload, this.#key);\n if (plain === null) {\n this.#onTamper();\n return;\n }\n const message = parseInbound(plain);\n if (message === null) {\n this.#options.log.warn(\n 'relay: ignoring inner message of type \"%s\" — unknown type or missing/invalid fields '\n + '(the phone may be newer than this plugin)',\n String((plain as { type?: unknown }).type),\n );\n return;\n }\n this.#options.onMessage(message);\n }\n\n /** Decryption failure is never silent: it means tampering or a stale pairing. */\n #onTamper(): void {\n this.#tamperTotal += 1;\n this.#tamperBurst += 1;\n const now = Date.now();\n if (now - this.#tamperWindowStart < TAMPER_LOG_WINDOW_MS) return;\n this.#tamperWindowStart = now;\n this.#options.log.error(\n 'relay: %d message(s) failed to decrypt — wrong key or tampering. Re-pair with /dispatch-repair '\n + 'and re-scan the QR on every phone.',\n this.#tamperBurst,\n );\n this.#tamperBurst = 0;\n }\n\n #onDown(reason: string): void {\n this.#teardownSocket();\n if (this.#stopped) return;\n this.#connected = false;\n this.#setPhoneOnline(false);\n this.#failures += 1;\n this.#options.log.debug('relay: %s, attempt %d', reason, this.#failures);\n this.#scheduleRetry();\n }\n\n #scheduleRetry(): void {\n if (this.#stopped || this.#retry !== undefined) return;\n if (this.#failures >= GIVE_UP_AFTER && !this.#gaveUp) {\n this.#gaveUp = true;\n this.#options.log.error(\n 'relay: %d consecutive failures connecting to %s — check that the relay is reachable. '\n + 'Still retrying every %ds.',\n this.#failures, this.#options.url, GIVE_UP_RETRY_MS / 1000,\n );\n }\n this.#retry = setTimeout(() => {\n this.#retry = undefined;\n this.#connect();\n }, this.#backoffMs());\n this.#retry.unref?.();\n }\n\n #backoffMs(): number {\n if (this.#gaveUp) return GIVE_UP_RETRY_MS;\n const exponential = Math.min(BACKOFF_BASE_MS * 2 ** this.#failures, BACKOFF_MAX_MS);\n return Math.round(exponential * (0.75 + Math.random() * 0.5));\n }\n\n #teardownSocket(): void {\n const socket = this.#socket;\n this.#socket = undefined;\n if (socket === undefined) return;\n try {\n socket.close();\n } catch (error) {\n this.#options.log.debug('relay: error closing socket: %s', error);\n }\n }\n}\n\nconst INBOUND_FIELDS: Record<PhoneToMachineMsg['type'], readonly string[]> = {\n 'sessions.get': [],\n 'approval.respond': ['requestId', 'approvalId', 'decision'],\n 'dispatch.request': ['requestId', 'prompt'],\n 'session.message': ['requestId', 'sessionId', 'text'],\n};\n\n/**\n * Accept only inner messages this plugin understands, with their required\n * string fields present. Unknown `type`/`v` returns null so the caller logs it\n * (forward compatibility per docs/PROTOCOL.md) rather than crashing.\n */\nexport function parseInbound(value: unknown): PhoneToMachineMsg | null {\n if (typeof value !== 'object' || value === null) return null;\n const record = value as Record<string, unknown>;\n if (record['v'] !== 1) return null;\n const type = record['type'];\n if (typeof type !== 'string' || !(type in INBOUND_FIELDS)) return null;\n const required = INBOUND_FIELDS[type as PhoneToMachineMsg['type']];\n for (const field of required) {\n if (typeof record[field] !== 'string' || record[field] === '') return null;\n }\n if (type === 'approval.respond' && record['decision'] !== 'allow' && record['decision'] !== 'deny') {\n return null;\n }\n if (type === 'dispatch.request' && typeof record['worktree'] !== 'boolean') return null;\n return record as unknown as PhoneToMachineMsg;\n}\n","/**\n * Live session board: maps dsh agent lifecycle events onto the protocol's\n * `Session` objects and pushes `session.update` / `session.snapshot` to the\n * phone.\n */\n\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { Agent } from '@deepseek-ai/dsh-agent';\nimport { SessionId } from '@deepseek-ai/dsh-session';\nimport type {} from '@deepseek-ai/dsh-session-query';\nimport { truncate } from '@dsh-dispatch/shared';\nimport type { Session as WireSession, SessionState } from '@dsh-dispatch/shared';\nimport type { Log } from './log.js';\nimport type { RelayClient } from './relay-client.js';\n\n/** docs/PROTOCOL.md: `title` = first 80 chars of the initial prompt. */\nconst TITLE_CHARS = 80;\n\n/** Lifecycle state before the approval overlay is applied. */\ntype BaseState = 'idle' | 'running' | 'done' | 'error';\n\ninterface Tracked {\n sessionId: string;\n title: string;\n cwd: string;\n base: BaseState;\n openApprovals: number;\n lastActivity: number;\n dispatched: boolean;\n /**\n * A dispatched session whose `turn.final` has been sent. It stays `done` on\n * the phone until a NEW turn starts — the idle status that follows every\n * turn must not demote the card back to 空闲.\n */\n finalized: boolean;\n}\n\nexport class SessionTracker {\n readonly #ctx: Context;\n readonly #relay: RelayClient;\n readonly #log: Log;\n readonly #tracked = new Map<string, Tracked>();\n readonly #published = new Map<string, string>();\n\n constructor(ctx: Context, relay: RelayClient, log: Log) {\n this.#ctx = ctx;\n this.#relay = relay;\n this.#log = log;\n }\n\n /** Subscribe the agent lifecycle. Registrations unwind with the plugin. */\n install(): void {\n this.#ctx.on('agent/created', ({ agent }) => {\n this.#adopt(agent);\n this.#publish(agent.id);\n void this.#refreshTitle(agent);\n });\n this.#ctx.on('agent/session-start', ({ agent }) => {\n void this.#refreshTitle(agent);\n });\n this.#ctx.on('agent/status', ({ agent, status }) => {\n this.#update(agent.id, (entry) => {\n // The trailing idle of a finished turn is not new information; only a\n // fresh `running` reopens a finalized session.\n if (status === 'idle' && entry.finalized) return;\n entry.finalized = false;\n entry.base = status;\n });\n });\n this.#ctx.on('agent/error', ({ agent, error }) => {\n this.#log.error('session %s: agent error: %s', agent.id, error);\n this.#update(agent.id, entry => { entry.base = 'error'; });\n });\n this.#ctx.on('agent/turn-stopping', ({ agent }) => {\n void this.#refreshTitle(agent);\n });\n this.#ctx.on('agent/disposed', ({ agent }) => {\n this.#update(agent.id, entry => { entry.base = 'done'; });\n this.#tracked.delete(agent.id);\n this.#published.delete(agent.id);\n });\n }\n\n /** Mark a session as started by us, with the dispatch prompt as its title. */\n markDispatched(sessionId: string, prompt: string): void {\n this.#update(sessionId, entry => {\n entry.dispatched = true;\n entry.title = truncate(prompt.trim().split('\\n', 1)[0] ?? '', TITLE_CHARS);\n });\n }\n\n isDispatched(sessionId: string): boolean {\n return this.#tracked.get(sessionId)?.dispatched ?? false;\n }\n\n /**\n * A dispatched turn has closed and its `turn.final` is on its way. Latch the\n * card at done/error so the trailing idle status cannot demote it.\n */\n markTurnComplete(sessionId: string): void {\n this.#update(sessionId, (entry) => {\n entry.finalized = true;\n if (entry.base !== 'error') entry.base = 'done';\n });\n }\n\n hasError(sessionId: string): boolean {\n return this.#tracked.get(sessionId)?.base === 'error';\n }\n\n /** The live agent for a session id, or undefined when it is not live. */\n agentOf(sessionId: string): Agent | undefined {\n return this.#ctx.agents.get(SessionId(sessionId));\n }\n\n /** An approval for this session is now waiting on a human. */\n approvalOpened(sessionId: string): void {\n this.#update(sessionId, entry => { entry.openApprovals += 1; });\n }\n\n approvalClosed(sessionId: string): void {\n this.#update(sessionId, entry => {\n entry.openApprovals = Math.max(0, entry.openApprovals - 1);\n });\n }\n\n /** Active sessions only — the phone board never shows cold history. */\n async snapshot(): Promise<WireSession[]> {\n const records = await this.#ctx.sessionQuery.listSessions();\n const wire: WireSession[] = [];\n for (const record of records) {\n if (!record.live) continue;\n const id = record.header.id;\n const known = this.#tracked.get(id) ?? this.#adopt(this.#ctx.agents.get(id));\n wire.push(known === undefined\n ? this.#fallbackWire(id, record.header.cwd ?? '', record.header.createdAt)\n : toWire(known));\n }\n return wire;\n }\n\n #fallbackWire(sessionId: string, cwd: string, createdAt: number): WireSession {\n return {\n sessionId,\n title: truncate(sessionId, TITLE_CHARS),\n cwd,\n state: 'idle',\n lastActivity: createdAt,\n dispatched: false,\n };\n }\n\n #adopt(agent: Agent | undefined): Tracked | undefined {\n if (agent === undefined) return undefined;\n const existing = this.#tracked.get(agent.id);\n if (existing !== undefined) return existing;\n const entry: Tracked = {\n sessionId: agent.id,\n title: truncate(firstPrompt(agent) ?? agent.id, TITLE_CHARS),\n cwd: agent.session.header.cwd ?? '',\n base: agent.status,\n openApprovals: 0,\n lastActivity: Date.now(),\n dispatched: false,\n finalized: false,\n };\n this.#tracked.set(agent.id, entry);\n return entry;\n }\n\n #update(sessionId: string, mutate: (entry: Tracked) => void): void {\n const entry = this.#tracked.get(sessionId) ?? this.#adopt(this.#ctx.agents.get(SessionId(sessionId)));\n if (entry === undefined) return;\n mutate(entry);\n entry.lastActivity = Date.now();\n this.#publish(sessionId);\n }\n\n /** Emit `session.update` only when the phone-visible projection changed. */\n #publish(sessionId: string): void {\n const entry = this.#tracked.get(sessionId);\n if (entry === undefined) return;\n const session = toWire(entry);\n const fingerprint = `${session.state}|${session.title}|${session.cwd}|${String(session.dispatched)}`;\n if (this.#published.get(sessionId) === fingerprint) return;\n this.#published.set(sessionId, fingerprint);\n this.#relay.send({ v: 1, ts: Date.now(), type: 'session.update', session });\n }\n\n async #refreshTitle(agent: Agent): Promise<void> {\n const entry = this.#tracked.get(agent.id);\n if (entry === undefined || entry.dispatched) return;\n try {\n const snapshot = await this.#ctx.sessionQuery.readTitle(SessionId(agent.id));\n const title = snapshot?.title ?? firstPrompt(agent);\n if (title === undefined || title === '') return;\n entry.title = truncate(title, TITLE_CHARS);\n this.#publish(agent.id);\n } catch (error) {\n this.#log.error('session %s: could not read title: %s', agent.id, error);\n }\n }\n}\n\nfunction toWire(entry: Tracked): WireSession {\n return {\n sessionId: entry.sessionId,\n title: entry.title,\n cwd: entry.cwd,\n state: stateOf(entry),\n lastActivity: entry.lastActivity,\n dispatched: entry.dispatched,\n };\n}\n\n/**\n * `awaiting_approval` is an overlay, not a lifecycle state: it holds exactly\n * while our answerer has an open question for this session.\n */\nfunction stateOf(entry: Tracked): SessionState {\n if (entry.base === 'done' || entry.base === 'error') return entry.base;\n return entry.openApprovals > 0 ? 'awaiting_approval' : entry.base;\n}\n\n/** Fallback title source: the first human prompt in the session log. */\nfunction firstPrompt(agent: Agent): string | undefined {\n for (const event of agent.session.events) {\n if (event.type !== 'user/message') continue;\n const text = event.data.content\n .filter(block => block.type === 'text')\n .map(block => block.text)\n .join('')\n .trim();\n if (text !== '') return text.split('\\n', 1)[0];\n }\n return undefined;\n}\n","/**\n * Plugin version reported in `machine.status`.\n *\n * Kept as a literal rather than read from package.json so the bundled ESM\n * output has no runtime filesystem dependency. `tests/version.test.ts` fails\n * the build if this drifts from package.json.\n */\nexport const PLUGIN_VERSION = '0.1.0';\n","/**\n * Plugin configuration. Validated by Schemastery before `apply` runs, so every\n * field below is present with its default already filled in.\n */\n\nimport { hostname } from 'node:os';\nimport z from '@deepseek-ai/schemastery';\n\nexport interface Config {\n /** Relay websocket URL, e.g. `wss://relay.example.com`. */\n relay: string;\n /** Name shown on the phone's machine card. */\n machineName: string;\n /**\n * Absolute directories a dispatched agent may run in. Empty (the default)\n * keeps remote dispatch off — approval forwarding still works.\n */\n allowedRoots: string[];\n /** Where the pairing secret and dispatch worktrees live. */\n dataDir: string;\n /** Base URL of the PWA, used only to print the pairing convenience link. */\n pwaUrl: string;\n}\n\nexport const Config: z<Config> = z.object({\n relay: z.string().required(),\n machineName: z.string().default(hostname()),\n allowedRoots: z.array(z.string()).default([]),\n dataDir: z.string().default('~/.dsh-dispatch'),\n pwaUrl: z.string().default('http://localhost:5173'),\n});\n","/**\n * dsh-dispatch — command your DeepSeek Harness machines from your phone.\n *\n * Approvals raised anywhere in this dsh instance are forwarded to a paired\n * phone and answered there; the phone can also start new agent sessions in\n * configured directories and follow up on live ones. Everything between this\n * plugin and the phone is end-to-end encrypted; the relay only routes\n * ciphertext.\n */\n\nimport type { Context } from '@deepseek-ai/cordis';\n// Empty type imports carry the Context/Events declaration merges this plugin\n// listens on and calls into.\nimport type {} from '@deepseek-ai/dsh-agent';\nimport type {} from '@deepseek-ai/dsh-commands';\nimport type {} from '@deepseek-ai/dsh-session-query';\nimport type {} from '@deepseek-ai/dsh-user-approval';\nimport type { PhoneToMachineMsg } from '@dsh-dispatch/shared';\nimport { installApprovals } from './approvals.js';\nimport type { ApprovalRouter } from './approvals.js';\nimport { installCommands } from './commands.js';\nimport type { Config } from './config.js';\nimport { installDispatch } from './dispatch.js';\nimport type { DispatchRouter } from './dispatch.js';\nimport { createHub } from './hub.js';\nimport type { Hub } from './hub.js';\nimport { PairingStore } from './pairing.js';\nimport { RelayClient } from './relay-client.js';\nimport { SessionTracker } from './sessions.js';\nimport { PLUGIN_VERSION } from './version.js';\n\nexport { Config } from './config.js';\n\nexport const name = 'dsh-dispatch';\n\n/**\n * `approval` is deliberately absent: the plugin is useful (dispatch, session\n * board) in a composition without it, and the dossier's apiproxy precedent\n * guards the service rather than requiring it.\n */\nexport const inject = ['agents', 'commands', 'sessionQuery'];\n\n/** docs/PROTOCOL.md: machine.status on connect and every 60s. */\nconst STATUS_INTERVAL_MS = 60_000;\n\nexport function apply(ctx: Context, config: Config): void {\n const log = ctx.logger('dsh-dispatch');\n const pairing = PairingStore.open(config.dataDir, log);\n // The router is built from the relay, and the relay calls into the router:\n // late-bind the two callbacks rather than smuggling a half-built object.\n const deferred = { message(_: PhoneToMachineMsg): void {}, hello(): void {} };\n const relay = new RelayClient({\n url: config.relay,\n secret: pairing.secret,\n log,\n onMessage: message => { deferred.message(message); },\n onConnected: () => { deferred.hello(); },\n onPhonePresence: (online) => {\n if (!online) {\n log.info('relay: phone left the room');\n return;\n }\n pairing.markPaired();\n log.info('relay: phone joined the room');\n deferred.hello();\n },\n });\n const sessions = new SessionTracker(ctx, relay, log);\n const hub = createHub({ ctx, config, log, relay, sessions, pairing });\n const approvals = installApprovals(hub);\n const dispatch = installDispatch(hub);\n deferred.message = message => { route(hub, approvals, dispatch, message); };\n deferred.hello = () => {\n publishStatus(hub);\n approvals.resendPending();\n void publishSnapshot(hub);\n };\n sessions.install();\n installCommands(hub, { approvals, dispatch });\n startRelay(hub);\n}\n\n/** Bring the link up, unless it cannot possibly work — say so once, loudly. */\nfunction startRelay(hub: Hub): void {\n const url = hub.config.relay;\n if (url === '') {\n hub.log.error(\n 'relay is not configured: set `relay: \\'wss://…\\'` on the dsh-dispatch row in your profile\\'s '\n + 'cordis.patch.yml. Approval forwarding and dispatch stay off until then.',\n );\n return;\n }\n if (!/^wss?:\\/\\//.test(url)) {\n hub.log.error('relay \"%s\" is not a ws:// or wss:// URL; refusing to connect', url);\n return;\n }\n hub.ctx.effect(() => {\n hub.relay.start();\n return () => { hub.relay.stop(); };\n }, 'dsh-dispatch: relay client');\n hub.ctx.effect(() => {\n const timer = setInterval(() => { publishStatus(hub); }, STATUS_INTERVAL_MS);\n timer.unref?.();\n return () => { clearInterval(timer); };\n }, 'dsh-dispatch: status heartbeat');\n}\n\nfunction route(\n hub: Hub,\n approvals: ApprovalRouter,\n dispatch: DispatchRouter,\n message: PhoneToMachineMsg,\n): void {\n switch (message.type) {\n case 'sessions.get':\n void publishSnapshot(hub);\n return;\n case 'approval.respond':\n approvals.respond(message);\n return;\n case 'dispatch.request':\n void dispatch.request(message).catch((error: unknown) => {\n hub.fail(`dispatch.request ${message.requestId}`, error);\n });\n return;\n case 'session.message':\n void dispatch.followup(message).catch((error: unknown) => {\n hub.fail(`session.message ${message.requestId}`, error);\n });\n }\n}\n\nfunction publishStatus(hub: Hub): void {\n hub.relay.send({\n v: 1,\n ts: Date.now(),\n type: 'machine.status',\n machine: hub.config.machineName,\n pluginVersion: PLUGIN_VERSION,\n });\n}\n\nasync function publishSnapshot(hub: Hub): Promise<void> {\n try {\n hub.relay.send({\n v: 1,\n ts: Date.now(),\n type: 'session.snapshot',\n sessions: await hub.sessions.snapshot(),\n });\n } catch (error) {\n hub.fail('session.snapshot', error);\n }\n}\n"],"mappings":"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","names":["resolve","entry","randomUUID","resolve","join","randomUUID","resolve","ok","SessionId","TITLE_CHARS","SessionId"]}
|
|
1
|
+
{"version":3,"sources":["../src/approvals.ts","../../shared/src/crypto.ts","../../shared/src/limits.ts","../src/commands.ts","../src/dispatch.ts","../src/pairing.ts","../src/worktree.ts","../src/hub.ts","../src/questions.ts","../src/relay-client.ts","../src/sessions.ts","../src/version.ts","../src/config.ts","../src/index.ts"],"sourcesContent":["/**\n * Approval forwarding — the M1 core.\n *\n * Registers a waterfall answerer on `approval/request` and races three ways:\n * the phone's decision, a local answer arriving through `next()`, and the\n * request's own abort. A timeout is deliberately NOT one of them: docs/PROTOCOL.md\n * invariant 1 forbids auto-allow, so an unanswered question stays pending and\n * is re-pushed instead.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { Agent } from '@deepseek-ai/dsh-agent';\nimport type { CallId } from '@deepseek-ai/dsh-llm';\nimport type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval';\nimport { clampBytes, MAX_DETAIL_CHARS, MAX_FIELD_BYTES, truncate } from '@dsh-dispatch/shared';\nimport type {\n ApprovalRequestMsg,\n ApprovalRespondMsg,\n ApprovalResolution,\n PushCompactMsg,\n} from '@dsh-dispatch/shared';\nimport type { Hub } from './hub.js';\n\nconst TITLE_CHARS = 200;\n/** How often an unanswered approval nudges the phone again. */\nconst REMINDER_MS = 5 * 60_000;\n/** Stop nudging after this many reminders; the question still stays open. */\nconst MAX_REMINDERS = 6;\n\ntype Settle = (outcome: ApprovalOutcome, resolution: ApprovalResolution) => void;\n\ninterface Pending {\n readonly frame: ApprovalRequestMsg;\n readonly push: PushCompactMsg;\n readonly settle: Settle;\n reminders: number;\n timer: ReturnType<typeof setTimeout> | undefined;\n}\n\nexport interface ApprovalRouter {\n /** Apply a phone decision. Unknown ids are answered `expired` so cards clear. */\n respond(message: ApprovalRespondMsg): void;\n /** Re-push every still-open question after a reconnect. */\n resendPending(): void;\n openCount(): number;\n}\n\nexport function installApprovals(hub: Hub): ApprovalRouter {\n const pending = new Map<string, Pending>();\n\n hub.ctx.on('approval/request', (request, next) => {\n // Nothing to forward to: delegate immediately so an unpaired or offline\n // machine pays zero added latency on its local approval UI.\n if (!hub.pairing.everPaired || !hub.relay.connected) return next();\n if (request.signal?.aborted === true) return Promise.resolve<ApprovalOutcome>('cancelled');\n return forward(hub, pending, request, next);\n });\n\n return {\n respond(message: ApprovalRespondMsg): void {\n const entry = pending.get(message.approvalId);\n if (entry === undefined) {\n // Double-tap, or the desktop already answered. Tell the phone to clear.\n hub.relay.send(closedMsg(message.approvalId, 'expired'));\n return;\n }\n entry.settle(message.decision === 'allow' ? 'allowed-once' : 'rejected', message.decision);\n },\n resendPending(): void {\n for (const entry of pending.values()) hub.relay.send(entry.frame, entry.push);\n },\n openCount: () => pending.size,\n };\n}\n\nfunction forward(\n hub: Hub,\n pending: Map<string, Pending>,\n request: ApprovalRequest,\n next: () => Promise<ApprovalOutcome>,\n): Promise<ApprovalOutcome> {\n const approvalId = randomUUID();\n const sessionId = request.agent.id;\n const frame = buildRequestMsg(approvalId, sessionId, request);\n const push = buildPush(sessionId, frame.title);\n if (!hub.relay.send(frame, push)) {\n hub.report('approval', `审批 ${frame.title} 未能推送到手机(relay 未连接),已交回本机处理`);\n return next();\n }\n return race(hub, pending, { approvalId, sessionId, frame, push }, request, next);\n}\n\ninterface Forwarded {\n readonly approvalId: string;\n readonly sessionId: string;\n readonly frame: ApprovalRequestMsg;\n readonly push: PushCompactMsg;\n}\n\nfunction race(\n hub: Hub,\n pending: Map<string, Pending>,\n forwarded: Forwarded,\n request: ApprovalRequest,\n next: () => Promise<ApprovalOutcome>,\n): Promise<ApprovalOutcome> {\n const { approvalId, sessionId } = forwarded;\n return new Promise<ApprovalOutcome>((resolve) => {\n let settled = false;\n const settle: Settle = (outcome, resolution) => {\n if (settled) return;\n settled = true;\n const entry = pending.get(approvalId);\n if (entry?.timer !== undefined) clearTimeout(entry.timer);\n pending.delete(approvalId);\n request.signal?.removeEventListener('abort', onAbort);\n hub.sessions.approvalClosed(sessionId);\n hub.relay.send(closedMsg(approvalId, resolution));\n resolve(outcome);\n };\n function onAbort(): void {\n settle('cancelled', 'superseded');\n }\n request.signal?.addEventListener('abort', onAbort, { once: true });\n const entry: Pending = { ...forwarded, settle, reminders: 0, timer: undefined };\n pending.set(approvalId, entry);\n hub.sessions.approvalOpened(sessionId);\n armReminder(hub, entry);\n // `unavailable` means nobody downstream answered — keep waiting for the\n // phone. Any real decision means a human answered on the desktop first.\n void next().then(\n (outcome) => { if (outcome !== 'unavailable') settle(outcome, 'local'); },\n (error: unknown) => { hub.fail(`approval ${approvalId}: local answerer failed`, error); },\n );\n });\n}\n\n/** Never auto-allow; nudge instead, then go quiet while staying open. */\nfunction armReminder(hub: Hub, entry: Pending): void {\n entry.timer = setTimeout(() => {\n entry.timer = undefined;\n entry.reminders += 1;\n hub.relay.send(entry.frame, entry.push);\n if (entry.reminders >= MAX_REMINDERS) {\n hub.log.warn(\n 'approval %s still unanswered after %d reminders; it stays open (never auto-approved)',\n entry.frame.approvalId, entry.reminders,\n );\n return;\n }\n armReminder(hub, entry);\n }, REMINDER_MS);\n entry.timer.unref?.();\n}\n\nfunction buildRequestMsg(\n approvalId: string,\n sessionId: string,\n request: ApprovalRequest,\n): ApprovalRequestMsg {\n return {\n v: 1,\n ts: Date.now(),\n type: 'approval.request',\n approvalId,\n sessionId,\n title: truncate(request.toolName, TITLE_CHARS),\n // Chars first (protocol conformance), then bytes: 8 192 chars of CJK is\n // ~24 KB of UTF-8, which base64 would expand past the 16 KB envelope.\n detail: clampBytes(truncate(buildDetail(request), MAX_DETAIL_CHARS), MAX_FIELD_BYTES),\n createdAt: Date.now(),\n };\n}\n\n/**\n * The phone must be able to see exactly what it is allowing before allowing it\n * (docs/PRODUCT.md 防呆). `ApprovalRequest` deliberately omits tool arguments —\n * they live on the already-logged `tool/call` this `callId` points at.\n */\nfunction buildDetail(request: ApprovalRequest): string {\n const parts = [`工具: ${request.toolName}`];\n if (request.reason !== undefined && request.reason !== '') parts.push(`原因: ${request.reason}`);\n const args = toolCallArguments(request.agent, request.callId);\n parts.push(args === undefined ? '参数: (调用未记录参数)' : `参数:\\n${args}`);\n return parts.join('\\n\\n');\n}\n\nfunction toolCallArguments(agent: Agent, callId: CallId | undefined): string | undefined {\n if (callId === undefined) return undefined;\n const events = agent.session.events;\n for (let index = events.length - 1; index >= 0; index -= 1) {\n const event = events[index];\n if (event?.type === 'tool/call' && event.data.callId === callId) return event.data.arguments;\n }\n return undefined;\n}\n\nfunction buildPush(sessionId: string, title: string): PushCompactMsg {\n return { v: 1, ts: Date.now(), type: 'push', kind: 'approval', sessionId, title };\n}\n\nfunction closedMsg(approvalId: string, resolution: ApprovalResolution) {\n return { v: 1, ts: Date.now(), type: 'approval.closed', approvalId, resolution } as const;\n}\n","// E2E crypto for dsh-dispatch. Isomorphic (Node + browser) — tweetnacl only.\n// Key/room derivation uses SHA-512 (nacl.hash) truncated; see docs/PROTOCOL.md.\n\nimport nacl from 'tweetnacl';\nimport type { PairingInfo } from './types.js';\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nconst KEY_PREFIX = encoder.encode('dsh-dispatch/key');\nconst ROOM_PREFIX = encoder.encode('dsh-dispatch/room');\n\nconst NONCE_LENGTH = nacl.secretbox.nonceLength;\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n const out = new Uint8Array(a.length + b.length);\n out.set(a, 0);\n out.set(b, a.length);\n return out;\n}\n\nconst B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\nexport function toBase64(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i < bytes.length; i += 3) {\n const b0 = bytes[i]!;\n const b1 = bytes[i + 1];\n const b2 = bytes[i + 2];\n out += B64_ALPHABET[b0 >> 2]!;\n out += B64_ALPHABET[((b0 & 3) << 4) | ((b1 ?? 0) >> 4)]!;\n out += b1 === undefined ? '=' : B64_ALPHABET[((b1 & 15) << 2) | ((b2 ?? 0) >> 6)]!;\n out += b2 === undefined ? '=' : B64_ALPHABET[b2 & 63]!;\n }\n return out;\n}\n\nexport function fromBase64(text: string): Uint8Array | null {\n const clean = text.replace(/=+$/, '');\n if (!/^[A-Za-z0-9+/]*$/.test(clean)) return null;\n const out = new Uint8Array(Math.floor((clean.length * 3) / 4));\n let bits = 0;\n let value = 0;\n let index = 0;\n for (const char of clean) {\n value = (value << 6) | B64_ALPHABET.indexOf(char);\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n out[index++] = (value >> bits) & 0xff;\n }\n }\n return out.slice(0, index);\n}\n\nexport function toBase64Url(bytes: Uint8Array): string {\n return toBase64(bytes).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport function fromBase64Url(text: string): Uint8Array | null {\n return fromBase64(text.replace(/-/g, '+').replace(/_/g, '/'));\n}\n\nexport function generateSecret(): Uint8Array {\n return nacl.randomBytes(32);\n}\n\nexport function deriveKey(secret: Uint8Array): Uint8Array {\n return nacl.hash(concat(KEY_PREFIX, secret)).slice(0, nacl.secretbox.keyLength);\n}\n\nexport function deriveRoomId(secret: Uint8Array): string {\n return toBase64Url(nacl.hash(concat(ROOM_PREFIX, secret)).slice(0, 16));\n}\n\nexport function seal(message: unknown, key: Uint8Array): string {\n const nonce = nacl.randomBytes(NONCE_LENGTH);\n const box = nacl.secretbox(encoder.encode(JSON.stringify(message)), nonce, key);\n return toBase64(concat(nonce, box));\n}\n\n// Returns null on any tamper/garbage/wrong-key input — caller MUST surface it, not drop it.\nexport function open(payload: string, key: Uint8Array): unknown | null {\n const bytes = fromBase64(payload);\n if (bytes === null || bytes.length < NONCE_LENGTH + nacl.secretbox.overheadLength) return null;\n const box = nacl.secretbox.open(bytes.slice(NONCE_LENGTH), bytes.slice(0, NONCE_LENGTH), key);\n if (box === null) return null;\n try {\n return JSON.parse(decoder.decode(box));\n } catch {\n return null;\n }\n}\n\nexport function encodePairing(info: PairingInfo): string {\n const json = JSON.stringify({\n v: 1,\n relay: info.relay,\n secret: toBase64Url(info.secret),\n machine: info.machine,\n });\n return toBase64Url(encoder.encode(json));\n}\n\nexport function decodePairing(code: string): PairingInfo | null {\n const bytes = fromBase64Url(code.trim());\n if (bytes === null) return null;\n try {\n const parsed = JSON.parse(decoder.decode(bytes)) as {\n v?: number;\n relay?: string;\n secret?: string;\n machine?: string;\n };\n if (parsed.v !== 1 || !parsed.relay || !parsed.secret || !parsed.machine) return null;\n const secret = fromBase64Url(parsed.secret);\n if (secret === null || secret.length !== 32) return null;\n return { relay: parsed.relay, secret, machine: parsed.machine };\n } catch {\n return null;\n }\n}\n","// Payload limits from docs/PROTOCOL.md. The sender truncates BEFORE encryption.\n// The relay imports THIS FILE by subpath (src/limits.js) to keep tweetnacl out of its\n// bundle — if this package ever gains a package.json \"exports\" map, add a matching entry.\n\nexport const MAX_ENVELOPE_BYTES = 16 * 1024;\nexport const MAX_DETAIL_CHARS = 8 * 1024;\nexport const MAX_SUMMARY_CHARS = 4 * 1024;\n// Char limits alone don't bound the envelope: CJK is ~3 bytes/char in UTF-8.\n// Free-text fields are clamped by chars first, then by bytes (clampBytes) before seal.\nexport const MAX_PROMPT_CHARS = 8 * 1024;\nexport const MAX_PROMPT_BYTES = 10 * 1024;\nexport const MAX_FIELD_BYTES = 10 * 1024;\nexport const MAX_PUSH_BYTES = 3 * 1024;\nexport const TRUNCATION_SUFFIX = '…[truncated]';\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) return text;\n return text.slice(0, maxChars - TRUNCATION_SUFFIX.length) + TRUNCATION_SUFFIX;\n}\n\nconst byteEncoder = new TextEncoder();\n\nexport function utf8Length(text: string): number {\n return byteEncoder.encode(text).length;\n}\n\n// Clamp to a UTF-8 byte budget without splitting a multi-byte char or surrogate pair.\nexport function clampBytes(text: string, maxBytes: number): string {\n if (utf8Length(text) <= maxBytes) return text;\n const budget = maxBytes - utf8Length(TRUNCATION_SUFFIX);\n let low = 0;\n let high = text.length;\n while (low < high) {\n const mid = Math.ceil((low + high) / 2);\n if (utf8Length(text.slice(0, mid)) <= budget) low = mid;\n else high = mid - 1;\n }\n const tail = text.charCodeAt(low - 1);\n if (tail >= 0xd800 && tail <= 0xdbff) low -= 1;\n return text.slice(0, low) + TRUNCATION_SUFFIX;\n}\n","/**\n * The desktop-side surfaces: pairing, re-pairing, and the status line that\n * makes a broken link visible without leaving dsh (docs/PRODUCT.md 失败可见).\n */\n\nimport type { CommandResult } from '@deepseek-ai/dsh-commands';\nimport type { Hub } from './hub.js';\nimport type { Routers } from './index.js';\n\nexport type CommandDeps = Routers;\n\nexport function installCommands(hub: Hub, deps: CommandDeps): void {\n hub.ctx.effect(() => hub.ctx.commands.register({\n name: 'dispatch-pair',\n description: 'Show the dsh-dispatch pairing code for this machine',\n handler: () => pairingResult(hub),\n }), 'dsh-dispatch: /dispatch-pair');\n\n hub.ctx.effect(() => hub.ctx.commands.register({\n name: 'dispatch-repair',\n description: 'Generate a new dsh-dispatch pairing secret (invalidates every paired phone)',\n handler: () => {\n hub.relay.rekey(hub.pairing.regenerate());\n hub.log.warn('pairing secret regenerated; every previously paired phone is now locked out');\n return pairingResult(hub, '旧配对码已作废,请在每台手机上重新扫码。\\n\\n');\n },\n }), 'dsh-dispatch: /dispatch-repair');\n\n hub.ctx.effect(() => hub.ctx.commands.register({\n name: 'dispatch-status',\n description: 'Show the dsh-dispatch relay connection and remote session state',\n handler: () => ({ kind: 'success', text: statusText(hub, deps) }),\n }), 'dsh-dispatch: /dispatch-status');\n}\n\nfunction pairingResult(hub: Hub, prefix = ''): CommandResult {\n if (hub.config.relay === '') {\n return {\n kind: 'error',\n text: 'relay 未配置:请在 profile 的 cordis.patch.yml 中为 dsh-dispatch 设置 relay: \\'wss://…\\'',\n };\n }\n const code = hub.pairing.pairingCode(hub.config.relay, hub.config.machineName);\n return {\n kind: 'success',\n text: `${prefix}配对码(${hub.config.machineName}):\\n${code}\\n\\n`\n + `或在手机上打开:\\n${hub.config.pwaUrl}/#pair=${code}\\n\\n`\n + '⚠️ 此码等同于本机的控制权:持有者可以批准工具调用并在允许目录中派发任务。'\n + '不要发到群里或截图外传;一旦泄露立即运行 /dispatch-repair。',\n };\n}\n\nfunction statusText(hub: Hub, deps: CommandDeps): string {\n const status = hub.relay.status();\n const link = hub.config.relay === ''\n ? '未配置(请设置 relay)'\n : status.connected\n ? `已连接 ${hub.config.relay}`\n : status.gaveUp\n ? `连接失败 ${String(status.consecutiveFailures)} 次,仍在每 60s 重试 — 请检查 relay`\n : `未连接(重试中,已失败 ${String(status.consecutiveFailures)} 次)`;\n return [\n `relay: ${link}`,\n `房间: ${status.room.slice(0, 8)}…`,\n `已配对: ${hub.pairing.everPaired ? 'yes' : 'no'}`\n + `${status.phoneOnline ? '(手机在线)' : '(手机离线)'}`,\n `待批审批: ${String(deps.approvals.openCount())}`,\n `待答提问: ${String(deps.questions.openCount())}`,\n `派发会话: ${String(deps.dispatch.dispatchedCount())}`,\n `full-access 派发: ${hub.config.allowFullAccessDispatch ? '已开启 ⚠️' : '已禁用'}`,\n `解密失败: ${String(status.decryptFailures)}`\n + `${status.decryptFailures > 0 ? ' — 密钥不匹配,请重新配对' : ''}`,\n `allowedRoots: ${hub.config.allowedRoots.length === 0\n ? '(空 — 远程派任务已禁用)'\n : hub.config.allowedRoots.join(', ')}`,\n ].join('\\n');\n}\n","/**\n * Remote dispatch — the M2 core. Starts agent sessions on request from the\n * phone, guards the working directory, and reports the final assistant text\n * when a dispatched turn closes.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { isAbsolute, resolve, sep } from 'node:path';\nimport type { Context } from '@deepseek-ai/cordis';\nimport { installModelSelection } from '@deepseek-ai/dsh-agent';\nimport type { AgentHandle, ModelSelection } from '@deepseek-ai/dsh-agent';\nimport type {} from '@deepseek-ai/dsh-agent-default-model';\n// Carries the `ctx.agentPresets` Context merge; the roster is optional at\n// runtime, so it is read with ctx.get rather than declared in `inject`.\nimport type {} from '@deepseek-ai/dsh-agent-presets';\n// Carries the `ctx.permissionPresets` Context merge.\nimport type {} from '@deepseek-ai/dsh-permission-presets';\nimport { createUserMessage } from '@deepseek-ai/dsh-llm';\nimport { SessionId } from '@deepseek-ai/dsh-session';\nimport type { SurfaceEvent } from '@deepseek-ai/dsh-session';\nimport {\n clampBytes,\n MAX_FIELD_BYTES,\n MAX_PROMPT_BYTES,\n MAX_PROMPT_CHARS,\n MAX_SUMMARY_CHARS,\n truncate,\n utf8Length,\n} from '@dsh-dispatch/shared';\nimport type { DispatchRequestMsg, DispatchResultMsg, SessionMessageMsg } from '@dsh-dispatch/shared';\nimport type { Hub } from './hub.js';\nimport { expandHome } from './pairing.js';\nimport { prepareWorkspace, WorktreeError } from './worktree.js';\n\n/** docs/PROTOCOL.md: LRU of seen request ids, for double-tap safety. */\nconst SEEN_LIMIT = 256;\n\nconst DISPATCH_DISABLED = 'dispatch 未启用:请在插件配置中设置 allowedRoots';\nconst NO_REMOTE_FOLLOWUP = '该会话不支持远程追加';\nconst FULL_ACCESS_DISABLED = 'full-access 派发未启用:请在插件配置中开启 allowFullAccessDispatch';\nconst FULL_ACCESS_UNAVAILABLE = 'full-access 不可用:本机 dsh 未组合 permission-presets 插件';\n/** dsh's shipped preset table key (permission-presets/src/index.ts:188). */\nconst FULL_ACCESS_PRESET = 'danger-full-access';\n\nexport interface DispatchRouter {\n request(message: DispatchRequestMsg): Promise<void>;\n followup(message: SessionMessageMsg): Promise<void>;\n dispatchedCount(): number;\n}\n\n/** Per-plugin dispatch state; kept in one object so the handlers stay flat. */\ninterface DispatchState {\n readonly handles: Map<string, AgentHandle>;\n readonly seen: ResultCache;\n readonly inflight: Map<string, Promise<DispatchResultMsg>>;\n loaderReady: Promise<void> | undefined;\n}\n\nexport function installDispatch(hub: Hub): DispatchRouter {\n const state: DispatchState = {\n handles: new Map(),\n seen: new ResultCache(),\n inflight: new Map(),\n loaderReady: undefined,\n };\n hub.ctx.on('agent/turn-stopping', ({ agent }) => {\n if (!hub.sessions.isDispatched(agent.id)) return;\n // Latch the card synchronously: the idle status that closes this turn\n // races the async surface read below and would otherwise land first.\n hub.sessions.markTurnComplete(agent.id);\n void reportTurnFinal(hub, agent.id).catch((error: unknown) => {\n hub.fail(`turn.final for ${agent.id}`, error);\n });\n });\n hub.ctx.on('agent/disposed', ({ agent }) => { state.handles.delete(agent.id); });\n return {\n request: message => handleRequest(hub, state, message),\n followup: message => handleFollowup(hub, state, message),\n dispatchedCount: () => state.handles.size,\n };\n}\n\n/** Answer exactly once per requestId, even under a double-tap. */\nasync function handleRequest(\n hub: Hub,\n state: DispatchState,\n message: DispatchRequestMsg,\n): Promise<void> {\n // Pure validation runs BEFORE the idempotency mark: malformed input can\n // never half-apply, so a retry deserves the same visible complaint rather\n // than a cached one.\n const rejected = validate(hub, message);\n if (rejected !== undefined) {\n hub.relay.send(failure(message.requestId, rejected));\n return;\n }\n const replay = state.seen.get(message.requestId);\n if (replay !== undefined) {\n if (replay !== null) hub.relay.send(replay);\n return;\n }\n const pending = state.inflight.get(message.requestId) ?? startSession(hub, state, message);\n state.inflight.set(message.requestId, pending);\n try {\n const result = await pending;\n state.seen.set(message.requestId, result);\n hub.relay.send(result);\n } finally {\n state.inflight.delete(message.requestId);\n }\n}\n\nasync function startSession(\n hub: Hub,\n state: DispatchState,\n message: DispatchRequestMsg,\n): Promise<DispatchResultMsg> {\n const prompt = message.prompt;\n const target = resolveCwd(hub, message.cwd);\n if (typeof target !== 'string') return failure(message.requestId, target.error);\n const full = message.access === 'full';\n try {\n const workspace = await prepareWorkspace({\n cwd: target,\n dataDir: hub.pairing.dataDir,\n worktree: message.worktree,\n });\n // Loader siblings mount concurrently; never create an agent into a\n // half-composed application (dossier §3.2).\n state.loaderReady ??= Promise.resolve(hub.ctx.get('loader')?.await()).then(() => undefined);\n await state.loaderReady;\n const created = await createSession(hub.ctx, workspace.cwd, prompt, full);\n state.handles.set(created.handle.agent.id, created.handle);\n // Mark access from the REQUEST, never gated behind the preset call that may\n // have thrown: an audit mark that vanishes on failure is worse than useless.\n hub.sessions.markDispatched(created.handle.agent.id, prompt, full ? 'full' : 'standard');\n if (created.fullAccessError !== undefined) {\n // The turn still started (fail-open), but the preset did not stick — say\n // so loudly rather than run at standard while the card claims full.\n hub.report(`dispatch ${message.requestId}`, `完全访问权限未能应用:${created.fullAccessError}`);\n }\n const result = ok(message.requestId, created.handle.agent.id);\n // `note` is advisory on a successful result; `error` never appears with\n // ok: true, so the phone can key its failure state on `ok` alone.\n const advisory = created.fullAccessError === undefined\n ? workspace.note\n : `完全访问权限未能应用,已在标准权限下运行:${created.fullAccessError}`;\n return advisory === undefined ? result : { ...result, note: advisory };\n } catch (error) {\n const detail = error instanceof WorktreeError ? error.message : String(error);\n hub.log.error('dispatch %s failed: %s', message.requestId, detail);\n return failure(message.requestId, detail);\n }\n}\n\n/**\n * Append a phone-typed message to a live session. Cold (persisted-only)\n * sessions are refused out loud rather than silently resumed.\n */\nasync function handleFollowup(\n hub: Hub,\n state: DispatchState,\n message: SessionMessageMsg,\n): Promise<void> {\n // Malformed input can never double-apply, so it is answered before the\n // idempotency mark: a retry deserves the same visible complaint.\n const oversized = overLimit('消息', message.text);\n if (oversized !== undefined) {\n hub.report(`session.message ${message.sessionId}`, oversized);\n return;\n }\n if (state.seen.get(message.requestId) !== undefined) return;\n state.seen.set(message.requestId, null);\n const agent = state.handles.get(message.sessionId)?.agent\n ?? hub.sessions.agentOf(message.sessionId);\n if (agent === undefined) {\n hub.report(`session.message ${message.sessionId}`, NO_REMOTE_FOLLOWUP);\n return;\n }\n agent.followup(createUserMessage({\n content: [{ type: 'text', text: message.text }],\n source: { kind: 'user' },\n }));\n}\n\n/**\n * Reject over-limit inbound text instead of clamping it. A well-behaved phone\n * clamps client-side (docs/PROTOCOL.md), so an oversized prompt is malformed\n * input — silently trimming it would run a task the user never wrote.\n * @returns the visible complaint, or undefined when the text is within limits.\n */\nfunction overLimit(field: string, text: string): string | undefined {\n const tail = '请在手机端缩短后重发。';\n if (text.length > MAX_PROMPT_CHARS) {\n return `${field} 超长:${String(text.length)} 字符,上限 ${String(MAX_PROMPT_CHARS)} 字符。${tail}`;\n }\n const bytes = utf8Length(text);\n if (bytes > MAX_PROMPT_BYTES) {\n return `${field} 超长:${String(bytes)} 字节(UTF-8),上限 ${String(MAX_PROMPT_BYTES)} 字节。${tail}`;\n }\n return undefined;\n}\n\n/**\n * Compose the agent's model-facing world: the model selection, and — when the\n * deployment runs a preset roster — the preset that carries its tools.\n *\n * Mirrors `composeAgent()` in packages/host/apiproxy/src/api-proxy.ts:1168,\n * which is what a user-created web session goes through. Skipping the mount is\n * not a SMALLER toolset, it is NO toolset: the web bundle disables every\n * model-facing row in the host plane (`tool-bash`, `tool-fs`, `tool-todo`, …\n * all `disabled: true` in packages/bundle/web-app/cordis.patch.yml) and moves\n * them into each preset's own scope layer, so an agent that joins no preset\n * reaches the model with an empty tool registry and no persona sections.\n */\nasync function composeAgent(ctx: Context, selection: ModelSelection | undefined): Promise<{\n agentPreset?: string;\n setup: (agentCtx: Context) => Promise<void>;\n}> {\n const install = (agentCtx: Context): void => {\n if (selection === undefined) return;\n installModelSelection(agentCtx, { current: selection, assembled: undefined });\n };\n const presets = ctx.get('agentPresets');\n // No roster composed (the headless shape): model-facing rows sit in the host\n // plane and the agent reads them from the global layer.\n if (presets === undefined) {\n return { setup: (agentCtx: Context) => { install(agentCtx); return Promise.resolve(); } };\n }\n // Name no preset, exactly as when a web client creates a session without\n // picking one: the roster's configured default wins.\n const resolvedId = (await presets.resolve()).id;\n return {\n agentPreset: resolvedId,\n setup: async (agentCtx: Context) => {\n install(agentCtx);\n await presets.mount(agentCtx, resolvedId);\n },\n };\n}\n\n/**\n * Everything we can refuse without touching the filesystem or the registry.\n * @returns the visible complaint, or undefined when the request is well formed.\n */\nfunction validate(hub: Hub, message: DispatchRequestMsg): string | undefined {\n const oversized = overLimit('prompt', message.prompt);\n if (oversized !== undefined) return oversized;\n if (message.access !== undefined && message.access !== 'standard' && message.access !== 'full') {\n return `access 取值无效:${String(message.access)}`;\n }\n if (message.access !== 'full') return undefined;\n if (!hub.config.allowFullAccessDispatch) return FULL_ACCESS_DISABLED;\n // Refusing here beats running the task at standard permissions while the\n // phone believes it asked for full access.\n if (hub.ctx.get('permissionPresets') === undefined) return FULL_ACCESS_UNAVAILABLE;\n return undefined;\n}\n\n/** Outcome of one session creation: the handle, and any full-access snag. */\ninterface CreatedSession {\n readonly handle: AgentHandle;\n /** Set when the danger-full-access preset could not be applied. */\n readonly fullAccessError?: string;\n}\n\n/** Create a session in `cwd` with the same composition a web session gets. */\nasync function createSession(\n ctx: Context,\n cwd: string,\n prompt: string,\n fullAccess: boolean,\n): Promise<CreatedSession> {\n const selection = ctx.get('agentDefaultModel')?.currentSelection();\n const composition = await composeAgent(ctx, selection);\n const handle = await ctx.agents.create({\n sessionId: SessionId(`session-${randomUUID()}`),\n // The preset is recorded on the header so a later cold resume rebuilds the\n // composition this session's history was actually produced under.\n meta: composition.agentPreset === undefined\n ? { cwd }\n : { cwd, agentPreset: composition.agentPreset },\n agentOptions: selection === undefined\n ? undefined\n : { provider: selection.provider, model: selection.model },\n setup: composition.setup,\n });\n await handle.agent.whenIdle();\n // Apply the tier BEFORE the first turn — but never let it skip the followup:\n // a thrown set() previously wedged the session with only setup events and no\n // turn (the acceptance-test repro). Fail-open on the turn.\n const fullAccessError = fullAccess ? applyFullAccess(ctx, handle) : undefined;\n handle.agent.followup(createUserMessage({\n content: [{ type: 'text', text: prompt }],\n source: { kind: 'user' },\n }));\n return fullAccessError === undefined ? { handle } : { handle, fullAccessError };\n}\n\n/**\n * Switch a freshly created session to danger-full-access.\n *\n * Resolved via `ctx.get()`, NOT the `ctx.permissionPresets` property proxy: an\n * un-injected service read through the proxy from this nested fiber throws\n * `cannot get property \"permissionPresets\" without inject`\n * (vendor/cordis/src/reflect.ts:144), while `get()` reads the store directly\n * (reflect.ts:233). `set()` then appends the durable `permission/preset` event\n * and drives the sandbox + approval knobs\n * (packages/interaction/permission-presets/src/index.ts:391).\n * @returns undefined on success, or a message describing why it could not apply.\n */\nfunction applyFullAccess(ctx: Context, handle: AgentHandle): string | undefined {\n try {\n const presets = ctx.get('permissionPresets');\n if (presets === undefined) return 'permission-presets 插件未组合';\n presets.set(handle.agent.session, FULL_ACCESS_PRESET);\n return undefined;\n } catch (error) {\n return error instanceof Error ? error.message : String(error);\n }\n}\n\n/**\n * Validate the requested cwd against the configured roots.\n * @returns the absolute directory, or the visible failure to send back.\n */\nfunction resolveCwd(hub: Hub, requested: string | undefined): string | { error: string } {\n const roots = hub.config.allowedRoots.map(root => resolve(expandHome(root)));\n if (roots.length === 0) return { error: DISPATCH_DISABLED };\n const first = roots[0];\n if (requested === undefined) return first === undefined ? { error: DISPATCH_DISABLED } : first;\n const target = resolve(isAbsolute(requested) ? requested : expandHome(requested));\n const inside = roots.some(root => target === root || target.startsWith(root + sep));\n return inside ? target : { error: `cwd 不在允许目录内:${target}` };\n}\n\nasync function reportTurnFinal(hub: Hub, sessionId: string): Promise<void> {\n const snapshot = await hub.ctx.sessionQuery.readSurface(SessionId(sessionId));\n const ok = !hub.sessions.hasError(sessionId);\n const summary = clampBytes(\n truncate(lastAssistantText(snapshot.events), MAX_SUMMARY_CHARS),\n MAX_FIELD_BYTES,\n );\n hub.relay.send(\n { v: 1, ts: Date.now(), type: 'turn.final', sessionId, ok, summary },\n {\n v: 1,\n ts: Date.now(),\n type: 'push',\n kind: ok ? 'done' : 'error',\n sessionId,\n title: truncate(summary.split('\\n', 1)[0] ?? '', 120),\n },\n );\n}\n\nfunction lastAssistantText(events: readonly SurfaceEvent[]): string {\n for (let index = events.length - 1; index >= 0; index -= 1) {\n const event = events[index];\n if (event?.type !== 'assistant/message') continue;\n const text = event.data.message.content\n .filter(block => block.type === 'text')\n .map(block => block.text)\n .join('')\n .trim();\n if (text !== '') return text;\n }\n return '(本轮没有产生助手文本)';\n}\n\nfunction ok(requestId: string, sessionId: string): DispatchResultMsg {\n return { v: 1, ts: Date.now(), type: 'dispatch.result', requestId, ok: true, sessionId };\n}\n\nfunction failure(requestId: string, error: string): DispatchResultMsg {\n return { v: 1, ts: Date.now(), type: 'dispatch.result', requestId, ok: false, error };\n}\n\n/** Bounded replay cache. `null` marks a request that succeeded with no reply frame. */\nclass ResultCache {\n readonly #entries = new Map<string, DispatchResultMsg | null>();\n\n get(requestId: string): DispatchResultMsg | null | undefined {\n return this.#entries.get(requestId);\n }\n\n set(requestId: string, value: DispatchResultMsg | null): void {\n this.#entries.delete(requestId);\n this.#entries.set(requestId, value);\n while (this.#entries.size > SEEN_LIMIT) {\n const oldest = this.#entries.keys().next();\n if (oldest.done === true) break;\n this.#entries.delete(oldest.value);\n }\n }\n}\n","/**\n * Pairing secret lifecycle: the 32 random bytes that are the ONLY thing\n * standing between a stranger and control of this machine. Stored at\n * `<dataDir>/secret` with mode 0600 and never sent to the relay.\n */\n\nimport { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\nimport { encodePairing, fromBase64, generateSecret, toBase64 } from '@dsh-dispatch/shared';\nimport type { Log } from './log.js';\n\nconst SECRET_FILE = 'secret';\nconst PAIRED_FILE = 'paired.json';\nconst SECRET_BYTES = 32;\n\n/** Expand a leading `~` so a configured `~/.dsh-dispatch` resolves to a real path. */\nexport function expandHome(input: string): string {\n if (input === '~') return homedir();\n if (input.startsWith('~/')) return join(homedir(), input.slice(2));\n return resolve(input);\n}\n\n/** Everything the plugin needs to prove a phone is allowed to talk to it. */\nexport class PairingStore {\n readonly dataDir: string;\n #secret: Uint8Array;\n #pairedAt: number | undefined;\n\n private constructor(dataDir: string, secret: Uint8Array, pairedAt: number | undefined) {\n this.dataDir = dataDir;\n this.#secret = secret;\n this.#pairedAt = pairedAt;\n }\n\n /** Load the stored secret, generating and persisting one on first start. */\n static open(dataDir: string, log: Log): PairingStore {\n const dir = expandHome(dataDir);\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n const secretPath = join(dir, SECRET_FILE);\n let secret: Uint8Array | null = null;\n if (existsSync(secretPath)) {\n secret = fromBase64(readFileSync(secretPath, 'utf8').trim());\n if (secret === null || secret.length !== SECRET_BYTES) {\n throw new Error(\n `dsh-dispatch: ${secretPath} is not a valid pairing secret. `\n + 'Delete the file to generate a new one (this invalidates existing pairings).',\n );\n }\n } else {\n secret = generateSecret();\n writeSecret(secretPath, secret);\n }\n return new PairingStore(dir, secret, readPairedAt(join(dir, PAIRED_FILE), log));\n }\n\n get secret(): Uint8Array {\n return this.#secret;\n }\n\n /**\n * Whether a phone has ever completed a pairing with this machine. Approval\n * forwarding stays out of the way entirely until this is true, so an\n * unpaired machine pays zero added approval latency.\n */\n get everPaired(): boolean {\n return this.#pairedAt !== undefined;\n }\n\n get pairedAt(): number | undefined {\n return this.#pairedAt;\n }\n\n /** Record the first sighting of a phone in this room. Idempotent. */\n markPaired(): void {\n if (this.#pairedAt !== undefined) return;\n this.#pairedAt = Date.now();\n writeFileSync(join(this.dataDir, PAIRED_FILE), JSON.stringify({ pairedAt: this.#pairedAt }), {\n mode: 0o600,\n });\n }\n\n /** Replace the secret, invalidating every existing pairing. */\n regenerate(): Uint8Array {\n this.#secret = generateSecret();\n writeSecret(join(this.dataDir, SECRET_FILE), this.#secret);\n this.#pairedAt = undefined;\n rmSync(join(this.dataDir, PAIRED_FILE), { force: true });\n return this.#secret;\n }\n\n /** The out-of-band pairing payload: relay + secret + machine name. */\n pairingCode(relay: string, machine: string): string {\n return encodePairing({ relay, secret: this.#secret, machine });\n }\n}\n\nfunction writeSecret(path: string, secret: Uint8Array): void {\n writeFileSync(path, toBase64(secret), { mode: 0o600 });\n // writeFileSync only applies `mode` when it creates the file; a rewrite of an\n // existing loose-permission file would otherwise stay world-readable.\n chmodSync(path, 0o600);\n}\n\nfunction readPairedAt(path: string, log: Log): number | undefined {\n if (!existsSync(path)) return undefined;\n try {\n const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));\n const value = (parsed as { pairedAt?: unknown }).pairedAt;\n return typeof value === 'number' ? value : undefined;\n } catch (error) {\n // A corrupt marker only costs us the \"have we ever paired\" shortcut; the\n // next phone connection rewrites it. Loud, then treat as never paired.\n log.error('pairing: %s is unreadable, treating this machine as never paired: %s', path, error);\n return undefined;\n }\n}\n","/**\n * Git worktree preparation for dispatched tasks.\n *\n * v0 never deletes a worktree (docs/PRODUCT.md 防呆): cleanup is the user's\n * call, because guessing wrong destroys real work.\n */\n\nimport { execFile } from 'node:child_process';\nimport { randomBytes } from 'node:crypto';\nimport { join } from 'node:path';\nimport { promisify } from 'node:util';\n\nconst run = promisify(execFile);\n\n/** docs/PROTOCOL.md: a failure result carries at most 500 chars of stderr. */\nconst STDERR_TAIL = 500;\n\nexport class WorktreeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'WorktreeError';\n }\n}\n\nexport interface Workspace {\n /** Directory the agent will actually run in. */\n readonly cwd: string;\n /** Branch created for this task, absent when running in `cwd` directly. */\n readonly branch?: string;\n /** Human-readable degradation note the phone must show, when degraded. */\n readonly note?: string;\n}\n\nexport interface PrepareOptions {\n readonly cwd: string;\n readonly dataDir: string;\n readonly worktree: boolean;\n}\n\n/**\n * Resolve the directory a dispatched task runs in.\n * @throws WorktreeError when `git worktree add` fails; the message carries the stderr tail.\n */\nexport async function prepareWorkspace(options: PrepareOptions): Promise<Workspace> {\n if (!options.worktree) return { cwd: options.cwd };\n if (!await isGitRepo(options.cwd)) {\n return {\n cwd: options.cwd,\n note: `已降级:${options.cwd} 不是 git 仓库,任务直接在该目录运行(未创建 worktree)`,\n };\n }\n const root = join(options.dataDir, 'worktrees');\n try {\n return await addWorktree(options.cwd, root, freshSlug());\n } catch (error) {\n const stderr = stderrOf(error);\n if (!isCollision(stderr)) throw new WorktreeError(stderr);\n return await addWorktree(options.cwd, root, freshSlug()).catch((retry: unknown) => {\n throw new WorktreeError(stderrOf(retry));\n });\n }\n}\n\nasync function addWorktree(cwd: string, root: string, slug: string): Promise<Workspace> {\n const target = join(root, slug);\n const branch = `dsh-dispatch/${slug}`;\n await run('git', ['-C', cwd, 'worktree', 'add', target, '-b', branch]);\n return { cwd: target, branch };\n}\n\nasync function isGitRepo(cwd: string): Promise<boolean> {\n try {\n const { stdout } = await run('git', ['-C', cwd, 'rev-parse', '--is-inside-work-tree']);\n return stdout.trim() === 'true';\n } catch {\n // Not a repo, or git is missing. Both mean \"no worktree here\"; the caller\n // reports the degradation to the phone, so this is not a silent swallow.\n return false;\n }\n}\n\n/** `timestamp + short random` — sortable, collision-resistant, readable in `git worktree list`. */\nfunction freshSlug(): string {\n const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\\..+$/, '');\n return `${stamp}-${randomBytes(3).toString('hex')}`;\n}\n\nfunction isCollision(stderr: string): boolean {\n return /already exists|already used by worktree|already checked out/i.test(stderr);\n}\n\nfunction stderrOf(error: unknown): string {\n const raw = (error as { stderr?: unknown }).stderr;\n const text = typeof raw === 'string' && raw.trim() !== ''\n ? raw.trim()\n : error instanceof Error ? error.message : String(error);\n return text.length <= STDERR_TAIL ? text : text.slice(-STDERR_TAIL);\n}\n","/**\n * The shared runtime handle every feature module receives. Also the single\n * place a failure becomes visible: `fail()` logs on the desktop AND pushes an\n * `error` message to the phone, so no catch can quietly swallow anything.\n */\n\nimport type { Context } from '@deepseek-ai/cordis';\nimport { truncate } from '@dsh-dispatch/shared';\nimport type { Config } from './config.js';\nimport type { Log } from './log.js';\nimport type { PairingStore } from './pairing.js';\nimport type { RelayClient } from './relay-client.js';\nimport type { SessionTracker } from './sessions.js';\n\n/** Phone screens are small; a wall of stack trace helps nobody there. */\nconst ERROR_CHARS = 1_000;\n\nexport interface Hub {\n readonly ctx: Context;\n readonly config: Config;\n readonly log: Log;\n readonly relay: RelayClient;\n readonly sessions: SessionTracker;\n readonly pairing: PairingStore;\n /** Report a caught error on both surfaces. */\n fail(context: string, error: unknown): void;\n /** Report an already human-readable failure on both surfaces. */\n report(context: string, message: string): void;\n}\n\nexport function createHub(parts: Omit<Hub, 'fail' | 'report'>): Hub {\n const report = (context: string, message: string): void => {\n parts.log.error('%s: %s', context, message);\n parts.relay.send({\n v: 1,\n ts: Date.now(),\n type: 'error',\n message: truncate(message, ERROR_CHARS),\n context,\n });\n };\n return {\n ...parts,\n report,\n fail(context: string, error: unknown): void {\n report(context, error instanceof Error ? error.message : String(error));\n },\n };\n}\n","/**\n * `ask_user_question` forwarding.\n *\n * Seam: the `tools/execute` around-dispatch waterfall, NOT `tools/pre-execute`.\n * `PreToolDecision` is only allow/deny/ask (packages/core/tools/src/index.ts:588)\n * and cannot carry a tool result, so it can never answer the question — while a\n * `tools/execute` listener returns a `ToolExecutionResult` directly\n * (index.ts:163) and its `next()` runs the real tool body, which is exactly the\n * approval race shape.\n *\n * Authoring only `value` is enough: `normalizeDispatchResult`\n * (packages/core/tools/src/index.ts:1401) pushes a wrapper's result back\n * through the owning tool's `output.schema` and `output.render`, so a\n * phone-answered call is indistinguishable from a locally answered one.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { ToolDispatchExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools';\nimport { clampBytes, truncate } from '@dsh-dispatch/shared';\nimport type {\n PushCompactMsg,\n QuestionAnswer,\n QuestionItem,\n QuestionOption,\n QuestionRequestMsg,\n QuestionRespondMsg,\n QuestionResolution,\n} from '@dsh-dispatch/shared';\nimport type { Hub } from './hub.js';\n\n/** Registered name of the tool this module answers for. */\nconst ASK_USER_QUESTION = 'ask_user_question';\n\nconst PROMPT_CHARS = 2_000;\nconst PROMPT_BYTES = 4_000;\nconst LABEL_CHARS = 300;\nconst LABEL_BYTES = 600;\nconst REMINDER_MS = 5 * 60_000;\nconst MAX_REMINDERS = 6;\n\n/** The dsh tool's argument shape (packages/interaction/tool-ask-user/src/index.ts). */\ninterface RawOption {\n label: string;\n description?: string;\n}\ninterface RawQuestion {\n id: string;\n question: string;\n header?: string;\n options?: RawOption[];\n multi_select?: boolean;\n}\n\n/**\n * The tool's canonical return value; `selected` holds option LABELS.\n * A type alias, not an interface, so it satisfies `JsonValue`'s index\n * signature when handed back as a `ToolExecutionSuccess.value`.\n */\ntype AnswerValue = {\n answers: { id: string; selected: string[]; custom?: string }[];\n};\n\ntype Settle = (result: ToolExecutionResult, resolution: QuestionResolution) => void;\n\ninterface Forwarded {\n readonly questionId: string;\n readonly sessionId: string;\n readonly frame: QuestionRequestMsg;\n readonly push: PushCompactMsg;\n /** itemId → wire optionId → the exact dsh label to echo back. */\n readonly labels: Map<string, Map<string, string>>;\n}\n\ninterface Pending extends Forwarded {\n readonly settle: Settle;\n reminders: number;\n timer: ReturnType<typeof setTimeout> | undefined;\n}\n\nexport interface QuestionRouter {\n respond(message: QuestionRespondMsg): void;\n resendPending(): void;\n openCount(): number;\n}\n\nexport function installQuestions(hub: Hub): QuestionRouter {\n const pending = new Map<string, Pending>();\n\n hub.ctx.on('tools/execute', (exec, next) => {\n if (exec.name !== ASK_USER_QUESTION || exec.agent === undefined) return next();\n if (!hub.pairing.everPaired || !hub.relay.connected) return next();\n if (exec.signal.aborted) return next();\n return forward(hub, pending, exec, next);\n });\n\n return {\n respond(message: QuestionRespondMsg): void {\n const entry = pending.get(message.questionId);\n if (entry === undefined) {\n hub.relay.send(closedMsg(message.questionId, 'expired'));\n return;\n }\n const value = toAnswerValue(entry, message.answers);\n if (typeof value === 'string') {\n // Malformed input: the question stays OPEN so a corrected answer can\n // still land, and the phone is told exactly what was wrong.\n hub.report(`question ${entry.questionId}`, value);\n return;\n }\n entry.settle(successResult(value), 'phone');\n },\n resendPending(): void {\n for (const entry of pending.values()) hub.relay.send(entry.frame, entry.push);\n },\n openCount: () => pending.size,\n };\n}\n\nfunction forward(\n hub: Hub,\n pending: Map<string, Pending>,\n exec: ToolDispatchExecution,\n next: () => Promise<ToolExecutionResult>,\n): Promise<ToolExecutionResult> {\n const normalized = normalize(exec.arguments);\n if (normalized === null) {\n // Not a shape we can represent: let the real tool run and complain, rather\n // than answering a question we did not understand.\n hub.report('question', 'ask_user_question 参数无法解析,已交回本机处理');\n return next();\n }\n const questionId = randomUUID();\n const sessionId = exec.agent?.id ?? '';\n const frame: QuestionRequestMsg = {\n v: 1, ts: Date.now(), type: 'question.request',\n questionId, sessionId, items: normalized.items, createdAt: Date.now(),\n };\n const push: PushCompactMsg = {\n v: 1, ts: Date.now(), type: 'push', kind: 'question', sessionId,\n title: normalized.items[0]?.prompt.slice(0, 120) ?? '需要你回答一个问题',\n };\n if (!hub.relay.send(frame, push)) {\n hub.report('question', '提问未能推送到手机(relay 未连接或内容过大),已交回本机处理');\n return next();\n }\n const forwarded: Forwarded = { questionId, sessionId, frame, push, labels: normalized.labels };\n return race(hub, pending, forwarded, exec, next);\n}\n\nfunction race(\n hub: Hub,\n pending: Map<string, Pending>,\n forwarded: Forwarded,\n exec: ToolDispatchExecution,\n next: () => Promise<ToolExecutionResult>,\n): Promise<ToolExecutionResult> {\n const { questionId } = forwarded;\n return new Promise<ToolExecutionResult>((resolve) => {\n let settled = false;\n let localFailure: ToolExecutionResult | undefined;\n const settle: Settle = (result, resolution) => {\n if (settled) return;\n settled = true;\n const entry = pending.get(questionId);\n if (entry?.timer !== undefined) clearTimeout(entry.timer);\n pending.delete(questionId);\n exec.signal.removeEventListener('abort', onAbort);\n hub.relay.send(closedMsg(questionId, resolution));\n resolve(result);\n };\n function onAbort(): void {\n settle(localFailure ?? abortedResult(), 'cancelled');\n }\n exec.signal.addEventListener('abort', onAbort, { once: true });\n const entry: Pending = { ...forwarded, settle, reminders: 0, timer: undefined };\n pending.set(questionId, entry);\n armReminder(hub, entry);\n // Only a SUCCESS counts as a local answer. The tool body never rejects —\n // it returns an error result (packages/core/tools/src/index.ts:1554) — so a\n // missing provider (NO_PROVIDER, upstream #2544) or an abort lands here as\n // isError, and the phone stays the live answerer.\n void next().then((result) => {\n if (!result.isError) settle(result, 'local');\n else localFailure = result;\n }, (error: unknown) => {\n hub.fail(`question ${questionId}: local provider failed`, error);\n });\n });\n}\n\n/** Never auto-answer; nudge, then go quiet while staying open. */\nfunction armReminder(hub: Hub, entry: Pending): void {\n entry.timer = setTimeout(() => {\n entry.timer = undefined;\n entry.reminders += 1;\n hub.relay.send(entry.frame, entry.push);\n if (entry.reminders >= MAX_REMINDERS) {\n hub.log.warn(\n 'question %s still unanswered after %d reminders; it stays open (never auto-answered)',\n entry.questionId, entry.reminders,\n );\n return;\n }\n armReminder(hub, entry);\n }, REMINDER_MS);\n entry.timer.unref?.();\n}\n\n/**\n * Project the tool's arguments onto the wire form.\n *\n * `header` and an option's `description` have no wire slot, so they are folded\n * into the text a human reads rather than dropped — a visible degradation, per\n * docs/PRODUCT.md 失败可见.\n */\nfunction normalize(args: unknown): { items: QuestionItem[]; labels: Forwarded['labels'] } | null {\n const questions = (args as { questions?: unknown } | null)?.questions;\n if (!Array.isArray(questions) || questions.length === 0) return null;\n const items: QuestionItem[] = [];\n const labels: Forwarded['labels'] = new Map();\n for (const raw of questions as RawQuestion[]) {\n if (typeof raw?.id !== 'string' || typeof raw.question !== 'string') return null;\n const options: QuestionOption[] = [];\n const byId = new Map<string, string>();\n (raw.options ?? []).forEach((option, index) => {\n if (typeof option?.label !== 'string') return;\n const id = `o${String(index)}`;\n byId.set(id, option.label);\n options.push({ id, label: clamp(decorate(option.label, option.description), LABEL_CHARS, LABEL_BYTES) });\n });\n labels.set(raw.id, byId);\n items.push({\n id: raw.id,\n prompt: clamp(prefix(raw.header, raw.question), PROMPT_CHARS, PROMPT_BYTES),\n options,\n multiSelect: raw.multi_select === true,\n // The tool's output schema always permits `custom`, so free text is\n // always a valid answer — and the only answer when there are no options.\n allowFreeText: true,\n });\n }\n return items.length === 0 ? null : { items, labels };\n}\n\nconst decorate = (label: string, description?: string): string =>\n description === undefined || description === '' ? label : `${label} — ${description}`;\n\nconst prefix = (header: string | undefined, question: string): string =>\n header === undefined || header === '' ? question : `${header}\\n\\n${question}`;\n\nconst clamp = (text: string, chars: number, bytes: number): string =>\n clampBytes(truncate(text, chars), bytes);\n\n/**\n * Validate a phone answer and project it onto the tool's canonical value.\n * `selected` carries option LABELS, not ids\n * (packages/interaction/user-questions/src/types.ts, AskUserQuestionAnswerItem).\n * @returns the value to return from the tool, or a human-readable complaint.\n */\nfunction toAnswerValue(entry: Forwarded, answers: readonly QuestionAnswer[]): AnswerValue | string {\n if (!Array.isArray(answers)) return '回答格式无效:answers 不是数组';\n const seen = new Set<string>();\n const projected: AnswerValue['answers'] = [];\n for (const answer of answers) {\n const options = entry.labels.get(answer.itemId);\n if (options === undefined) return `回答引用了未知的问题 id:${String(answer.itemId)}`;\n if (seen.has(answer.itemId)) return `问题 ${answer.itemId} 被回答了多次`;\n seen.add(answer.itemId);\n const selected: string[] = [];\n for (const optionId of answer.optionIds ?? []) {\n const label = options.get(optionId);\n if (label === undefined) return `问题 ${answer.itemId} 的选项 id 未知:${String(optionId)}`;\n selected.push(label);\n }\n const item = entry.frame.items.find(candidate => candidate.id === answer.itemId);\n if (item !== undefined && !item.multiSelect && selected.length > 1) {\n return `问题 ${answer.itemId} 是单选,却收到 ${String(selected.length)} 个选项`;\n }\n const custom = answer.freeText;\n if (selected.length === 0 && (custom === undefined || custom === '')) {\n return `问题 ${answer.itemId} 既没有选项也没有文字回答`;\n }\n projected.push({ id: answer.itemId, selected, ...custom === undefined || custom === '' ? {} : { custom } });\n }\n const missing = entry.frame.items.filter(item => !seen.has(item.id)).map(item => item.id);\n if (missing.length > 0) return `还有问题未回答:${missing.join(', ')}`;\n return { answers: projected };\n}\n\n/**\n * `content` here is a placeholder: the registry re-renders it through the\n * tool's own `output.render` in `normalizeDispatchResult`.\n */\nfunction successResult(value: AnswerValue): ToolExecutionResult {\n return { isError: false, value, content: [{ type: 'text', text: JSON.stringify(value) }] };\n}\n\n/** Same shape as the registry's own `toolErrorResult` (tools/src/index.ts:1870). */\nfunction abortedResult(): ToolExecutionResult {\n const message = 'ask_user_question was cancelled before the user answered';\n return { isError: true, error: { message }, content: [{ type: 'text', text: `Error: ${message}` }] };\n}\n\nfunction closedMsg(questionId: string, resolution: QuestionResolution) {\n return { v: 1, ts: Date.now(), type: 'question.closed', questionId, resolution } as const;\n}\n","/**\n * Outbound wss client to the dsh-dispatch relay. Uses Node's global\n * `WebSocket` (Node >= 22), so the plugin adds no transport dependency.\n *\n * Everything crossing this boundary is sealed with the pairing key: the relay\n * routes ciphertext and learns nothing but room id, role and message size.\n */\n\nimport {\n deriveKey,\n deriveRoomId,\n MAX_ENVELOPE_BYTES,\n open,\n seal,\n} from '@dsh-dispatch/shared';\nimport type {\n MachineToPhoneMsg,\n MsgFrame,\n PhoneToMachineMsg,\n PushCompactMsg,\n} from '@dsh-dispatch/shared';\nimport type { Log } from './log.js';\n\nconst BACKOFF_BASE_MS = 500;\nconst BACKOFF_MAX_MS = 30_000;\n/** Consecutive connect failures after which we surface a visible error state. */\nconst GIVE_UP_AFTER = 10;\n/** Retry interval once we have surfaced the error; we never stop trying. */\nconst GIVE_UP_RETRY_MS = 60_000;\n/** Decryption failures inside this window collapse into one log line. */\nconst TAMPER_LOG_WINDOW_MS = 30_000;\n\n/** Everything `/dispatch-status` needs to describe the link in one line. */\nexport interface RelayStatus {\n readonly connected: boolean;\n readonly phoneOnline: boolean;\n readonly consecutiveFailures: number;\n readonly gaveUp: boolean;\n readonly room: string;\n readonly decryptFailures: number;\n}\n\nexport interface RelayClientOptions {\n readonly url: string;\n readonly secret: Uint8Array;\n readonly log: Log;\n /** A validated inner message arrived from the phone. */\n onMessage(message: PhoneToMachineMsg): void;\n /** The relay accepted our hello — time to publish status and a snapshot. */\n onConnected(): void;\n /** A phone joined or left this room. */\n onPhonePresence(online: boolean): void;\n}\n\nexport class RelayClient {\n readonly #options: RelayClientOptions;\n #key: Uint8Array;\n #room: string;\n #socket: WebSocket | undefined;\n #retry: ReturnType<typeof setTimeout> | undefined;\n #stopped = true;\n #connected = false;\n #phoneOnline = false;\n #failures = 0;\n #gaveUp = false;\n #tamperBurst = 0;\n #tamperWindowStart = 0;\n #tamperTotal = 0;\n\n constructor(options: RelayClientOptions) {\n this.#options = options;\n this.#key = deriveKey(options.secret);\n this.#room = deriveRoomId(options.secret);\n }\n\n get connected(): boolean {\n return this.#connected;\n }\n\n get phoneOnline(): boolean {\n return this.#phoneOnline;\n }\n\n status(): RelayStatus {\n return {\n connected: this.#connected,\n phoneOnline: this.#phoneOnline,\n consecutiveFailures: this.#failures,\n gaveUp: this.#gaveUp,\n room: this.#room,\n decryptFailures: this.#tamperTotal,\n };\n }\n\n start(): void {\n if (!this.#stopped) return;\n this.#stopped = false;\n this.#connect();\n }\n\n stop(): void {\n this.#stopped = true;\n if (this.#retry !== undefined) clearTimeout(this.#retry);\n this.#retry = undefined;\n this.#teardownSocket();\n this.#connected = false;\n this.#phoneOnline = false;\n }\n\n /** Adopt a freshly generated secret: new room, new key, new connection. */\n rekey(secret: Uint8Array): void {\n this.#key = deriveKey(secret);\n this.#room = deriveRoomId(secret);\n this.#failures = 0;\n this.#gaveUp = false;\n if (this.#stopped) return;\n this.stop();\n this.start();\n }\n\n /**\n * Seal and send one inner message. `push` rides along so the relay can wake\n * an offline phone through Web Push without ever seeing the plaintext.\n * @returns whether the frame reached the socket.\n */\n send(message: MachineToPhoneMsg, push?: PushCompactMsg): boolean {\n const socket = this.#socket;\n if (socket === undefined || !this.#connected) return false;\n const frame: MsgFrame = {\n kind: 'msg',\n room: this.#room,\n payload: seal(message, this.#key),\n push: push === undefined ? null : { payload: seal(push, this.#key), tag: push.kind },\n };\n const text = JSON.stringify(frame);\n if (text.length > MAX_ENVELOPE_BYTES) {\n this.#options.log.error(\n 'relay: refusing to send an oversized %s envelope (%d bytes > %d); this is a truncation bug',\n message.type, text.length, MAX_ENVELOPE_BYTES,\n );\n return false;\n }\n socket.send(text);\n return true;\n }\n\n #connect(): void {\n if (this.#stopped) return;\n let socket: WebSocket;\n try {\n socket = new WebSocket(this.#options.url);\n } catch (error) {\n this.#options.log.error('relay: cannot open %s: %s', this.#options.url, error);\n this.#scheduleRetry();\n return;\n }\n this.#socket = socket;\n socket.addEventListener('open', () => {\n socket.send(JSON.stringify({ kind: 'hello', room: this.#room, role: 'machine' }));\n });\n socket.addEventListener('message', (event: MessageEvent) => {\n this.#onFrame(event.data);\n });\n socket.addEventListener('error', () => {\n // 'close' always follows; the event itself carries no useful detail.\n });\n socket.addEventListener('close', (event) => {\n if (this.#socket !== socket) return;\n const code = (event as unknown as { code?: number }).code;\n this.#onDown(`socket closed (${String(code ?? 'no code')})`);\n });\n }\n\n #onFrame(data: unknown): void {\n if (typeof data !== 'string') return;\n let frame: unknown;\n try {\n frame = JSON.parse(data);\n } catch (error) {\n this.#options.log.error('relay: dropped an unparseable frame: %s', error);\n return;\n }\n const kind = (frame as { kind?: unknown }).kind;\n if (kind === 'hello-ok') this.#onHelloOk(frame);\n else if (kind === 'presence') this.#onPresence(frame);\n else if (kind === 'msg') this.#onData(frame);\n else if (kind === 'error') {\n const code = String((frame as { code?: unknown }).code ?? 'unknown');\n this.#options.log.error('relay: rejected our frame with code \"%s\"', code);\n } else {\n this.#options.log.warn('relay: ignoring unknown frame kind \"%s\"', String(kind));\n }\n }\n\n #onHelloOk(frame: unknown): void {\n this.#connected = true;\n this.#failures = 0;\n this.#gaveUp = false;\n const peers = (frame as { peers?: { phone?: unknown } }).peers;\n const phones = typeof peers?.phone === 'number' ? peers.phone : 0;\n this.#options.log.info('relay: connected (room %s…, phones online: %d)', this.#room.slice(0, 8), phones);\n this.#options.onConnected();\n if (phones > 0) this.#setPhoneOnline(true);\n }\n\n #onPresence(frame: unknown): void {\n const { role, online } = frame as { role?: unknown; online?: unknown };\n if (role !== 'phone' || typeof online !== 'boolean') return;\n this.#setPhoneOnline(online);\n }\n\n #setPhoneOnline(online: boolean): void {\n if (this.#phoneOnline === online) return;\n this.#phoneOnline = online;\n this.#options.onPhonePresence(online);\n }\n\n #onData(frame: unknown): void {\n const payload = (frame as { payload?: unknown }).payload;\n if (typeof payload !== 'string') {\n this.#options.log.error('relay: dropped a msg frame with no payload');\n return;\n }\n const plain = open(payload, this.#key);\n if (plain === null) {\n this.#onTamper();\n return;\n }\n const message = parseInbound(plain);\n if (message === null) {\n this.#options.log.warn(\n 'relay: ignoring inner message of type \"%s\" — unknown type or missing/invalid fields '\n + '(the phone may be newer than this plugin)',\n String((plain as { type?: unknown }).type),\n );\n return;\n }\n this.#options.onMessage(message);\n }\n\n /** Decryption failure is never silent: it means tampering or a stale pairing. */\n #onTamper(): void {\n this.#tamperTotal += 1;\n this.#tamperBurst += 1;\n const now = Date.now();\n if (now - this.#tamperWindowStart < TAMPER_LOG_WINDOW_MS) return;\n this.#tamperWindowStart = now;\n this.#options.log.error(\n 'relay: %d message(s) failed to decrypt — wrong key or tampering. Re-pair with /dispatch-repair '\n + 'and re-scan the QR on every phone.',\n this.#tamperBurst,\n );\n this.#tamperBurst = 0;\n }\n\n #onDown(reason: string): void {\n this.#teardownSocket();\n if (this.#stopped) return;\n this.#connected = false;\n this.#setPhoneOnline(false);\n this.#failures += 1;\n this.#options.log.debug('relay: %s, attempt %d', reason, this.#failures);\n this.#scheduleRetry();\n }\n\n #scheduleRetry(): void {\n if (this.#stopped || this.#retry !== undefined) return;\n if (this.#failures >= GIVE_UP_AFTER && !this.#gaveUp) {\n this.#gaveUp = true;\n this.#options.log.error(\n 'relay: %d consecutive failures connecting to %s — check that the relay is reachable. '\n + 'Still retrying every %ds.',\n this.#failures, this.#options.url, GIVE_UP_RETRY_MS / 1000,\n );\n }\n this.#retry = setTimeout(() => {\n this.#retry = undefined;\n this.#connect();\n }, this.#backoffMs());\n this.#retry.unref?.();\n }\n\n #backoffMs(): number {\n if (this.#gaveUp) return GIVE_UP_RETRY_MS;\n const exponential = Math.min(BACKOFF_BASE_MS * 2 ** this.#failures, BACKOFF_MAX_MS);\n return Math.round(exponential * (0.75 + Math.random() * 0.5));\n }\n\n #teardownSocket(): void {\n const socket = this.#socket;\n this.#socket = undefined;\n if (socket === undefined) return;\n try {\n socket.close();\n } catch (error) {\n this.#options.log.debug('relay: error closing socket: %s', error);\n }\n }\n}\n\nconst INBOUND_FIELDS: Record<PhoneToMachineMsg['type'], readonly string[]> = {\n 'sessions.get': [],\n 'approval.respond': ['requestId', 'approvalId', 'decision'],\n 'question.respond': ['requestId', 'questionId'],\n 'dispatch.request': ['requestId', 'prompt'],\n 'session.message': ['requestId', 'sessionId', 'text'],\n};\n\n/**\n * Accept only inner messages this plugin understands, with their required\n * string fields present. Unknown `type`/`v` returns null so the caller logs it\n * (forward compatibility per docs/PROTOCOL.md) rather than crashing.\n */\nexport function parseInbound(value: unknown): PhoneToMachineMsg | null {\n if (typeof value !== 'object' || value === null) return null;\n const record = value as Record<string, unknown>;\n if (record['v'] !== 1) return null;\n const type = record['type'];\n if (typeof type !== 'string' || !(type in INBOUND_FIELDS)) return null;\n const required = INBOUND_FIELDS[type as PhoneToMachineMsg['type']];\n for (const field of required) {\n if (typeof record[field] !== 'string' || record[field] === '') return null;\n }\n if (type === 'approval.respond' && record['decision'] !== 'allow' && record['decision'] !== 'deny') {\n return null;\n }\n if (type === 'dispatch.request' && typeof record['worktree'] !== 'boolean') return null;\n // Shape only — an unknown `access` value is a protocol-level complaint the\n // dispatch handler answers visibly, not something to drop on the floor here.\n if (type === 'question.respond' && !isAnswerList(record['answers'])) return null;\n return record as unknown as PhoneToMachineMsg;\n}\n\n/** `answers: [{ itemId, optionIds, freeText? }]`, checked structurally. */\nfunction isAnswerList(value: unknown): boolean {\n if (!Array.isArray(value) || value.length === 0) return false;\n return value.every((entry: unknown) => {\n const answer = entry as { itemId?: unknown; optionIds?: unknown; freeText?: unknown };\n if (typeof answer?.itemId !== 'string') return false;\n if (!Array.isArray(answer.optionIds)) return false;\n if (!answer.optionIds.every(id => typeof id === 'string')) return false;\n return answer.freeText === undefined || typeof answer.freeText === 'string';\n });\n}\n","/**\n * Live session board: maps dsh agent lifecycle events onto the protocol's\n * `Session` objects and pushes `session.update` / `session.snapshot` to the\n * phone.\n */\n\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { Agent } from '@deepseek-ai/dsh-agent';\nimport { SessionId } from '@deepseek-ai/dsh-session';\nimport type {} from '@deepseek-ai/dsh-session-query';\nimport { truncate } from '@dsh-dispatch/shared';\nimport type { Session as WireSession, SessionState } from '@dsh-dispatch/shared';\n\n/** Permission tier as it appears on the wire. */\ntype SessionAccess = NonNullable<WireSession['access']>;\nimport type { Log } from './log.js';\nimport type { RelayClient } from './relay-client.js';\n\n/** docs/PROTOCOL.md: `title` = first 80 chars of the initial prompt. */\nconst TITLE_CHARS = 80;\n\n/** Lifecycle state before the approval overlay is applied. */\ntype BaseState = 'idle' | 'running' | 'done' | 'error';\n\ninterface Tracked {\n sessionId: string;\n title: string;\n cwd: string;\n base: BaseState;\n openApprovals: number;\n lastActivity: number;\n dispatched: boolean;\n /** Permission tier, for audit. Once 'full' it is never downgraded. */\n access: SessionAccess;\n /**\n * A dispatched session whose `turn.final` has been sent. It stays `done` on\n * the phone until a NEW turn starts — the idle status that follows every\n * turn must not demote the card back to 空闲.\n */\n finalized: boolean;\n}\n\nexport class SessionTracker {\n readonly #ctx: Context;\n readonly #relay: RelayClient;\n readonly #log: Log;\n readonly #tracked = new Map<string, Tracked>();\n readonly #published = new Map<string, string>();\n\n constructor(ctx: Context, relay: RelayClient, log: Log) {\n this.#ctx = ctx;\n this.#relay = relay;\n this.#log = log;\n }\n\n /** Subscribe the agent lifecycle. Registrations unwind with the plugin. */\n install(): void {\n this.#ctx.on('agent/created', ({ agent }) => {\n this.#adopt(agent);\n this.#publish(agent.id);\n void this.#refreshTitle(agent);\n });\n this.#ctx.on('agent/session-start', ({ agent }) => {\n void this.#refreshTitle(agent);\n });\n this.#ctx.on('agent/status', ({ agent, status }) => {\n this.#update(agent.id, (entry) => {\n // The trailing idle of a finished turn is not new information; only a\n // fresh `running` reopens a finalized session.\n if (status === 'idle' && entry.finalized) return;\n entry.finalized = false;\n entry.base = status;\n });\n });\n this.#ctx.on('agent/error', ({ agent, error }) => {\n this.#log.error('session %s: agent error: %s', agent.id, error);\n this.#update(agent.id, entry => { entry.base = 'error'; });\n });\n this.#ctx.on('agent/turn-stopping', ({ agent }) => {\n void this.#refreshTitle(agent);\n });\n this.#ctx.on('agent/disposed', ({ agent }) => {\n this.#update(agent.id, entry => { entry.base = 'done'; });\n this.#tracked.delete(agent.id);\n this.#published.delete(agent.id);\n });\n }\n\n /** Mark a session as started by us, with the dispatch prompt as its title. */\n markDispatched(sessionId: string, prompt: string, access: SessionAccess = 'standard'): void {\n this.#update(sessionId, (entry) => {\n entry.dispatched = true;\n entry.title = truncate(prompt.trim().split('\\n', 1)[0] ?? '', TITLE_CHARS);\n // Full access is an audit fact: recorded permanently, never downgraded.\n if (access === 'full') entry.access = 'full';\n });\n }\n\n isDispatched(sessionId: string): boolean {\n return this.#tracked.get(sessionId)?.dispatched ?? false;\n }\n\n /**\n * A dispatched turn has closed and its `turn.final` is on its way. Latch the\n * card at done/error so the trailing idle status cannot demote it.\n */\n markTurnComplete(sessionId: string): void {\n this.#update(sessionId, (entry) => {\n entry.finalized = true;\n if (entry.base !== 'error') entry.base = 'done';\n });\n }\n\n hasError(sessionId: string): boolean {\n return this.#tracked.get(sessionId)?.base === 'error';\n }\n\n /** The live agent for a session id, or undefined when it is not live. */\n agentOf(sessionId: string): Agent | undefined {\n return this.#ctx.agents.get(SessionId(sessionId));\n }\n\n /** An approval for this session is now waiting on a human. */\n approvalOpened(sessionId: string): void {\n this.#update(sessionId, entry => { entry.openApprovals += 1; });\n }\n\n approvalClosed(sessionId: string): void {\n this.#update(sessionId, entry => {\n entry.openApprovals = Math.max(0, entry.openApprovals - 1);\n });\n }\n\n /** Active sessions only — the phone board never shows cold history. */\n async snapshot(): Promise<WireSession[]> {\n const records = await this.#ctx.sessionQuery.listSessions();\n const wire: WireSession[] = [];\n for (const record of records) {\n if (!record.live) continue;\n const id = record.header.id;\n const known = this.#tracked.get(id) ?? this.#adopt(this.#ctx.agents.get(id));\n wire.push(known === undefined\n ? this.#fallbackWire(id, record.header.cwd ?? '', record.header.createdAt)\n : toWire(known));\n }\n return wire;\n }\n\n #fallbackWire(sessionId: string, cwd: string, createdAt: number): WireSession {\n return {\n sessionId,\n title: truncate(sessionId, TITLE_CHARS),\n cwd,\n state: 'idle',\n lastActivity: createdAt,\n dispatched: false,\n access: 'standard',\n };\n }\n\n #adopt(agent: Agent | undefined): Tracked | undefined {\n if (agent === undefined) return undefined;\n const existing = this.#tracked.get(agent.id);\n if (existing !== undefined) return existing;\n const entry: Tracked = {\n sessionId: agent.id,\n title: truncate(firstPrompt(agent) ?? agent.id, TITLE_CHARS),\n cwd: agent.session.header.cwd ?? '',\n base: agent.status,\n openApprovals: 0,\n lastActivity: Date.now(),\n dispatched: false,\n access: 'standard',\n finalized: false,\n };\n this.#tracked.set(agent.id, entry);\n return entry;\n }\n\n #update(sessionId: string, mutate: (entry: Tracked) => void): void {\n const entry = this.#tracked.get(sessionId) ?? this.#adopt(this.#ctx.agents.get(SessionId(sessionId)));\n if (entry === undefined) return;\n mutate(entry);\n entry.lastActivity = Date.now();\n this.#publish(sessionId);\n }\n\n /** Emit `session.update` only when the phone-visible projection changed. */\n #publish(sessionId: string): void {\n const entry = this.#tracked.get(sessionId);\n if (entry === undefined) return;\n const session = toWire(entry);\n const fingerprint = `${session.state}|${session.title}|${session.cwd}`\n + `|${String(session.dispatched)}|${String(session.access)}`;\n if (this.#published.get(sessionId) === fingerprint) return;\n this.#published.set(sessionId, fingerprint);\n this.#relay.send({ v: 1, ts: Date.now(), type: 'session.update', session });\n }\n\n async #refreshTitle(agent: Agent): Promise<void> {\n const entry = this.#tracked.get(agent.id);\n if (entry === undefined || entry.dispatched) return;\n try {\n const snapshot = await this.#ctx.sessionQuery.readTitle(SessionId(agent.id));\n const title = snapshot?.title ?? firstPrompt(agent);\n if (title === undefined || title === '') return;\n entry.title = truncate(title, TITLE_CHARS);\n this.#publish(agent.id);\n } catch (error) {\n this.#log.error('session %s: could not read title: %s', agent.id, error);\n }\n }\n}\n\nfunction toWire(entry: Tracked): WireSession {\n return {\n sessionId: entry.sessionId,\n title: entry.title,\n cwd: entry.cwd,\n state: stateOf(entry),\n lastActivity: entry.lastActivity,\n dispatched: entry.dispatched,\n access: entry.access,\n };\n}\n\n/**\n * `awaiting_approval` is an overlay, not a lifecycle state: it holds exactly\n * while our answerer has an open question for this session.\n */\nfunction stateOf(entry: Tracked): SessionState {\n if (entry.base === 'done' || entry.base === 'error') return entry.base;\n return entry.openApprovals > 0 ? 'awaiting_approval' : entry.base;\n}\n\n/** Fallback title source: the first human prompt in the session log. */\nfunction firstPrompt(agent: Agent): string | undefined {\n for (const event of agent.session.events) {\n if (event.type !== 'user/message') continue;\n const text = event.data.content\n .filter(block => block.type === 'text')\n .map(block => block.text)\n .join('')\n .trim();\n if (text !== '') return text.split('\\n', 1)[0];\n }\n return undefined;\n}\n","/**\n * Plugin version reported in `machine.status`.\n *\n * Kept as a literal rather than read from package.json so the bundled ESM\n * output has no runtime filesystem dependency. `tests/version.test.ts` fails\n * the build if this drifts from package.json.\n */\nexport const PLUGIN_VERSION = '0.2.0';\n","/**\n * Plugin configuration. Validated by Schemastery before `apply` runs, so every\n * field below is present with its default already filled in.\n */\n\nimport { hostname } from 'node:os';\nimport z from '@deepseek-ai/schemastery';\n\nexport interface Config {\n /** Relay websocket URL, e.g. `wss://relay.example.com`. */\n relay: string;\n /** Name shown on the phone's machine card. */\n machineName: string;\n /**\n * Absolute directories a dispatched agent may run in. Empty (the default)\n * keeps remote dispatch off — approval forwarding still works.\n */\n allowedRoots: string[];\n /**\n * Whether the phone may request `access: 'full'`, which runs the session\n * under dsh's `danger-full-access` permission preset — full file access with\n * no approval prompts. Off by default; turning it on is a deliberate choice.\n */\n allowFullAccessDispatch: boolean;\n /** Where the pairing secret and dispatch worktrees live. */\n dataDir: string;\n /** Base URL of the PWA, used only to print the pairing convenience link. */\n pwaUrl: string;\n}\n\nexport const Config: z<Config> = z.object({\n relay: z.string().required(),\n machineName: z.string().default(hostname()),\n allowedRoots: z.array(z.string()).default([]),\n allowFullAccessDispatch: z.boolean().default(false),\n dataDir: z.string().default('~/.dsh-dispatch'),\n pwaUrl: z.string().default('http://localhost:5173'),\n});\n","/**\n * dsh-dispatch — command your DeepSeek Harness machines from your phone.\n *\n * Approvals raised anywhere in this dsh instance are forwarded to a paired\n * phone and answered there; the phone can also start new agent sessions in\n * configured directories and follow up on live ones. Everything between this\n * plugin and the phone is end-to-end encrypted; the relay only routes\n * ciphertext.\n */\n\nimport type { Context } from '@deepseek-ai/cordis';\n// Empty type imports carry the Context/Events declaration merges this plugin\n// listens on and calls into.\nimport type {} from '@deepseek-ai/dsh-agent';\nimport type {} from '@deepseek-ai/dsh-commands';\nimport type {} from '@deepseek-ai/dsh-session-query';\nimport type {} from '@deepseek-ai/dsh-user-approval';\nimport type { PhoneToMachineMsg } from '@dsh-dispatch/shared';\nimport { installApprovals } from './approvals.js';\nimport type { ApprovalRouter } from './approvals.js';\nimport { installCommands } from './commands.js';\nimport type { Config } from './config.js';\nimport { installDispatch } from './dispatch.js';\nimport type { DispatchRouter } from './dispatch.js';\nimport { createHub } from './hub.js';\nimport type { Hub } from './hub.js';\nimport { PairingStore } from './pairing.js';\nimport { installQuestions } from './questions.js';\nimport type { QuestionRouter } from './questions.js';\nimport { RelayClient } from './relay-client.js';\nimport { SessionTracker } from './sessions.js';\nimport { PLUGIN_VERSION } from './version.js';\n\nexport { Config } from './config.js';\n\nexport const name = 'dsh-dispatch';\n\n/**\n * `approval` is deliberately absent: the plugin is useful (dispatch, session\n * board) in a composition without it, and the dossier's apiproxy precedent\n * guards the service rather than requiring it.\n */\nexport const inject = ['agents', 'commands', 'sessionQuery'];\n\n/** docs/PROTOCOL.md: machine.status on connect and every 60s. */\nconst STATUS_INTERVAL_MS = 60_000;\n\n/** The three inbound feature handlers, passed around as one bundle. */\nexport interface Routers {\n readonly approvals: ApprovalRouter;\n readonly questions: QuestionRouter;\n readonly dispatch: DispatchRouter;\n}\n\nexport function apply(ctx: Context, config: Config): void {\n const log = ctx.logger('dsh-dispatch');\n const pairing = PairingStore.open(config.dataDir, log);\n // The router is built from the relay, and the relay calls into the router:\n // late-bind the two callbacks rather than smuggling a half-built object.\n const deferred = { message(_: PhoneToMachineMsg): void {}, hello(): void {} };\n const relay = new RelayClient({\n url: config.relay,\n secret: pairing.secret,\n log,\n onMessage: message => { deferred.message(message); },\n onConnected: () => { deferred.hello(); },\n onPhonePresence: (online) => {\n if (!online) {\n log.info('relay: phone left the room');\n return;\n }\n pairing.markPaired();\n log.info('relay: phone joined the room');\n deferred.hello();\n },\n });\n const sessions = new SessionTracker(ctx, relay, log);\n const hub = createHub({ ctx, config, log, relay, sessions, pairing });\n const approvals = installApprovals(hub);\n const questions = installQuestions(hub);\n const dispatch = installDispatch(hub);\n const routers: Routers = { approvals, questions, dispatch };\n deferred.message = message => { route(hub, routers, message); };\n deferred.hello = () => {\n publishStatus(hub);\n approvals.resendPending();\n questions.resendPending();\n void publishSnapshot(hub);\n };\n sessions.install();\n installCommands(hub, routers);\n startRelay(hub);\n}\n\n/** Bring the link up, unless it cannot possibly work — say so once, loudly. */\nfunction startRelay(hub: Hub): void {\n const url = hub.config.relay;\n if (url === '') {\n hub.log.error(\n 'relay is not configured: set `relay: \\'wss://…\\'` on the dsh-dispatch row in your profile\\'s '\n + 'cordis.patch.yml. Approval forwarding and dispatch stay off until then.',\n );\n return;\n }\n if (!/^wss?:\\/\\//.test(url)) {\n hub.log.error('relay \"%s\" is not a ws:// or wss:// URL; refusing to connect', url);\n return;\n }\n hub.ctx.effect(() => {\n hub.relay.start();\n return () => { hub.relay.stop(); };\n }, 'dsh-dispatch: relay client');\n hub.ctx.effect(() => {\n const timer = setInterval(() => { publishStatus(hub); }, STATUS_INTERVAL_MS);\n timer.unref?.();\n return () => { clearInterval(timer); };\n }, 'dsh-dispatch: status heartbeat');\n}\n\nfunction route(hub: Hub, routers: Routers, message: PhoneToMachineMsg): void {\n const { approvals, questions, dispatch } = routers;\n switch (message.type) {\n case 'sessions.get':\n void publishSnapshot(hub);\n return;\n case 'approval.respond':\n approvals.respond(message);\n return;\n case 'question.respond':\n questions.respond(message);\n return;\n case 'dispatch.request':\n void dispatch.request(message).catch((error: unknown) => {\n hub.fail(`dispatch.request ${message.requestId}`, error);\n });\n return;\n case 'session.message':\n void dispatch.followup(message).catch((error: unknown) => {\n hub.fail(`session.message ${message.requestId}`, error);\n });\n }\n}\n\nfunction publishStatus(hub: Hub): void {\n hub.relay.send({\n v: 1,\n ts: Date.now(),\n type: 'machine.status',\n machine: hub.config.machineName,\n pluginVersion: PLUGIN_VERSION,\n capabilities: {\n fullAccessDispatch: hub.config.allowFullAccessDispatch,\n questionForwarding: true,\n },\n });\n}\n\nasync function publishSnapshot(hub: Hub): Promise<void> {\n try {\n hub.relay.send({\n v: 1,\n ts: Date.now(),\n type: 'session.snapshot',\n sessions: await hub.sessions.snapshot(),\n });\n } catch (error) {\n hub.fail('session.snapshot', error);\n }\n}\n"],"mappings":"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","names":["resolve","entry","prefix","randomUUID","resolve","join","randomUUID","resolve","ok","randomUUID","REMINDER_MS","MAX_REMINDERS","forward","closedMsg","randomUUID","race","resolve","entry","armReminder","SessionId","TITLE_CHARS","SessionId"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dispatch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Command your DeepSeek Harness machines from your phone: approval push, remote dispatch, session board. Built on DeepSeek Harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -44,9 +44,12 @@
|
|
|
44
44
|
"@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.2",
|
|
45
45
|
"@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
|
|
46
46
|
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-permission-presets": "^0.1.1-rc.2",
|
|
47
48
|
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
48
49
|
"@deepseek-ai/dsh-session-query": "^0.1.1-rc.2",
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-user-approval": "^0.1.1-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-user-questions": "^0.1.1-rc.2"
|
|
50
53
|
},
|
|
51
54
|
"devDependencies": {
|
|
52
55
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -55,16 +58,19 @@
|
|
|
55
58
|
"@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.2",
|
|
56
59
|
"@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
|
|
57
60
|
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-permission-presets": "^0.1.1-rc.2",
|
|
58
62
|
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
59
63
|
"@deepseek-ai/dsh-session-query": "^0.1.1-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
60
65
|
"@deepseek-ai/dsh-user-approval": "^0.1.1-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-user-questions": "^0.1.1-rc.2",
|
|
61
67
|
"@types/node": "^22.19.0",
|
|
62
68
|
"@types/ws": "^8.18.1",
|
|
63
69
|
"tsup": "^8.5.1",
|
|
64
70
|
"typescript": "^7.0.2",
|
|
65
71
|
"vitest": "^4.1.11",
|
|
66
72
|
"ws": "^8.18.3",
|
|
67
|
-
"@dsh-dispatch/shared": "0.
|
|
73
|
+
"@dsh-dispatch/shared": "0.2.0"
|
|
68
74
|
},
|
|
69
75
|
"repository": {
|
|
70
76
|
"type": "git",
|