dsh-dispatch 0.1.0 → 0.3.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 +433 -43
- package/dist/index.js.map +1 -1
- package/package.json +17 -11
- package/LICENSE +0 -21
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");
|
|
@@ -411,7 +413,8 @@ async function prepareWorkspace(options) {
|
|
|
411
413
|
if (!await isGitRepo(options.cwd)) {
|
|
412
414
|
return {
|
|
413
415
|
cwd: options.cwd,
|
|
414
|
-
note: `\u5DF2\u964D\u7EA7\uFF1A${options.cwd} \u4E0D\u662F git \u4ED3\u5E93\uFF0C\u4EFB\u52A1\u76F4\u63A5\u5728\u8BE5\u76EE\u5F55\u8FD0\u884C\uFF08\u672A\u521B\u5EFA worktree\uFF09
|
|
416
|
+
note: `\u5DF2\u964D\u7EA7\uFF1A${options.cwd} \u4E0D\u662F git \u4ED3\u5E93\uFF0C\u4EFB\u52A1\u76F4\u63A5\u5728\u8BE5\u76EE\u5F55\u8FD0\u884C\uFF08\u672A\u521B\u5EFA worktree\uFF09`,
|
|
417
|
+
noteReason: { code: "not-a-git-repo", params: { cwd: options.cwd } }
|
|
415
418
|
};
|
|
416
419
|
}
|
|
417
420
|
const root = join2(options.dataDir, "worktrees");
|
|
@@ -454,8 +457,23 @@ function stderrOf(error) {
|
|
|
454
457
|
|
|
455
458
|
// src/dispatch.ts
|
|
456
459
|
var SEEN_LIMIT = 256;
|
|
457
|
-
|
|
458
|
-
|
|
460
|
+
function complain(text, code, params) {
|
|
461
|
+
return { text, reason: params === void 0 ? { code } : { code, params } };
|
|
462
|
+
}
|
|
463
|
+
var DISPATCH_DISABLED = complain(
|
|
464
|
+
"dispatch \u672A\u542F\u7528\uFF1A\u8BF7\u5728\u63D2\u4EF6\u914D\u7F6E\u4E2D\u8BBE\u7F6E allowedRoots",
|
|
465
|
+
"dispatch-disabled"
|
|
466
|
+
);
|
|
467
|
+
var NO_REMOTE_FOLLOWUP = complain("\u8BE5\u4F1A\u8BDD\u4E0D\u652F\u6301\u8FDC\u7A0B\u8FFD\u52A0", "no-remote-followup");
|
|
468
|
+
var FULL_ACCESS_DISABLED = complain(
|
|
469
|
+
"full-access \u6D3E\u53D1\u672A\u542F\u7528\uFF1A\u8BF7\u5728\u63D2\u4EF6\u914D\u7F6E\u4E2D\u5F00\u542F allowFullAccessDispatch",
|
|
470
|
+
"full-access-disabled"
|
|
471
|
+
);
|
|
472
|
+
var FULL_ACCESS_UNAVAILABLE = complain(
|
|
473
|
+
"full-access \u4E0D\u53EF\u7528\uFF1A\u672C\u673A dsh \u672A\u7EC4\u5408 permission-presets \u63D2\u4EF6",
|
|
474
|
+
"full-access-unavailable"
|
|
475
|
+
);
|
|
476
|
+
var FULL_ACCESS_PRESET = "danger-full-access";
|
|
459
477
|
function installDispatch(hub) {
|
|
460
478
|
const state = {
|
|
461
479
|
handles: /* @__PURE__ */ new Map(),
|
|
@@ -480,6 +498,11 @@ function installDispatch(hub) {
|
|
|
480
498
|
};
|
|
481
499
|
}
|
|
482
500
|
async function handleRequest(hub, state, message) {
|
|
501
|
+
const rejected = validate(hub, message);
|
|
502
|
+
if (rejected !== void 0) {
|
|
503
|
+
hub.relay.send(failure(message.requestId, rejected));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
483
506
|
const replay = state.seen.get(message.requestId);
|
|
484
507
|
if (replay !== void 0) {
|
|
485
508
|
if (replay !== null) hub.relay.send(replay);
|
|
@@ -497,10 +520,9 @@ async function handleRequest(hub, state, message) {
|
|
|
497
520
|
}
|
|
498
521
|
async function startSession(hub, state, message) {
|
|
499
522
|
const prompt = message.prompt;
|
|
500
|
-
const oversized = overLimit("prompt", prompt);
|
|
501
|
-
if (oversized !== void 0) return failure(message.requestId, oversized);
|
|
502
523
|
const target = resolveCwd(hub, message.cwd);
|
|
503
524
|
if (typeof target !== "string") return failure(message.requestId, target.error);
|
|
525
|
+
const full = message.access === "full";
|
|
504
526
|
try {
|
|
505
527
|
const workspace = await prepareWorkspace({
|
|
506
528
|
cwd: target,
|
|
@@ -509,28 +531,48 @@ async function startSession(hub, state, message) {
|
|
|
509
531
|
});
|
|
510
532
|
state.loaderReady ??= Promise.resolve(hub.ctx.get("loader")?.await()).then(() => void 0);
|
|
511
533
|
await state.loaderReady;
|
|
512
|
-
const
|
|
513
|
-
state.handles.set(handle.agent.id, handle);
|
|
514
|
-
hub.sessions.markDispatched(handle.agent.id, prompt);
|
|
515
|
-
|
|
516
|
-
|
|
534
|
+
const created = await createSession(hub.ctx, workspace.cwd, prompt, full);
|
|
535
|
+
state.handles.set(created.handle.agent.id, created.handle);
|
|
536
|
+
hub.sessions.markDispatched(created.handle.agent.id, prompt, full ? "full" : "standard");
|
|
537
|
+
if (created.fullAccessError !== void 0) {
|
|
538
|
+
hub.report(`dispatch ${message.requestId}`, `\u5B8C\u5168\u8BBF\u95EE\u6743\u9650\u672A\u80FD\u5E94\u7528\uFF1A${created.fullAccessError}`);
|
|
539
|
+
}
|
|
540
|
+
const result = ok(message.requestId, created.handle.agent.id);
|
|
541
|
+
const advisory = created.fullAccessError === void 0 ? workspace.note === void 0 ? void 0 : { text: workspace.note, reason: workspace.noteReason ?? { code: "not-a-git-repo" } } : complain(
|
|
542
|
+
`\u5B8C\u5168\u8BBF\u95EE\u6743\u9650\u672A\u80FD\u5E94\u7528\uFF0C\u5DF2\u5728\u6807\u51C6\u6743\u9650\u4E0B\u8FD0\u884C\uFF1A${created.fullAccessError}`,
|
|
543
|
+
"full-access-not-applied",
|
|
544
|
+
{ detail: created.fullAccessError }
|
|
545
|
+
);
|
|
546
|
+
return advisory === void 0 ? result : { ...result, note: advisory.text, reason: advisory.reason };
|
|
517
547
|
} catch (error) {
|
|
518
548
|
const detail = error instanceof WorktreeError ? error.message : String(error);
|
|
519
549
|
hub.log.error("dispatch %s failed: %s", message.requestId, detail);
|
|
520
|
-
return failure(message.requestId,
|
|
550
|
+
return failure(message.requestId, complain(
|
|
551
|
+
detail,
|
|
552
|
+
error instanceof WorktreeError ? "worktree-failed" : "dispatch-failed",
|
|
553
|
+
{ detail }
|
|
554
|
+
));
|
|
521
555
|
}
|
|
522
556
|
}
|
|
523
557
|
async function handleFollowup(hub, state, message) {
|
|
524
|
-
const oversized = overLimit("
|
|
558
|
+
const oversized = overLimit("message", message.text);
|
|
525
559
|
if (oversized !== void 0) {
|
|
526
|
-
hub.report(
|
|
560
|
+
hub.report(
|
|
561
|
+
`session.message ${message.sessionId}`,
|
|
562
|
+
oversized.text,
|
|
563
|
+
{ sessionId: message.sessionId, reason: oversized.reason }
|
|
564
|
+
);
|
|
527
565
|
return;
|
|
528
566
|
}
|
|
529
567
|
if (state.seen.get(message.requestId) !== void 0) return;
|
|
530
568
|
state.seen.set(message.requestId, null);
|
|
531
569
|
const agent = state.handles.get(message.sessionId)?.agent ?? hub.sessions.agentOf(message.sessionId);
|
|
532
570
|
if (agent === void 0) {
|
|
533
|
-
hub.report(
|
|
571
|
+
hub.report(
|
|
572
|
+
`session.message ${message.sessionId}`,
|
|
573
|
+
NO_REMOTE_FOLLOWUP.text,
|
|
574
|
+
{ sessionId: message.sessionId, reason: NO_REMOTE_FOLLOWUP.reason }
|
|
575
|
+
);
|
|
534
576
|
return;
|
|
535
577
|
}
|
|
536
578
|
agent.followup(createUserMessage({
|
|
@@ -539,13 +581,22 @@ async function handleFollowup(hub, state, message) {
|
|
|
539
581
|
}));
|
|
540
582
|
}
|
|
541
583
|
function overLimit(field, text) {
|
|
584
|
+
const label = field === "prompt" ? "\u63D0\u793A\u8BCD" : "\u6D88\u606F";
|
|
542
585
|
const tail = "\u8BF7\u5728\u624B\u673A\u7AEF\u7F29\u77ED\u540E\u91CD\u53D1\u3002";
|
|
543
586
|
if (text.length > MAX_PROMPT_CHARS) {
|
|
544
|
-
return
|
|
587
|
+
return complain(
|
|
588
|
+
`${label} \u8D85\u957F\uFF1A${String(text.length)} \u5B57\u7B26\uFF0C\u4E0A\u9650 ${String(MAX_PROMPT_CHARS)} \u5B57\u7B26\u3002${tail}`,
|
|
589
|
+
`${field}-too-long-chars`,
|
|
590
|
+
{ actual: text.length, limit: MAX_PROMPT_CHARS }
|
|
591
|
+
);
|
|
545
592
|
}
|
|
546
593
|
const bytes = utf8Length(text);
|
|
547
594
|
if (bytes > MAX_PROMPT_BYTES) {
|
|
548
|
-
return
|
|
595
|
+
return complain(
|
|
596
|
+
`${label} \u8D85\u957F\uFF1A${String(bytes)} \u5B57\u8282\uFF08UTF-8\uFF09\uFF0C\u4E0A\u9650 ${String(MAX_PROMPT_BYTES)} \u5B57\u8282\u3002${tail}`,
|
|
597
|
+
`${field}-too-long-bytes`,
|
|
598
|
+
{ actual: bytes, limit: MAX_PROMPT_BYTES }
|
|
599
|
+
);
|
|
549
600
|
}
|
|
550
601
|
return void 0;
|
|
551
602
|
}
|
|
@@ -570,7 +621,22 @@ async function composeAgent(ctx, selection) {
|
|
|
570
621
|
}
|
|
571
622
|
};
|
|
572
623
|
}
|
|
573
|
-
|
|
624
|
+
function validate(hub, message) {
|
|
625
|
+
const oversized = overLimit("prompt", message.prompt);
|
|
626
|
+
if (oversized !== void 0) return oversized;
|
|
627
|
+
if (message.access !== void 0 && message.access !== "standard" && message.access !== "full") {
|
|
628
|
+
return complain(
|
|
629
|
+
`access \u53D6\u503C\u65E0\u6548\uFF1A${String(message.access)}`,
|
|
630
|
+
"bad-access-value",
|
|
631
|
+
{ value: String(message.access) }
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (message.access !== "full") return void 0;
|
|
635
|
+
if (!hub.config.allowFullAccessDispatch) return FULL_ACCESS_DISABLED;
|
|
636
|
+
if (hub.ctx.get("permissionPresets") === void 0) return FULL_ACCESS_UNAVAILABLE;
|
|
637
|
+
return void 0;
|
|
638
|
+
}
|
|
639
|
+
async function createSession(ctx, cwd, prompt, fullAccess) {
|
|
574
640
|
const selection = ctx.get("agentDefaultModel")?.currentSelection();
|
|
575
641
|
const composition = await composeAgent(ctx, selection);
|
|
576
642
|
const handle = await ctx.agents.create({
|
|
@@ -582,30 +648,54 @@ async function createSession(ctx, cwd, prompt) {
|
|
|
582
648
|
setup: composition.setup
|
|
583
649
|
});
|
|
584
650
|
await handle.agent.whenIdle();
|
|
651
|
+
const fullAccessError = fullAccess ? applyFullAccess(ctx, handle) : void 0;
|
|
585
652
|
handle.agent.followup(createUserMessage({
|
|
586
653
|
content: [{ type: "text", text: prompt }],
|
|
587
654
|
source: { kind: "user" }
|
|
588
655
|
}));
|
|
589
|
-
return handle;
|
|
656
|
+
return fullAccessError === void 0 ? { handle } : { handle, fullAccessError };
|
|
657
|
+
}
|
|
658
|
+
function applyFullAccess(ctx, handle) {
|
|
659
|
+
try {
|
|
660
|
+
const presets = ctx.get("permissionPresets");
|
|
661
|
+
if (presets === void 0) return "permission-presets \u63D2\u4EF6\u672A\u7EC4\u5408";
|
|
662
|
+
presets.set(handle.agent.session, FULL_ACCESS_PRESET);
|
|
663
|
+
return void 0;
|
|
664
|
+
} catch (error) {
|
|
665
|
+
return error instanceof Error ? error.message : String(error);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function resolvedRoots(hub) {
|
|
669
|
+
return hub.config.allowedRoots.map((root) => resolve2(expandHome(root)));
|
|
590
670
|
}
|
|
591
671
|
function resolveCwd(hub, requested) {
|
|
592
|
-
const roots = hub
|
|
672
|
+
const roots = resolvedRoots(hub);
|
|
593
673
|
if (roots.length === 0) return { error: DISPATCH_DISABLED };
|
|
594
674
|
const first = roots[0];
|
|
595
675
|
if (requested === void 0) return first === void 0 ? { error: DISPATCH_DISABLED } : first;
|
|
596
676
|
const target = resolve2(isAbsolute(requested) ? requested : expandHome(requested));
|
|
597
677
|
const inside = roots.some((root) => target === root || target.startsWith(root + sep));
|
|
598
|
-
return inside ? target : { error: `cwd \u4E0D\u5728\u5141\u8BB8\u76EE\u5F55\u5185\uFF1A${target}
|
|
678
|
+
return inside ? target : { error: complain(`cwd \u4E0D\u5728\u5141\u8BB8\u76EE\u5F55\u5185\uFF1A${target}`, "cwd-not-allowed", { cwd: target }) };
|
|
599
679
|
}
|
|
600
680
|
async function reportTurnFinal(hub, sessionId) {
|
|
601
681
|
const snapshot = await hub.ctx.sessionQuery.readSurface(SessionId(sessionId));
|
|
602
682
|
const ok2 = !hub.sessions.hasError(sessionId);
|
|
683
|
+
const text = lastAssistantText(snapshot.events);
|
|
684
|
+
const placeholder = text === void 0 ? complain("\uFF08\u672C\u8F6E\u6CA1\u6709\u4EA7\u751F\u52A9\u624B\u6587\u672C\uFF09", "no-assistant-text") : void 0;
|
|
603
685
|
const summary = clampBytes(
|
|
604
|
-
truncate(
|
|
686
|
+
truncate(text ?? placeholder.text, MAX_SUMMARY_CHARS),
|
|
605
687
|
MAX_FIELD_BYTES
|
|
606
688
|
);
|
|
607
689
|
hub.relay.send(
|
|
608
|
-
{
|
|
690
|
+
{
|
|
691
|
+
v: 1,
|
|
692
|
+
ts: Date.now(),
|
|
693
|
+
type: "turn.final",
|
|
694
|
+
sessionId,
|
|
695
|
+
ok: ok2,
|
|
696
|
+
summary,
|
|
697
|
+
...placeholder === void 0 ? {} : { reason: placeholder.reason }
|
|
698
|
+
},
|
|
609
699
|
{
|
|
610
700
|
v: 1,
|
|
611
701
|
ts: Date.now(),
|
|
@@ -623,13 +713,21 @@ function lastAssistantText(events) {
|
|
|
623
713
|
const text = event.data.message.content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
|
|
624
714
|
if (text !== "") return text;
|
|
625
715
|
}
|
|
626
|
-
return
|
|
716
|
+
return void 0;
|
|
627
717
|
}
|
|
628
718
|
function ok(requestId, sessionId) {
|
|
629
719
|
return { v: 1, ts: Date.now(), type: "dispatch.result", requestId, ok: true, sessionId };
|
|
630
720
|
}
|
|
631
|
-
function failure(requestId,
|
|
632
|
-
return {
|
|
721
|
+
function failure(requestId, complaint) {
|
|
722
|
+
return {
|
|
723
|
+
v: 1,
|
|
724
|
+
ts: Date.now(),
|
|
725
|
+
type: "dispatch.result",
|
|
726
|
+
requestId,
|
|
727
|
+
ok: false,
|
|
728
|
+
error: complaint.text,
|
|
729
|
+
reason: complaint.reason
|
|
730
|
+
};
|
|
633
731
|
}
|
|
634
732
|
var ResultCache = class {
|
|
635
733
|
#entries = /* @__PURE__ */ new Map();
|
|
@@ -650,14 +748,16 @@ var ResultCache = class {
|
|
|
650
748
|
// src/hub.ts
|
|
651
749
|
var ERROR_CHARS = 1e3;
|
|
652
750
|
function createHub(parts) {
|
|
653
|
-
const report = (context, message) => {
|
|
751
|
+
const report = (context, message, about = {}) => {
|
|
654
752
|
parts.log.error("%s: %s", context, message);
|
|
655
753
|
parts.relay.send({
|
|
656
754
|
v: 1,
|
|
657
755
|
ts: Date.now(),
|
|
658
756
|
type: "error",
|
|
659
757
|
message: truncate(message, ERROR_CHARS),
|
|
660
|
-
context
|
|
758
|
+
context,
|
|
759
|
+
...about.sessionId === void 0 ? {} : { sessionId: about.sessionId },
|
|
760
|
+
...about.reason === void 0 ? {} : { reason: about.reason }
|
|
661
761
|
});
|
|
662
762
|
};
|
|
663
763
|
return {
|
|
@@ -669,6 +769,195 @@ function createHub(parts) {
|
|
|
669
769
|
};
|
|
670
770
|
}
|
|
671
771
|
|
|
772
|
+
// src/questions.ts
|
|
773
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
774
|
+
var ASK_USER_QUESTION = "ask_user_question";
|
|
775
|
+
var PROMPT_CHARS = 2e3;
|
|
776
|
+
var PROMPT_BYTES = 4e3;
|
|
777
|
+
var LABEL_CHARS = 300;
|
|
778
|
+
var LABEL_BYTES = 600;
|
|
779
|
+
var REMINDER_MS2 = 5 * 6e4;
|
|
780
|
+
var MAX_REMINDERS2 = 6;
|
|
781
|
+
function installQuestions(hub) {
|
|
782
|
+
const pending = /* @__PURE__ */ new Map();
|
|
783
|
+
hub.ctx.on("tools/execute", (exec, next) => {
|
|
784
|
+
if (exec.name !== ASK_USER_QUESTION || exec.agent === void 0) return next();
|
|
785
|
+
if (!hub.pairing.everPaired || !hub.relay.connected) return next();
|
|
786
|
+
if (exec.signal.aborted) return next();
|
|
787
|
+
return forward2(hub, pending, exec, next);
|
|
788
|
+
});
|
|
789
|
+
return {
|
|
790
|
+
respond(message) {
|
|
791
|
+
const entry = pending.get(message.questionId);
|
|
792
|
+
if (entry === void 0) {
|
|
793
|
+
hub.relay.send(closedMsg2(message.questionId, "expired"));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const value = toAnswerValue(entry, message.answers);
|
|
797
|
+
if (typeof value === "string") {
|
|
798
|
+
hub.report(`question ${entry.questionId}`, value);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
entry.settle(successResult(value), "phone");
|
|
802
|
+
},
|
|
803
|
+
resendPending() {
|
|
804
|
+
for (const entry of pending.values()) hub.relay.send(entry.frame, entry.push);
|
|
805
|
+
},
|
|
806
|
+
openCount: () => pending.size
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function forward2(hub, pending, exec, next) {
|
|
810
|
+
const normalized = normalize(exec.arguments);
|
|
811
|
+
if (normalized === null) {
|
|
812
|
+
hub.report("question", "ask_user_question \u53C2\u6570\u65E0\u6CD5\u89E3\u6790\uFF0C\u5DF2\u4EA4\u56DE\u672C\u673A\u5904\u7406");
|
|
813
|
+
return next();
|
|
814
|
+
}
|
|
815
|
+
const questionId = randomUUID3();
|
|
816
|
+
const sessionId = exec.agent?.id ?? "";
|
|
817
|
+
const frame = {
|
|
818
|
+
v: 1,
|
|
819
|
+
ts: Date.now(),
|
|
820
|
+
type: "question.request",
|
|
821
|
+
questionId,
|
|
822
|
+
sessionId,
|
|
823
|
+
items: normalized.items,
|
|
824
|
+
createdAt: Date.now()
|
|
825
|
+
};
|
|
826
|
+
const push = {
|
|
827
|
+
v: 1,
|
|
828
|
+
ts: Date.now(),
|
|
829
|
+
type: "push",
|
|
830
|
+
kind: "question",
|
|
831
|
+
sessionId,
|
|
832
|
+
title: normalized.items[0]?.prompt.slice(0, 120) ?? "\u9700\u8981\u4F60\u56DE\u7B54\u4E00\u4E2A\u95EE\u9898"
|
|
833
|
+
};
|
|
834
|
+
if (!hub.relay.send(frame, push)) {
|
|
835
|
+
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");
|
|
836
|
+
return next();
|
|
837
|
+
}
|
|
838
|
+
const forwarded = { questionId, sessionId, frame, push, labels: normalized.labels };
|
|
839
|
+
return race2(hub, pending, forwarded, exec, next);
|
|
840
|
+
}
|
|
841
|
+
function race2(hub, pending, forwarded, exec, next) {
|
|
842
|
+
const { questionId } = forwarded;
|
|
843
|
+
return new Promise((resolve3) => {
|
|
844
|
+
let settled = false;
|
|
845
|
+
let localFailure;
|
|
846
|
+
const settle = (result, resolution) => {
|
|
847
|
+
if (settled) return;
|
|
848
|
+
settled = true;
|
|
849
|
+
const entry2 = pending.get(questionId);
|
|
850
|
+
if (entry2?.timer !== void 0) clearTimeout(entry2.timer);
|
|
851
|
+
pending.delete(questionId);
|
|
852
|
+
exec.signal.removeEventListener("abort", onAbort);
|
|
853
|
+
hub.relay.send(closedMsg2(questionId, resolution));
|
|
854
|
+
resolve3(result);
|
|
855
|
+
};
|
|
856
|
+
function onAbort() {
|
|
857
|
+
settle(localFailure ?? abortedResult(), "cancelled");
|
|
858
|
+
}
|
|
859
|
+
exec.signal.addEventListener("abort", onAbort, { once: true });
|
|
860
|
+
const entry = { ...forwarded, settle, reminders: 0, timer: void 0 };
|
|
861
|
+
pending.set(questionId, entry);
|
|
862
|
+
armReminder2(hub, entry);
|
|
863
|
+
void next().then((result) => {
|
|
864
|
+
if (!result.isError) settle(result, "local");
|
|
865
|
+
else localFailure = result;
|
|
866
|
+
}, (error) => {
|
|
867
|
+
hub.fail(`question ${questionId}: local provider failed`, error);
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
function armReminder2(hub, entry) {
|
|
872
|
+
entry.timer = setTimeout(() => {
|
|
873
|
+
entry.timer = void 0;
|
|
874
|
+
entry.reminders += 1;
|
|
875
|
+
hub.relay.send(entry.frame, entry.push);
|
|
876
|
+
if (entry.reminders >= MAX_REMINDERS2) {
|
|
877
|
+
hub.log.warn(
|
|
878
|
+
"question %s still unanswered after %d reminders; it stays open (never auto-answered)",
|
|
879
|
+
entry.questionId,
|
|
880
|
+
entry.reminders
|
|
881
|
+
);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
armReminder2(hub, entry);
|
|
885
|
+
}, REMINDER_MS2);
|
|
886
|
+
entry.timer.unref?.();
|
|
887
|
+
}
|
|
888
|
+
function normalize(args) {
|
|
889
|
+
const questions = args?.questions;
|
|
890
|
+
if (!Array.isArray(questions) || questions.length === 0) return null;
|
|
891
|
+
const items = [];
|
|
892
|
+
const labels = /* @__PURE__ */ new Map();
|
|
893
|
+
for (const raw of questions) {
|
|
894
|
+
if (typeof raw?.id !== "string" || typeof raw.question !== "string") return null;
|
|
895
|
+
const options = [];
|
|
896
|
+
const byId = /* @__PURE__ */ new Map();
|
|
897
|
+
(raw.options ?? []).forEach((option, index) => {
|
|
898
|
+
if (typeof option?.label !== "string") return;
|
|
899
|
+
const id = `o${String(index)}`;
|
|
900
|
+
byId.set(id, option.label);
|
|
901
|
+
options.push({ id, label: clamp(decorate(option.label, option.description), LABEL_CHARS, LABEL_BYTES) });
|
|
902
|
+
});
|
|
903
|
+
labels.set(raw.id, byId);
|
|
904
|
+
items.push({
|
|
905
|
+
id: raw.id,
|
|
906
|
+
prompt: clamp(prefix(raw.header, raw.question), PROMPT_CHARS, PROMPT_BYTES),
|
|
907
|
+
options,
|
|
908
|
+
multiSelect: raw.multi_select === true,
|
|
909
|
+
// The tool's output schema always permits `custom`, so free text is
|
|
910
|
+
// always a valid answer — and the only answer when there are no options.
|
|
911
|
+
allowFreeText: true
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
return items.length === 0 ? null : { items, labels };
|
|
915
|
+
}
|
|
916
|
+
var decorate = (label, description) => description === void 0 || description === "" ? label : `${label} \u2014 ${description}`;
|
|
917
|
+
var prefix = (header, question) => header === void 0 || header === "" ? question : `${header}
|
|
918
|
+
|
|
919
|
+
${question}`;
|
|
920
|
+
var clamp = (text, chars, bytes) => clampBytes(truncate(text, chars), bytes);
|
|
921
|
+
function toAnswerValue(entry, answers) {
|
|
922
|
+
if (!Array.isArray(answers)) return "\u56DE\u7B54\u683C\u5F0F\u65E0\u6548\uFF1Aanswers \u4E0D\u662F\u6570\u7EC4";
|
|
923
|
+
const seen = /* @__PURE__ */ new Set();
|
|
924
|
+
const projected = [];
|
|
925
|
+
for (const answer of answers) {
|
|
926
|
+
const options = entry.labels.get(answer.itemId);
|
|
927
|
+
if (options === void 0) return `\u56DE\u7B54\u5F15\u7528\u4E86\u672A\u77E5\u7684\u95EE\u9898 id\uFF1A${String(answer.itemId)}`;
|
|
928
|
+
if (seen.has(answer.itemId)) return `\u95EE\u9898 ${answer.itemId} \u88AB\u56DE\u7B54\u4E86\u591A\u6B21`;
|
|
929
|
+
seen.add(answer.itemId);
|
|
930
|
+
const selected = [];
|
|
931
|
+
for (const optionId of answer.optionIds ?? []) {
|
|
932
|
+
const label = options.get(optionId);
|
|
933
|
+
if (label === void 0) return `\u95EE\u9898 ${answer.itemId} \u7684\u9009\u9879 id \u672A\u77E5\uFF1A${String(optionId)}`;
|
|
934
|
+
selected.push(label);
|
|
935
|
+
}
|
|
936
|
+
const item = entry.frame.items.find((candidate) => candidate.id === answer.itemId);
|
|
937
|
+
if (item !== void 0 && !item.multiSelect && selected.length > 1) {
|
|
938
|
+
return `\u95EE\u9898 ${answer.itemId} \u662F\u5355\u9009\uFF0C\u5374\u6536\u5230 ${String(selected.length)} \u4E2A\u9009\u9879`;
|
|
939
|
+
}
|
|
940
|
+
const custom = answer.freeText;
|
|
941
|
+
if (selected.length === 0 && (custom === void 0 || custom === "")) {
|
|
942
|
+
return `\u95EE\u9898 ${answer.itemId} \u65E2\u6CA1\u6709\u9009\u9879\u4E5F\u6CA1\u6709\u6587\u5B57\u56DE\u7B54`;
|
|
943
|
+
}
|
|
944
|
+
projected.push({ id: answer.itemId, selected, ...custom === void 0 || custom === "" ? {} : { custom } });
|
|
945
|
+
}
|
|
946
|
+
const missing = entry.frame.items.filter((item) => !seen.has(item.id)).map((item) => item.id);
|
|
947
|
+
if (missing.length > 0) return `\u8FD8\u6709\u95EE\u9898\u672A\u56DE\u7B54\uFF1A${missing.join(", ")}`;
|
|
948
|
+
return { answers: projected };
|
|
949
|
+
}
|
|
950
|
+
function successResult(value) {
|
|
951
|
+
return { isError: false, value, content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
952
|
+
}
|
|
953
|
+
function abortedResult() {
|
|
954
|
+
const message = "ask_user_question was cancelled before the user answered";
|
|
955
|
+
return { isError: true, error: { message }, content: [{ type: "text", text: `Error: ${message}` }] };
|
|
956
|
+
}
|
|
957
|
+
function closedMsg2(questionId, resolution) {
|
|
958
|
+
return { v: 1, ts: Date.now(), type: "question.closed", questionId, resolution };
|
|
959
|
+
}
|
|
960
|
+
|
|
672
961
|
// src/relay-client.ts
|
|
673
962
|
var BACKOFF_BASE_MS = 500;
|
|
674
963
|
var BACKOFF_MAX_MS = 3e4;
|
|
@@ -818,7 +1107,12 @@ var RelayClient = class {
|
|
|
818
1107
|
#onPresence(frame) {
|
|
819
1108
|
const { role, online } = frame;
|
|
820
1109
|
if (role !== "phone" || typeof online !== "boolean") return;
|
|
821
|
-
|
|
1110
|
+
if (online) {
|
|
1111
|
+
this.#phoneOnline = true;
|
|
1112
|
+
this.#options.onPhonePresence(true);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
this.#setPhoneOnline(false);
|
|
822
1116
|
}
|
|
823
1117
|
#setPhoneOnline(online) {
|
|
824
1118
|
if (this.#phoneOnline === online) return;
|
|
@@ -904,6 +1198,7 @@ var RelayClient = class {
|
|
|
904
1198
|
var INBOUND_FIELDS = {
|
|
905
1199
|
"sessions.get": [],
|
|
906
1200
|
"approval.respond": ["requestId", "approvalId", "decision"],
|
|
1201
|
+
"question.respond": ["requestId", "questionId"],
|
|
907
1202
|
"dispatch.request": ["requestId", "prompt"],
|
|
908
1203
|
"session.message": ["requestId", "sessionId", "text"]
|
|
909
1204
|
};
|
|
@@ -921,18 +1216,33 @@ function parseInbound(value) {
|
|
|
921
1216
|
return null;
|
|
922
1217
|
}
|
|
923
1218
|
if (type === "dispatch.request" && typeof record["worktree"] !== "boolean") return null;
|
|
1219
|
+
if (type === "question.respond" && !isAnswerList(record["answers"])) return null;
|
|
924
1220
|
return record;
|
|
925
1221
|
}
|
|
1222
|
+
function isAnswerList(value) {
|
|
1223
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
1224
|
+
return value.every((entry) => {
|
|
1225
|
+
const answer = entry;
|
|
1226
|
+
if (typeof answer?.itemId !== "string") return false;
|
|
1227
|
+
if (!Array.isArray(answer.optionIds)) return false;
|
|
1228
|
+
if (!answer.optionIds.every((id) => typeof id === "string")) return false;
|
|
1229
|
+
return answer.freeText === void 0 || typeof answer.freeText === "string";
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
926
1232
|
|
|
927
1233
|
// src/sessions.ts
|
|
928
1234
|
import { SessionId as SessionId2 } from "@deepseek-ai/dsh-session";
|
|
929
1235
|
var TITLE_CHARS2 = 80;
|
|
1236
|
+
var ACTIVITY_CHARS = 40;
|
|
1237
|
+
var ACTIVITY_MIN_GAP_MS = 800;
|
|
930
1238
|
var SessionTracker = class {
|
|
931
1239
|
#ctx;
|
|
932
1240
|
#relay;
|
|
933
1241
|
#log;
|
|
934
1242
|
#tracked = /* @__PURE__ */ new Map();
|
|
935
1243
|
#published = /* @__PURE__ */ new Map();
|
|
1244
|
+
#activityAt = /* @__PURE__ */ new Map();
|
|
1245
|
+
#activityTimers = /* @__PURE__ */ new Map();
|
|
936
1246
|
constructor(ctx, relay, log) {
|
|
937
1247
|
this.#ctx = ctx;
|
|
938
1248
|
this.#relay = relay;
|
|
@@ -962,21 +1272,37 @@ var SessionTracker = class {
|
|
|
962
1272
|
});
|
|
963
1273
|
});
|
|
964
1274
|
this.#ctx.on("agent/turn-stopping", ({ agent }) => {
|
|
1275
|
+
this.#update(agent.id, (entry) => {
|
|
1276
|
+
entry.activity = void 0;
|
|
1277
|
+
entry.activityCallId = void 0;
|
|
1278
|
+
});
|
|
965
1279
|
void this.#refreshTitle(agent);
|
|
966
1280
|
});
|
|
967
1281
|
this.#ctx.on("agent/disposed", ({ agent }) => {
|
|
968
1282
|
this.#update(agent.id, (entry) => {
|
|
969
1283
|
entry.base = "done";
|
|
1284
|
+
entry.activity = void 0;
|
|
970
1285
|
});
|
|
971
1286
|
this.#tracked.delete(agent.id);
|
|
972
1287
|
this.#published.delete(agent.id);
|
|
1288
|
+
this.#clearActivityTimer(agent.id);
|
|
1289
|
+
});
|
|
1290
|
+
this.#ctx.on("session/event", (session, event) => {
|
|
1291
|
+
if (event.type === "tool/call") {
|
|
1292
|
+
this.#setActivity(session.id, truncate(event.data.name, ACTIVITY_CHARS), event.data.callId);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (event.type === "tool/result") {
|
|
1296
|
+
this.#clearActivity(session.id, event.data.message.content[0].toolCallId);
|
|
1297
|
+
}
|
|
973
1298
|
});
|
|
974
1299
|
}
|
|
975
1300
|
/** Mark a session as started by us, with the dispatch prompt as its title. */
|
|
976
|
-
markDispatched(sessionId, prompt) {
|
|
1301
|
+
markDispatched(sessionId, prompt, access = "standard") {
|
|
977
1302
|
this.#update(sessionId, (entry) => {
|
|
978
1303
|
entry.dispatched = true;
|
|
979
1304
|
entry.title = truncate(prompt.trim().split("\n", 1)[0] ?? "", TITLE_CHARS2);
|
|
1305
|
+
if (access === "full") entry.access = "full";
|
|
980
1306
|
});
|
|
981
1307
|
}
|
|
982
1308
|
isDispatched(sessionId) {
|
|
@@ -999,6 +1325,10 @@ var SessionTracker = class {
|
|
|
999
1325
|
agentOf(sessionId) {
|
|
1000
1326
|
return this.#ctx.agents.get(SessionId2(sessionId));
|
|
1001
1327
|
}
|
|
1328
|
+
/** Whether a follow-up would land: exactly the check `session.message` makes. */
|
|
1329
|
+
#isLive(sessionId) {
|
|
1330
|
+
return this.agentOf(sessionId) !== void 0;
|
|
1331
|
+
}
|
|
1002
1332
|
/** An approval for this session is now waiting on a human. */
|
|
1003
1333
|
approvalOpened(sessionId) {
|
|
1004
1334
|
this.#update(sessionId, (entry) => {
|
|
@@ -1018,7 +1348,7 @@ var SessionTracker = class {
|
|
|
1018
1348
|
if (!record.live) continue;
|
|
1019
1349
|
const id = record.header.id;
|
|
1020
1350
|
const known = this.#tracked.get(id) ?? this.#adopt(this.#ctx.agents.get(id));
|
|
1021
|
-
wire.push(known === void 0 ? this.#fallbackWire(id, record.header.cwd ?? "", record.header.createdAt) : toWire(known));
|
|
1351
|
+
wire.push(known === void 0 ? this.#fallbackWire(id, record.header.cwd ?? "", record.header.createdAt) : toWire(known, this.#isLive(id)));
|
|
1022
1352
|
}
|
|
1023
1353
|
return wire;
|
|
1024
1354
|
}
|
|
@@ -1029,7 +1359,10 @@ var SessionTracker = class {
|
|
|
1029
1359
|
cwd,
|
|
1030
1360
|
state: "idle",
|
|
1031
1361
|
lastActivity: createdAt,
|
|
1032
|
-
dispatched: false
|
|
1362
|
+
dispatched: false,
|
|
1363
|
+
access: "standard",
|
|
1364
|
+
// listSessions() already filtered to live records.
|
|
1365
|
+
live: true
|
|
1033
1366
|
};
|
|
1034
1367
|
}
|
|
1035
1368
|
#adopt(agent) {
|
|
@@ -1044,11 +1377,52 @@ var SessionTracker = class {
|
|
|
1044
1377
|
openApprovals: 0,
|
|
1045
1378
|
lastActivity: Date.now(),
|
|
1046
1379
|
dispatched: false,
|
|
1047
|
-
|
|
1380
|
+
access: "standard",
|
|
1381
|
+
finalized: false,
|
|
1382
|
+
activity: void 0,
|
|
1383
|
+
activityCallId: void 0
|
|
1048
1384
|
};
|
|
1049
1385
|
this.#tracked.set(agent.id, entry);
|
|
1050
1386
|
return entry;
|
|
1051
1387
|
}
|
|
1388
|
+
#setActivity(sessionId, name2, callId) {
|
|
1389
|
+
const entry = this.#tracked.get(sessionId);
|
|
1390
|
+
if (entry === void 0 || entry.activity === name2) return;
|
|
1391
|
+
entry.activity = name2;
|
|
1392
|
+
entry.activityCallId = callId;
|
|
1393
|
+
this.#publishActivity(sessionId);
|
|
1394
|
+
}
|
|
1395
|
+
/** Only the call that set the activity may clear it (tools can overlap). */
|
|
1396
|
+
#clearActivity(sessionId, callId) {
|
|
1397
|
+
const entry = this.#tracked.get(sessionId);
|
|
1398
|
+
if (entry === void 0 || entry.activityCallId !== callId) return;
|
|
1399
|
+
entry.activity = void 0;
|
|
1400
|
+
entry.activityCallId = void 0;
|
|
1401
|
+
this.#publishActivity(sessionId);
|
|
1402
|
+
}
|
|
1403
|
+
/** Trailing-edge coalescing, so a tool-heavy turn cannot flood the phone. */
|
|
1404
|
+
#publishActivity(sessionId) {
|
|
1405
|
+
const elapsed = Date.now() - (this.#activityAt.get(sessionId) ?? 0);
|
|
1406
|
+
if (elapsed >= ACTIVITY_MIN_GAP_MS) {
|
|
1407
|
+
this.#activityAt.set(sessionId, Date.now());
|
|
1408
|
+
this.#publish(sessionId);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (this.#activityTimers.has(sessionId)) return;
|
|
1412
|
+
const timer = setTimeout(() => {
|
|
1413
|
+
this.#activityTimers.delete(sessionId);
|
|
1414
|
+
this.#activityAt.set(sessionId, Date.now());
|
|
1415
|
+
this.#publish(sessionId);
|
|
1416
|
+
}, ACTIVITY_MIN_GAP_MS - elapsed);
|
|
1417
|
+
timer.unref?.();
|
|
1418
|
+
this.#activityTimers.set(sessionId, timer);
|
|
1419
|
+
}
|
|
1420
|
+
#clearActivityTimer(sessionId) {
|
|
1421
|
+
const timer = this.#activityTimers.get(sessionId);
|
|
1422
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1423
|
+
this.#activityTimers.delete(sessionId);
|
|
1424
|
+
this.#activityAt.delete(sessionId);
|
|
1425
|
+
}
|
|
1052
1426
|
#update(sessionId, mutate) {
|
|
1053
1427
|
const entry = this.#tracked.get(sessionId) ?? this.#adopt(this.#ctx.agents.get(SessionId2(sessionId)));
|
|
1054
1428
|
if (entry === void 0) return;
|
|
@@ -1060,8 +1434,8 @@ var SessionTracker = class {
|
|
|
1060
1434
|
#publish(sessionId) {
|
|
1061
1435
|
const entry = this.#tracked.get(sessionId);
|
|
1062
1436
|
if (entry === void 0) return;
|
|
1063
|
-
const session = toWire(entry);
|
|
1064
|
-
const fingerprint = `${session.state}|${session.title}|${session.cwd}|${String(session.dispatched)}`;
|
|
1437
|
+
const session = toWire(entry, this.#isLive(sessionId));
|
|
1438
|
+
const fingerprint = `${session.state}|${session.title}|${session.cwd}|${String(session.dispatched)}|${String(session.access)}|${String(session.live)}|${session.activity ?? ""}`;
|
|
1065
1439
|
if (this.#published.get(sessionId) === fingerprint) return;
|
|
1066
1440
|
this.#published.set(sessionId, fingerprint);
|
|
1067
1441
|
this.#relay.send({ v: 1, ts: Date.now(), type: "session.update", session });
|
|
@@ -1080,14 +1454,17 @@ var SessionTracker = class {
|
|
|
1080
1454
|
}
|
|
1081
1455
|
}
|
|
1082
1456
|
};
|
|
1083
|
-
function toWire(entry) {
|
|
1457
|
+
function toWire(entry, live) {
|
|
1084
1458
|
return {
|
|
1085
1459
|
sessionId: entry.sessionId,
|
|
1086
1460
|
title: entry.title,
|
|
1087
1461
|
cwd: entry.cwd,
|
|
1088
1462
|
state: stateOf(entry),
|
|
1089
1463
|
lastActivity: entry.lastActivity,
|
|
1090
|
-
dispatched: entry.dispatched
|
|
1464
|
+
dispatched: entry.dispatched,
|
|
1465
|
+
access: entry.access,
|
|
1466
|
+
live,
|
|
1467
|
+
...entry.activity === void 0 ? {} : { activity: entry.activity }
|
|
1091
1468
|
};
|
|
1092
1469
|
}
|
|
1093
1470
|
function stateOf(entry) {
|
|
@@ -1104,7 +1481,7 @@ function firstPrompt(agent) {
|
|
|
1104
1481
|
}
|
|
1105
1482
|
|
|
1106
1483
|
// src/version.ts
|
|
1107
|
-
var PLUGIN_VERSION = "0.
|
|
1484
|
+
var PLUGIN_VERSION = "0.3.0";
|
|
1108
1485
|
|
|
1109
1486
|
// src/config.ts
|
|
1110
1487
|
import { hostname } from "os";
|
|
@@ -1113,6 +1490,7 @@ var Config = z.object({
|
|
|
1113
1490
|
relay: z.string().required(),
|
|
1114
1491
|
machineName: z.string().default(hostname()),
|
|
1115
1492
|
allowedRoots: z.array(z.string()).default([]),
|
|
1493
|
+
allowFullAccessDispatch: z.boolean().default(false),
|
|
1116
1494
|
dataDir: z.string().default("~/.dsh-dispatch"),
|
|
1117
1495
|
pwaUrl: z.string().default("http://localhost:5173")
|
|
1118
1496
|
});
|
|
@@ -1150,17 +1528,20 @@ function apply(ctx, config) {
|
|
|
1150
1528
|
const sessions = new SessionTracker(ctx, relay, log);
|
|
1151
1529
|
const hub = createHub({ ctx, config, log, relay, sessions, pairing });
|
|
1152
1530
|
const approvals = installApprovals(hub);
|
|
1531
|
+
const questions = installQuestions(hub);
|
|
1153
1532
|
const dispatch = installDispatch(hub);
|
|
1533
|
+
const routers = { approvals, questions, dispatch };
|
|
1154
1534
|
deferred.message = (message) => {
|
|
1155
|
-
route(hub,
|
|
1535
|
+
route(hub, routers, message);
|
|
1156
1536
|
};
|
|
1157
1537
|
deferred.hello = () => {
|
|
1158
1538
|
publishStatus(hub);
|
|
1159
1539
|
approvals.resendPending();
|
|
1540
|
+
questions.resendPending();
|
|
1160
1541
|
void publishSnapshot(hub);
|
|
1161
1542
|
};
|
|
1162
1543
|
sessions.install();
|
|
1163
|
-
installCommands(hub,
|
|
1544
|
+
installCommands(hub, routers);
|
|
1164
1545
|
startRelay(hub);
|
|
1165
1546
|
}
|
|
1166
1547
|
function startRelay(hub) {
|
|
@@ -1191,7 +1572,8 @@ function startRelay(hub) {
|
|
|
1191
1572
|
};
|
|
1192
1573
|
}, "dsh-dispatch: status heartbeat");
|
|
1193
1574
|
}
|
|
1194
|
-
function route(hub,
|
|
1575
|
+
function route(hub, routers, message) {
|
|
1576
|
+
const { approvals, questions, dispatch } = routers;
|
|
1195
1577
|
switch (message.type) {
|
|
1196
1578
|
case "sessions.get":
|
|
1197
1579
|
void publishSnapshot(hub);
|
|
@@ -1199,6 +1581,9 @@ function route(hub, approvals, dispatch, message) {
|
|
|
1199
1581
|
case "approval.respond":
|
|
1200
1582
|
approvals.respond(message);
|
|
1201
1583
|
return;
|
|
1584
|
+
case "question.respond":
|
|
1585
|
+
questions.respond(message);
|
|
1586
|
+
return;
|
|
1202
1587
|
case "dispatch.request":
|
|
1203
1588
|
void dispatch.request(message).catch((error) => {
|
|
1204
1589
|
hub.fail(`dispatch.request ${message.requestId}`, error);
|
|
@@ -1216,7 +1601,12 @@ function publishStatus(hub) {
|
|
|
1216
1601
|
ts: Date.now(),
|
|
1217
1602
|
type: "machine.status",
|
|
1218
1603
|
machine: hub.config.machineName,
|
|
1219
|
-
pluginVersion: PLUGIN_VERSION
|
|
1604
|
+
pluginVersion: PLUGIN_VERSION,
|
|
1605
|
+
capabilities: {
|
|
1606
|
+
fullAccessDispatch: hub.config.allowFullAccessDispatch,
|
|
1607
|
+
questionForwarding: true
|
|
1608
|
+
},
|
|
1609
|
+
allowedRoots: resolvedRoots(hub)
|
|
1220
1610
|
});
|
|
1221
1611
|
}
|
|
1222
1612
|
async function publishSnapshot(hub) {
|