@klarkxy/dsh-session-manager 0.5.0 → 0.5.1
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/lib/client.inner.cjs.map +1 -1
- package/lib/index.js +840 -55
- package/lib/index.js.map +1 -1
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -7,8 +7,12 @@ import { deriveEventMessage, foldSurface, isAppendSurfaceEvent } from "@deepseek
|
|
|
7
7
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
8
8
|
import { SessionQueryError, SessionSearchCursor } from "@deepseek-ai/dsh-session-query";
|
|
9
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
|
+
import { appendFileSync, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, unlinkSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
10
15
|
import { lstat, mkdir, realpath } from "node:fs/promises";
|
|
11
|
-
import { join } from "node:path";
|
|
12
16
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
13
17
|
//#region src/contracts.ts
|
|
14
18
|
/** Shared names and bounds for cross-session discovery. */
|
|
@@ -535,57 +539,89 @@ async function callLlmText(llm, request) {
|
|
|
535
539
|
request.signal.throwIfAborted();
|
|
536
540
|
const live = () => request.isCurrent?.() !== false && !request.signal.aborted;
|
|
537
541
|
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
538
|
-
const
|
|
539
|
-
const
|
|
542
|
+
const started = Date.now();
|
|
543
|
+
const logBase = {
|
|
544
|
+
plugin: request.plugin,
|
|
545
|
+
feature: request.feature ?? null,
|
|
540
546
|
provider: request.route.provider,
|
|
541
547
|
model: request.route.model,
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}, request.signal, request.configure);
|
|
545
|
-
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
546
|
-
const maxTokens = request.maxTokens === void 0 ? prepared.config.maxTokens : Math.min(request.maxTokens, prepared.config.maxTokens ?? request.maxTokens);
|
|
547
|
-
const options = {
|
|
548
|
-
provider: prepared.config.provider,
|
|
549
|
-
model: prepared.config.model,
|
|
550
|
-
...prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {},
|
|
551
|
-
...maxTokens !== void 0 ? { maxTokens } : {},
|
|
552
|
-
...prepared.config.temperature !== void 0 ? { temperature: prepared.config.temperature } : {},
|
|
553
|
-
...prepared.config.stop ? { stop: [...prepared.config.stop] } : {},
|
|
554
|
-
system: request.system,
|
|
555
|
-
messages: [createUserMessage({
|
|
556
|
-
source: producerMessageSource(request.plugin),
|
|
557
|
-
content: [{
|
|
558
|
-
type: "text",
|
|
559
|
-
text: buildAuxiliaryUserText(request)
|
|
560
|
-
}]
|
|
561
|
-
})],
|
|
562
|
-
signal: request.signal,
|
|
563
|
-
...request.purpose ? { purpose: request.purpose } : {},
|
|
564
|
-
...request.sessionId ? { sessionId: request.sessionId } : {}
|
|
548
|
+
purpose: request.purpose ?? null,
|
|
549
|
+
sessionId: request.sessionId ?? null
|
|
565
550
|
};
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
551
|
+
request.logger?.debug("llm.call.started", "Auxiliary model call started.", logBase);
|
|
552
|
+
try {
|
|
553
|
+
const effort = request.route.reasoningEffort?.trim();
|
|
554
|
+
const prepared = await llm.prepareCall({
|
|
555
|
+
provider: request.route.provider,
|
|
556
|
+
model: request.route.model,
|
|
557
|
+
...effort ? { reasoningEffort: ReasoningEffortId(effort) } : {},
|
|
558
|
+
...request.maxTokens !== void 0 ? { maxTokens: request.maxTokens } : {}
|
|
559
|
+
}, request.signal, request.configure);
|
|
560
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
561
|
+
const maxTokens = request.maxTokens === void 0 ? prepared.config.maxTokens : Math.min(request.maxTokens, prepared.config.maxTokens ?? request.maxTokens);
|
|
562
|
+
const options = {
|
|
563
|
+
provider: prepared.config.provider,
|
|
564
|
+
model: prepared.config.model,
|
|
565
|
+
...prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {},
|
|
566
|
+
...maxTokens !== void 0 ? { maxTokens } : {},
|
|
567
|
+
...prepared.config.temperature !== void 0 ? { temperature: prepared.config.temperature } : {},
|
|
568
|
+
...prepared.config.stop ? { stop: [...prepared.config.stop] } : {},
|
|
569
|
+
system: request.system,
|
|
570
|
+
messages: [createUserMessage({
|
|
571
|
+
source: producerMessageSource(request.plugin),
|
|
572
|
+
content: [{
|
|
573
|
+
type: "text",
|
|
574
|
+
text: buildAuxiliaryUserText(request)
|
|
575
|
+
}]
|
|
576
|
+
})],
|
|
577
|
+
signal: request.signal,
|
|
578
|
+
...request.purpose ? { purpose: request.purpose } : {},
|
|
579
|
+
...request.sessionId ? { sessionId: request.sessionId } : {}
|
|
580
|
+
};
|
|
581
|
+
const assembler = await readStream(prepared.stream(options), request.signal, live);
|
|
582
|
+
if (request.signal.aborted || assembler.finish.kind === "aborted") throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
583
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
584
|
+
if (assembler.finish.kind === "error") {
|
|
585
|
+
const code = assembler.finish.failure.code;
|
|
586
|
+
throw Object.assign(new Error(assembler.finish.failure.message || "模型调用失败。"), { code });
|
|
587
|
+
}
|
|
588
|
+
if (assembler.blocks().some((block) => block.type === "tool-call") || assembler.finish.kind === "tool-calls") throw new Error("这次调用不使用工具。");
|
|
589
|
+
if (assembler.finish.kind !== "stop") throw new Error("模型调用未完成。");
|
|
590
|
+
const usage = assembler.usage;
|
|
591
|
+
request.logger?.debug("llm.call.completed", "Auxiliary model call completed.", {
|
|
592
|
+
...logBase,
|
|
593
|
+
provider: prepared.config.provider,
|
|
594
|
+
model: prepared.config.model,
|
|
595
|
+
durationMs: Date.now() - started,
|
|
596
|
+
...usage?.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
|
|
597
|
+
...usage?.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
|
|
598
|
+
...usage?.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {}
|
|
599
|
+
});
|
|
600
|
+
return {
|
|
601
|
+
text: joinText(assembler),
|
|
602
|
+
provider: prepared.config.provider,
|
|
603
|
+
model: prepared.config.model,
|
|
604
|
+
...usage ? {
|
|
605
|
+
inputTokens: usage.inputTokens,
|
|
606
|
+
outputTokens: usage.outputTokens,
|
|
607
|
+
...usage.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {},
|
|
608
|
+
...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
|
|
609
|
+
...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
|
|
610
|
+
...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {}
|
|
611
|
+
} : {}
|
|
612
|
+
};
|
|
613
|
+
} catch (error) {
|
|
614
|
+
const durationMs = Date.now() - started;
|
|
615
|
+
if (request.signal.aborted || error instanceof Error && error.name === "AbortError") request.logger?.debug("llm.call.cancelled", "Auxiliary model call cancelled.", {
|
|
616
|
+
...logBase,
|
|
617
|
+
durationMs
|
|
618
|
+
});
|
|
619
|
+
else request.logger?.warn("llm.call.failed", "Auxiliary model call failed.", error, {
|
|
620
|
+
...logBase,
|
|
621
|
+
durationMs
|
|
622
|
+
});
|
|
623
|
+
throw error;
|
|
572
624
|
}
|
|
573
|
-
if (assembler.blocks().some((block) => block.type === "tool-call") || assembler.finish.kind === "tool-calls") throw new Error("这次调用不使用工具。");
|
|
574
|
-
if (assembler.finish.kind !== "stop") throw new Error("模型调用未完成。");
|
|
575
|
-
const usage = assembler.usage;
|
|
576
|
-
return {
|
|
577
|
-
text: joinText(assembler),
|
|
578
|
-
provider: prepared.config.provider,
|
|
579
|
-
model: prepared.config.model,
|
|
580
|
-
...usage ? {
|
|
581
|
-
inputTokens: usage.inputTokens,
|
|
582
|
-
outputTokens: usage.outputTokens,
|
|
583
|
-
...usage.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {},
|
|
584
|
-
...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
|
|
585
|
-
...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
|
|
586
|
-
...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {}
|
|
587
|
-
} : {}
|
|
588
|
-
};
|
|
589
625
|
}
|
|
590
626
|
//#endregion
|
|
591
627
|
//#region src/turns.ts
|
|
@@ -3638,12 +3674,648 @@ function num(args, key) {
|
|
|
3638
3674
|
return typeof value === "number" ? value : void 0;
|
|
3639
3675
|
}
|
|
3640
3676
|
//#endregion
|
|
3677
|
+
//#region ../dsh-plugin-kit/src/log-shared.ts
|
|
3678
|
+
const PLUGIN_LOG_LEVELS = [
|
|
3679
|
+
"debug",
|
|
3680
|
+
"info",
|
|
3681
|
+
"warn",
|
|
3682
|
+
"error",
|
|
3683
|
+
"fault"
|
|
3684
|
+
];
|
|
3685
|
+
const DEFAULT_PLUGIN_LOG_LEVEL = "info";
|
|
3686
|
+
const PLUGIN_LOG_MAX_MESSAGE_CHARS = 16e3;
|
|
3687
|
+
const SOURCE_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
3688
|
+
const EVENT_RE = /^[a-z][a-z0-9._-]{0,127}$/;
|
|
3689
|
+
const CONTEXT_KEY_RE = /^[a-zA-Z][a-zA-Z0-9._-]{0,63}$/;
|
|
3690
|
+
const SECRET_KEY_RE = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|bearer|authorization|cookie|set[_-]?cookie|nonce|credential|private[_-]?key|password|secret|app[_-]?secret|client[_-]?secret|z_c0|d_c0)$/i;
|
|
3691
|
+
const CHAT_PAYLOAD_RE = /"role"\s*:\s*"(?:system|user|assistant|tool)"|"(?:messages|content|prompt|input_text|input_image|image_url)"\s*:\s*[\[{"']/i;
|
|
3692
|
+
const CREDENTIAL_BODY_RE = /-----BEGIN (?:RSA |EC |OPENSSH |PRIVATE )?PRIVATE KEY-----|"type"\s*:\s*"service_account"|credentials\.json|\.dsh[/\\](?:credentials|auth)/i;
|
|
3693
|
+
const AUTH_HEADER_RE = /\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie)\s*:\s*[^\r\n]*/gi;
|
|
3694
|
+
const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+=/]{8,}/g;
|
|
3695
|
+
const SECRET_FIELD_RE = /(["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|bearer|authorization|cookie|set-cookie|nonce|credential|private[_-]?key|password|secret|app[_-]?secret|client[_-]?secret|password|z_c0|d_c0)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,&;}]+)/gi;
|
|
3696
|
+
const SECRET_QUERY_RE = /([?&](?:token|api[_-]?key|key|secret|password|bootstrap|code|sig|signature|credential|password)=)[^\s&#"']+/gi;
|
|
3697
|
+
const URL_CREDENTIAL_RE = /\b(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
|
|
3698
|
+
const BOOTSTRAP_RE = /\/bootstrap\/[^\s"'?&#]+/gi;
|
|
3699
|
+
const KNOWN_TOKEN_RE = /\b(?:sk-[A-Za-z0-9_-]{10,}|gh[pousr]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{20,}|tvly-[A-Za-z0-9_-]{10,}|AKIA[0-9A-Z]{16})\b/g;
|
|
3700
|
+
const PATH_RE = /(?:[A-Za-z]:\\|\\\\)[^\s"'`<>|]+|(?:\/(?:home|Users|tmp|var|root)\/|~\/)[^\s"'`<>|]+/g;
|
|
3701
|
+
function isPluginLogLevel(value) {
|
|
3702
|
+
return typeof value === "string" && PLUGIN_LOG_LEVELS.includes(value);
|
|
3703
|
+
}
|
|
3704
|
+
function pluginLogLevelEnabled(level, minimum) {
|
|
3705
|
+
return PLUGIN_LOG_LEVELS.indexOf(level) >= PLUGIN_LOG_LEVELS.indexOf(minimum);
|
|
3706
|
+
}
|
|
3707
|
+
function normalizePluginLogSource(raw) {
|
|
3708
|
+
const stripped = String(raw || "").trim().replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3709
|
+
const bounded = (/^[a-z]/.test(stripped) ? stripped : `p-${stripped}`).slice(0, 64).replace(/-+$/g, "");
|
|
3710
|
+
return SOURCE_RE.test(bounded) ? bounded : "dsh-plugin";
|
|
3711
|
+
}
|
|
3712
|
+
function normalizePluginLogEvent(raw) {
|
|
3713
|
+
const cleaned = String(raw || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z]+/, "").slice(0, 128);
|
|
3714
|
+
return EVENT_RE.test(cleaned) ? cleaned : "plugin.event";
|
|
3715
|
+
}
|
|
3716
|
+
/**
|
|
3717
|
+
* Parse a level specification like `debug` or `dsh-zhihu=debug,dsh-dot:warn,info`.
|
|
3718
|
+
* Source-specific rules win over a bare fallback level.
|
|
3719
|
+
*/
|
|
3720
|
+
function parsePluginLogLevel(spec, source, fallback = DEFAULT_PLUGIN_LOG_LEVEL) {
|
|
3721
|
+
if (!spec || typeof spec !== "string") return fallback;
|
|
3722
|
+
const normalizedSource = source ? normalizePluginLogSource(source) : void 0;
|
|
3723
|
+
let wildcard;
|
|
3724
|
+
for (const token of spec.split(",").map((part) => part.trim()).filter(Boolean)) {
|
|
3725
|
+
const lowered = token.toLowerCase();
|
|
3726
|
+
if (isPluginLogLevel(lowered)) {
|
|
3727
|
+
wildcard = lowered;
|
|
3728
|
+
continue;
|
|
3729
|
+
}
|
|
3730
|
+
const match = /^([^=:]+)[=:]([a-z]+)$/i.exec(token);
|
|
3731
|
+
if (!match) continue;
|
|
3732
|
+
const target = match[1].trim();
|
|
3733
|
+
const candidate = match[2].trim().toLowerCase();
|
|
3734
|
+
if (!isPluginLogLevel(candidate)) continue;
|
|
3735
|
+
if (target === "*") {
|
|
3736
|
+
wildcard = candidate;
|
|
3737
|
+
continue;
|
|
3738
|
+
}
|
|
3739
|
+
if (normalizedSource && normalizePluginLogSource(target) === normalizedSource) return candidate;
|
|
3740
|
+
}
|
|
3741
|
+
return wildcard ?? fallback;
|
|
3742
|
+
}
|
|
3743
|
+
function sanitizePluginLogText(text, maxChars = PLUGIN_LOG_MAX_MESSAGE_CHARS) {
|
|
3744
|
+
if (!text) return "";
|
|
3745
|
+
if (CREDENTIAL_BODY_RE.test(text)) return "[omitted credential material]";
|
|
3746
|
+
if (CHAT_PAYLOAD_RE.test(text)) return "[omitted chat payload]";
|
|
3747
|
+
return truncateLogText(text.replace(AUTH_HEADER_RE, "$1: [redacted]").replace(BEARER_RE, "Bearer [redacted]").replace(SECRET_FIELD_RE, (full, prefix) => {
|
|
3748
|
+
const value = full.slice(prefix.length);
|
|
3749
|
+
if (value.startsWith("\"")) return `${prefix}"[redacted]"`;
|
|
3750
|
+
if (value.startsWith("'")) return `${prefix}'[redacted]'`;
|
|
3751
|
+
return `${prefix}[redacted]`;
|
|
3752
|
+
}).replace(SECRET_QUERY_RE, "$1[redacted]").replace(URL_CREDENTIAL_RE, "$1[redacted]@").replace(BOOTSTRAP_RE, "/bootstrap/[redacted]").replace(KNOWN_TOKEN_RE, "[redacted]").replace(PATH_RE, "[path]"), maxChars);
|
|
3753
|
+
}
|
|
3754
|
+
function sanitizePluginLogContext(input) {
|
|
3755
|
+
if (!input) return void 0;
|
|
3756
|
+
const output = {};
|
|
3757
|
+
for (const [key, value] of Object.entries(input).slice(0, 32)) {
|
|
3758
|
+
if (value === void 0 || !CONTEXT_KEY_RE.test(key)) continue;
|
|
3759
|
+
if (typeof value === "number" && !Number.isFinite(value)) continue;
|
|
3760
|
+
if (SECRET_KEY_RE.test(key)) {
|
|
3761
|
+
output[key] = "[redacted]";
|
|
3762
|
+
continue;
|
|
3763
|
+
}
|
|
3764
|
+
output[key] = typeof value === "string" ? sanitizePluginLogText(value, 1e3) : value;
|
|
3765
|
+
}
|
|
3766
|
+
if (!Object.keys(output).length) return void 0;
|
|
3767
|
+
if (JSON.stringify(output).length <= 8e3) return output;
|
|
3768
|
+
return {
|
|
3769
|
+
omitted: true,
|
|
3770
|
+
reason: "Diagnostic context exceeded its size limit."
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
function sanitizePluginLogError(error) {
|
|
3774
|
+
if (error === void 0) return void 0;
|
|
3775
|
+
const row = error && typeof error === "object" ? error : {};
|
|
3776
|
+
const rawMessage = typeof row.message === "string" ? row.message : String(error);
|
|
3777
|
+
const result = {
|
|
3778
|
+
name: typeof row.name === "string" && row.name.trim() ? truncateLogText(row.name.trim(), 100) : "Error",
|
|
3779
|
+
message: sanitizePluginLogText(rawMessage) || "Unknown error"
|
|
3780
|
+
};
|
|
3781
|
+
if (typeof row.stack === "string" && row.stack.trim()) result.stack = sanitizePluginLogText(row.stack, 24e3);
|
|
3782
|
+
if (typeof row.code === "string" && /^[A-Za-z0-9_./:-]{1,100}$/.test(row.code)) result.code = row.code;
|
|
3783
|
+
else if (typeof row.code === "number" && Number.isFinite(row.code)) result.code = String(row.code);
|
|
3784
|
+
return result;
|
|
3785
|
+
}
|
|
3786
|
+
function mergePluginLogContext(base, extra) {
|
|
3787
|
+
if (!base) return extra;
|
|
3788
|
+
if (!extra) return base;
|
|
3789
|
+
return {
|
|
3790
|
+
...base,
|
|
3791
|
+
...extra
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
function buildPluginLogEntry(input, meta) {
|
|
3795
|
+
const entry = {
|
|
3796
|
+
version: 1,
|
|
3797
|
+
at: meta.at.toISOString(),
|
|
3798
|
+
level: isPluginLogLevel(input.level) ? input.level : "info",
|
|
3799
|
+
source: normalizePluginLogSource(meta.source),
|
|
3800
|
+
surface: meta.surface,
|
|
3801
|
+
...meta.profile ? { profile: meta.profile } : {},
|
|
3802
|
+
event: normalizePluginLogEvent(input.event),
|
|
3803
|
+
message: sanitizePluginLogText(input.message || "No diagnostic message was provided.", PLUGIN_LOG_MAX_MESSAGE_CHARS),
|
|
3804
|
+
...meta.pid !== void 0 ? { pid: meta.pid } : {}
|
|
3805
|
+
};
|
|
3806
|
+
const context = sanitizePluginLogContext(mergePluginLogContext(meta.correlation, input.context));
|
|
3807
|
+
if (context) entry.context = context;
|
|
3808
|
+
const error = sanitizePluginLogError(input.error);
|
|
3809
|
+
if (error) entry.error = error;
|
|
3810
|
+
return entry;
|
|
3811
|
+
}
|
|
3812
|
+
function createChildPluginLogger(parent, bound) {
|
|
3813
|
+
return {
|
|
3814
|
+
get source() {
|
|
3815
|
+
return parent.source;
|
|
3816
|
+
},
|
|
3817
|
+
get file() {
|
|
3818
|
+
return parent.file;
|
|
3819
|
+
},
|
|
3820
|
+
get lastWriteError() {
|
|
3821
|
+
return parent.lastWriteError;
|
|
3822
|
+
},
|
|
3823
|
+
debug: (event, message, context) => parent.debug(event, message, mergePluginLogContext(bound, context)),
|
|
3824
|
+
info: (event, message, context) => parent.info(event, message, mergePluginLogContext(bound, context)),
|
|
3825
|
+
warn: (event, message, error, context) => parent.warn(event, message, error, mergePluginLogContext(bound, context)),
|
|
3826
|
+
error: (event, message, error, context) => parent.error(event, message, error, mergePluginLogContext(bound, context)),
|
|
3827
|
+
fault: (event, message, error, context) => parent.fault(event, message, error, mergePluginLogContext(bound, context)),
|
|
3828
|
+
write: (input) => parent.write({
|
|
3829
|
+
...input,
|
|
3830
|
+
context: mergePluginLogContext(bound, input.context)
|
|
3831
|
+
}),
|
|
3832
|
+
child: (next) => createChildPluginLogger(parent, mergePluginLogContext(bound, next) ?? {})
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
function truncateLogText(text, maxChars) {
|
|
3836
|
+
if (text.length <= maxChars) return text;
|
|
3837
|
+
const marker = "[truncated]\n";
|
|
3838
|
+
if (maxChars <= 12) return text.slice(0, Math.max(0, maxChars));
|
|
3839
|
+
return `${marker}${text.slice(-(maxChars - 12))}`;
|
|
3840
|
+
}
|
|
3841
|
+
const PROFILE_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
3842
|
+
const ARCHIVE_RE = /^[a-z][a-z0-9-]{0,63}\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z\.\d+\.\d+\.jsonl$/;
|
|
3843
|
+
const correlationStorage = new AsyncLocalStorage();
|
|
3844
|
+
const processFileLoggers = /* @__PURE__ */ new Map();
|
|
3845
|
+
let operationSequence = 0;
|
|
3846
|
+
function safeProfileName(profile) {
|
|
3847
|
+
const trimmed = String(profile ?? "").trim();
|
|
3848
|
+
if (PROFILE_RE.test(trimmed)) return trimmed.toLowerCase();
|
|
3849
|
+
const cleaned = trimmed.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
3850
|
+
return cleaned && PROFILE_RE.test(cleaned) ? cleaned : "default";
|
|
3851
|
+
}
|
|
3852
|
+
function pluginLogDirectory(home, profile = "default") {
|
|
3853
|
+
return join(resolve(home), "logs", "plugins", safeProfileName(profile));
|
|
3854
|
+
}
|
|
3855
|
+
function resolveHostLogTarget(ctx, options = {}) {
|
|
3856
|
+
const env = options.env ?? process.env;
|
|
3857
|
+
if (options.directory?.trim()) {
|
|
3858
|
+
const dir = resolve(options.directory.trim());
|
|
3859
|
+
return {
|
|
3860
|
+
directory: dir,
|
|
3861
|
+
anchor: dir,
|
|
3862
|
+
...options.profile ? { profile: safeProfileName(options.profile) } : {}
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
if (options.home?.trim()) {
|
|
3866
|
+
const home = resolve(options.home.trim());
|
|
3867
|
+
const profile = safeProfileName(options.profile);
|
|
3868
|
+
return {
|
|
3869
|
+
directory: pluginLogDirectory(home, profile),
|
|
3870
|
+
anchor: home,
|
|
3871
|
+
profile
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
if (env.DSH_PLUGIN_LOG_DIR?.trim()) {
|
|
3875
|
+
const dir = resolve(env.DSH_PLUGIN_LOG_DIR.trim());
|
|
3876
|
+
return {
|
|
3877
|
+
directory: dir,
|
|
3878
|
+
anchor: dir,
|
|
3879
|
+
...options.profile ? { profile: safeProfileName(options.profile) } : {}
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
const host = ctx && typeof ctx === "object" ? ctx : void 0;
|
|
3883
|
+
let profileCtx;
|
|
3884
|
+
try {
|
|
3885
|
+
profileCtx = host?.get?.("profileContext", false);
|
|
3886
|
+
} catch {
|
|
3887
|
+
try {
|
|
3888
|
+
profileCtx = host?.get?.("profileContext");
|
|
3889
|
+
} catch {
|
|
3890
|
+
profileCtx = void 0;
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
if (typeof profileCtx?.home === "string" && profileCtx.home.trim() && typeof profileCtx.dir === "string" && profileCtx.dir.trim()) {
|
|
3894
|
+
const home = resolve(profileCtx.home.trim());
|
|
3895
|
+
const dir = resolve(profileCtx.dir.trim());
|
|
3896
|
+
if (isInside(home, dir)) {
|
|
3897
|
+
const profile = safeProfileName(typeof profileCtx.name === "string" ? profileCtx.name : basename(dir));
|
|
3898
|
+
return {
|
|
3899
|
+
directory: pluginLogDirectory(home, profile),
|
|
3900
|
+
anchor: home,
|
|
3901
|
+
profile
|
|
3902
|
+
};
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
if (!Boolean(env.VITEST || env.NODE_TEST_CONTEXT) && typeof host?.baseUrl === "string" && host.baseUrl.trim()) {
|
|
3906
|
+
const basePath = pathFromBaseUrl(host.baseUrl);
|
|
3907
|
+
const configuredHome = resolve((env.DSH_HOME?.trim() || join(homedir(), ".dsh")).trim());
|
|
3908
|
+
const profilesRoot = join(configuredHome, "profiles");
|
|
3909
|
+
if (basePath && isInside(profilesRoot, basePath) && !samePath(profilesRoot, basePath)) {
|
|
3910
|
+
const firstSegment = basePath.slice(profilesRoot.length).replace(/^[/\\]+/, "").split(/[/\\]/)[0];
|
|
3911
|
+
if (firstSegment) {
|
|
3912
|
+
const profile = safeProfileName(firstSegment);
|
|
3913
|
+
return {
|
|
3914
|
+
directory: pluginLogDirectory(configuredHome, profile),
|
|
3915
|
+
anchor: configuredHome,
|
|
3916
|
+
profile
|
|
3917
|
+
};
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
return {
|
|
3922
|
+
directory: null,
|
|
3923
|
+
anchor: null,
|
|
3924
|
+
...options.profile ? { profile: safeProfileName(options.profile) } : {}
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
/**
|
|
3928
|
+
* Synchronous, append-only rotating JSONL logger modeled on DSH Spaces
|
|
3929
|
+
* diagnostics: 7-day or 100 MiB active rotation, 7-day / 100 MiB archive
|
|
3930
|
+
* retention, secret and path redaction, and non-throwing write resilience.
|
|
3931
|
+
*/
|
|
3932
|
+
var RotatingPluginLogger = class {
|
|
3933
|
+
source;
|
|
3934
|
+
file;
|
|
3935
|
+
lastError = null;
|
|
3936
|
+
size = 0;
|
|
3937
|
+
openedAt = 0;
|
|
3938
|
+
initialized = false;
|
|
3939
|
+
writing = false;
|
|
3940
|
+
sequence = 0;
|
|
3941
|
+
loggedWriteFailure = false;
|
|
3942
|
+
recent = [];
|
|
3943
|
+
directory;
|
|
3944
|
+
anchor;
|
|
3945
|
+
profile;
|
|
3946
|
+
minimum;
|
|
3947
|
+
now;
|
|
3948
|
+
pid;
|
|
3949
|
+
maxFileBytes;
|
|
3950
|
+
maxArchiveBytes;
|
|
3951
|
+
maxAgeMs;
|
|
3952
|
+
recentLimit;
|
|
3953
|
+
constructor(source, options = {}) {
|
|
3954
|
+
this.source = normalizePluginLogSource(source);
|
|
3955
|
+
const env = options.env ?? process.env;
|
|
3956
|
+
const resolved = options.directory !== void 0 || options.home !== void 0 ? resolveHostLogTarget(void 0, options) : {
|
|
3957
|
+
directory: null,
|
|
3958
|
+
anchor: options.anchor ?? null,
|
|
3959
|
+
profile: options.profile
|
|
3960
|
+
};
|
|
3961
|
+
this.directory = resolved.directory;
|
|
3962
|
+
this.anchor = options.anchor ?? resolved.anchor;
|
|
3963
|
+
this.profile = options.profile ?? resolved.profile;
|
|
3964
|
+
this.file = this.directory ? join(this.directory, `${this.source}.jsonl`) : null;
|
|
3965
|
+
this.minimum = options.minimumLevel ?? parsePluginLogLevel(env.DSH_PLUGIN_LOG_LEVEL, this.source, "info");
|
|
3966
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3967
|
+
this.pid = options.pid ?? process.pid;
|
|
3968
|
+
this.maxFileBytes = options.maxFileBytes ?? 104857600;
|
|
3969
|
+
this.maxArchiveBytes = options.maxArchiveBytes ?? 104857600;
|
|
3970
|
+
this.maxAgeMs = options.maxAgeMs ?? 6048e5;
|
|
3971
|
+
this.recentLimit = Math.max(1, options.recentLimit ?? 200);
|
|
3972
|
+
}
|
|
3973
|
+
get lastWriteError() {
|
|
3974
|
+
return this.lastError;
|
|
3975
|
+
}
|
|
3976
|
+
recentEntries(limit = this.recentLimit) {
|
|
3977
|
+
return this.recent.slice(-Math.max(0, limit));
|
|
3978
|
+
}
|
|
3979
|
+
debug(event, message, context) {
|
|
3980
|
+
return this.write({
|
|
3981
|
+
level: "debug",
|
|
3982
|
+
event,
|
|
3983
|
+
message,
|
|
3984
|
+
...context ? { context } : {}
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
info(event, message, context) {
|
|
3988
|
+
return this.write({
|
|
3989
|
+
level: "info",
|
|
3990
|
+
event,
|
|
3991
|
+
message,
|
|
3992
|
+
...context ? { context } : {}
|
|
3993
|
+
});
|
|
3994
|
+
}
|
|
3995
|
+
warn(event, message, error, context) {
|
|
3996
|
+
return this.write({
|
|
3997
|
+
level: "warn",
|
|
3998
|
+
event,
|
|
3999
|
+
message,
|
|
4000
|
+
...error !== void 0 ? { error } : {},
|
|
4001
|
+
...context ? { context } : {}
|
|
4002
|
+
});
|
|
4003
|
+
}
|
|
4004
|
+
error(event, message, error, context) {
|
|
4005
|
+
return this.write({
|
|
4006
|
+
level: "error",
|
|
4007
|
+
event,
|
|
4008
|
+
message,
|
|
4009
|
+
...error !== void 0 ? { error } : {},
|
|
4010
|
+
...context ? { context } : {}
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
fault(event, message, error, context) {
|
|
4014
|
+
return this.write({
|
|
4015
|
+
level: "fault",
|
|
4016
|
+
event,
|
|
4017
|
+
message,
|
|
4018
|
+
...error !== void 0 ? { error } : {},
|
|
4019
|
+
...context ? { context } : {}
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
4022
|
+
child(context) {
|
|
4023
|
+
return createChildPluginLogger(this, context);
|
|
4024
|
+
}
|
|
4025
|
+
write(input) {
|
|
4026
|
+
if (!pluginLogLevelEnabled(input.level, this.minimum) || this.writing) return false;
|
|
4027
|
+
const at = this.now();
|
|
4028
|
+
let entry;
|
|
4029
|
+
let line;
|
|
4030
|
+
try {
|
|
4031
|
+
entry = buildPluginLogEntry(input, {
|
|
4032
|
+
at,
|
|
4033
|
+
source: this.source,
|
|
4034
|
+
surface: "host",
|
|
4035
|
+
profile: this.profile,
|
|
4036
|
+
pid: this.pid,
|
|
4037
|
+
correlation: correlationStorage.getStore()
|
|
4038
|
+
});
|
|
4039
|
+
line = `${JSON.stringify(entry)}\n`;
|
|
4040
|
+
} catch {
|
|
4041
|
+
return false;
|
|
4042
|
+
}
|
|
4043
|
+
this.pushRecent(entry);
|
|
4044
|
+
if (!this.directory || !this.file) return true;
|
|
4045
|
+
this.writing = true;
|
|
4046
|
+
try {
|
|
4047
|
+
this.openManagedDirectory();
|
|
4048
|
+
this.prepare(at);
|
|
4049
|
+
if (this.size > 0 && (this.size + Buffer.byteLength(line) > this.maxFileBytes || at.getTime() - this.openedAt >= this.maxAgeMs)) this.rotate(at);
|
|
4050
|
+
appendFileSync(this.file, line, {
|
|
4051
|
+
encoding: "utf8",
|
|
4052
|
+
mode: 384
|
|
4053
|
+
});
|
|
4054
|
+
this.size += Buffer.byteLength(line);
|
|
4055
|
+
this.prune(at);
|
|
4056
|
+
this.lastError = null;
|
|
4057
|
+
this.loggedWriteFailure = false;
|
|
4058
|
+
return true;
|
|
4059
|
+
} catch (error) {
|
|
4060
|
+
const detail = sanitizePluginLogText(error instanceof Error ? error.message : String(error), 500);
|
|
4061
|
+
this.lastError = `The plugin diagnostic log could not be written: ${detail}`;
|
|
4062
|
+
this.initialized = false;
|
|
4063
|
+
if (!this.loggedWriteFailure) {
|
|
4064
|
+
this.loggedWriteFailure = true;
|
|
4065
|
+
try {
|
|
4066
|
+
process.stderr.write(`DSH Plugin (${this.source}): ${this.lastError}. The original operation continues unchanged.\n`);
|
|
4067
|
+
} catch {}
|
|
4068
|
+
}
|
|
4069
|
+
return false;
|
|
4070
|
+
} finally {
|
|
4071
|
+
this.writing = false;
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
pushRecent(entry) {
|
|
4075
|
+
this.recent.push(entry);
|
|
4076
|
+
const overflow = this.recent.length - this.recentLimit;
|
|
4077
|
+
if (overflow > 0) this.recent.splice(0, overflow);
|
|
4078
|
+
}
|
|
4079
|
+
prepare(at) {
|
|
4080
|
+
if (this.initialized || !this.file) return;
|
|
4081
|
+
this.openManagedDirectory();
|
|
4082
|
+
const stat = this.regularFile(this.file);
|
|
4083
|
+
this.size = stat?.size ?? 0;
|
|
4084
|
+
const stamps = stat ? [stat.birthtimeMs, stat.mtimeMs].filter((value) => value > 0) : [];
|
|
4085
|
+
this.openedAt = stamps.length ? Math.min(...stamps) : at.getTime();
|
|
4086
|
+
this.initialized = true;
|
|
4087
|
+
}
|
|
4088
|
+
rotate(at) {
|
|
4089
|
+
if (!this.directory || !this.file) return;
|
|
4090
|
+
const stamp = at.toISOString().replace(/:/g, "-");
|
|
4091
|
+
const name = `${this.source}.${stamp}.${this.pid}.${this.sequence++}.jsonl`;
|
|
4092
|
+
if (!ARCHIVE_RE.test(name)) throw new Error("Diagnostic log archive name is invalid.");
|
|
4093
|
+
const archive = join(this.directory, name);
|
|
4094
|
+
if (lexists(archive)) throw new Error("Diagnostic log archive already exists.");
|
|
4095
|
+
try {
|
|
4096
|
+
renameSync(this.file, archive);
|
|
4097
|
+
} catch (error) {
|
|
4098
|
+
if (error.code === "ENOENT") {
|
|
4099
|
+
this.size = 0;
|
|
4100
|
+
this.openedAt = at.getTime();
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
4103
|
+
throw error;
|
|
4104
|
+
}
|
|
4105
|
+
this.size = 0;
|
|
4106
|
+
this.openedAt = at.getTime();
|
|
4107
|
+
}
|
|
4108
|
+
rotateStaleActive(at) {
|
|
4109
|
+
if (!this.file) return;
|
|
4110
|
+
const stat = this.regularFile(this.file);
|
|
4111
|
+
if (!stat || stat.size === 0) return;
|
|
4112
|
+
if (at.getTime() - stat.mtimeMs < this.maxAgeMs) return;
|
|
4113
|
+
this.rotate(at);
|
|
4114
|
+
}
|
|
4115
|
+
prune(at) {
|
|
4116
|
+
if (!this.directory) return;
|
|
4117
|
+
this.rotateStaleActive(at);
|
|
4118
|
+
const archives = [];
|
|
4119
|
+
let total = 0;
|
|
4120
|
+
for (const name of readdirSync(this.directory)) {
|
|
4121
|
+
if (!ARCHIVE_RE.test(name) || !name.startsWith(`${this.source}.`)) continue;
|
|
4122
|
+
const file = join(this.directory, name);
|
|
4123
|
+
const stat = lstatSync(file);
|
|
4124
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error("Diagnostic log archive is not a regular file.");
|
|
4125
|
+
if (at.getTime() - stat.mtimeMs >= this.maxAgeMs) {
|
|
4126
|
+
try {
|
|
4127
|
+
unlinkSync(file);
|
|
4128
|
+
} catch {}
|
|
4129
|
+
continue;
|
|
4130
|
+
}
|
|
4131
|
+
archives.push({
|
|
4132
|
+
name,
|
|
4133
|
+
size: stat.size,
|
|
4134
|
+
mtime: stat.mtimeMs
|
|
4135
|
+
});
|
|
4136
|
+
total += stat.size;
|
|
4137
|
+
}
|
|
4138
|
+
archives.sort((left, right) => left.mtime - right.mtime || left.name.localeCompare(right.name));
|
|
4139
|
+
while (total > this.maxArchiveBytes && archives.length) {
|
|
4140
|
+
const oldest = archives.shift();
|
|
4141
|
+
try {
|
|
4142
|
+
unlinkSync(join(this.directory, oldest.name));
|
|
4143
|
+
} catch {}
|
|
4144
|
+
total -= oldest.size;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
openManagedDirectory() {
|
|
4148
|
+
if (!this.directory) return;
|
|
4149
|
+
if (this.anchor) {
|
|
4150
|
+
if ((existsSync(this.anchor) ? lstatSync(this.anchor) : void 0)?.isSymbolicLink()) throw new Error("Diagnostic log anchor cannot be a symbolic link.");
|
|
4151
|
+
}
|
|
4152
|
+
mkdirSync(this.directory, {
|
|
4153
|
+
recursive: true,
|
|
4154
|
+
mode: 448
|
|
4155
|
+
});
|
|
4156
|
+
const stat = lstatSync(this.directory);
|
|
4157
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("Diagnostic log directory is not a real directory.");
|
|
4158
|
+
const realDir = realpathSync(this.directory);
|
|
4159
|
+
if (this.anchor) {
|
|
4160
|
+
if (!isInside(realpathSync(this.anchor), realDir)) throw new Error("Diagnostic log directory escaped its managed root.");
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
regularFile(file) {
|
|
4164
|
+
try {
|
|
4165
|
+
const stat = lstatSync(file);
|
|
4166
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error("Diagnostic log is not a regular file.");
|
|
4167
|
+
const expected = join(realpathSync(dirname(file)), basename(file));
|
|
4168
|
+
if (!samePath(realpathSync(file), expected)) throw new Error("Diagnostic log path could not be verified.");
|
|
4169
|
+
return stat;
|
|
4170
|
+
} catch (error) {
|
|
4171
|
+
if (error.code === "ENOENT") return void 0;
|
|
4172
|
+
throw error;
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
};
|
|
4176
|
+
function createPluginLogger(ctxOrSource, maybeSource, maybeOptions) {
|
|
4177
|
+
const hasCtx = typeof ctxOrSource !== "string";
|
|
4178
|
+
const ctx = hasCtx ? ctxOrSource : void 0;
|
|
4179
|
+
const source = hasCtx ? String(maybeSource ?? "dsh-plugin") : ctxOrSource;
|
|
4180
|
+
const options = (hasCtx ? maybeOptions : maybeSource) ?? {};
|
|
4181
|
+
const normalized = normalizePluginLogSource(source);
|
|
4182
|
+
const target = resolveHostLogTarget(ctx, options);
|
|
4183
|
+
let backing;
|
|
4184
|
+
if (Boolean(target.directory && options.minimumLevel === void 0 && options.now === void 0 && options.pid === void 0 && options.maxFileBytes === void 0 && options.maxArchiveBytes === void 0 && options.maxAgeMs === void 0) && target.directory) {
|
|
4185
|
+
const key = resolve(target.directory, `${normalized}.jsonl`);
|
|
4186
|
+
let shared = processFileLoggers.get(key);
|
|
4187
|
+
if (!shared) {
|
|
4188
|
+
shared = new RotatingPluginLogger(normalized, {
|
|
4189
|
+
...options,
|
|
4190
|
+
directory: target.directory,
|
|
4191
|
+
anchor: target.anchor,
|
|
4192
|
+
profile: target.profile
|
|
4193
|
+
});
|
|
4194
|
+
processFileLoggers.set(key, shared);
|
|
4195
|
+
}
|
|
4196
|
+
backing = shared;
|
|
4197
|
+
} else backing = new RotatingPluginLogger(normalized, {
|
|
4198
|
+
...options,
|
|
4199
|
+
...target.directory ? {
|
|
4200
|
+
directory: target.directory,
|
|
4201
|
+
anchor: target.anchor
|
|
4202
|
+
} : {},
|
|
4203
|
+
...target.profile ? { profile: target.profile } : {}
|
|
4204
|
+
});
|
|
4205
|
+
if (options.forwardHostWarnError === false || !ctx || typeof ctx !== "object") return backing;
|
|
4206
|
+
return wrapWithHostForwarding(backing, ctx);
|
|
4207
|
+
}
|
|
4208
|
+
function wrapWithHostForwarding(backing, host) {
|
|
4209
|
+
const forward = (input) => {
|
|
4210
|
+
if (input.level !== "warn" && input.level !== "error" && input.level !== "fault") return;
|
|
4211
|
+
try {
|
|
4212
|
+
const hostLogger = host.logger;
|
|
4213
|
+
if (!hostLogger) return;
|
|
4214
|
+
const detail = input.error instanceof Error ? `${input.message} (${input.error.message})` : input.error !== void 0 ? `${input.message} (${String(input.error)})` : input.message;
|
|
4215
|
+
const text = sanitizePluginLogText(`${backing.source} [${input.event}]: ${detail}`, 1e3);
|
|
4216
|
+
if (input.level === "warn") {
|
|
4217
|
+
if (typeof hostLogger.warn === "function") hostLogger.warn(text);
|
|
4218
|
+
} else if (typeof hostLogger.error === "function") hostLogger.error(text);
|
|
4219
|
+
else if (typeof hostLogger.warn === "function") hostLogger.warn(text);
|
|
4220
|
+
} catch {}
|
|
4221
|
+
};
|
|
4222
|
+
const wrapped = {
|
|
4223
|
+
get source() {
|
|
4224
|
+
return backing.source;
|
|
4225
|
+
},
|
|
4226
|
+
get file() {
|
|
4227
|
+
return backing.file;
|
|
4228
|
+
},
|
|
4229
|
+
get lastWriteError() {
|
|
4230
|
+
return backing.lastWriteError;
|
|
4231
|
+
},
|
|
4232
|
+
debug: (event, message, context) => backing.debug(event, message, context),
|
|
4233
|
+
info: (event, message, context) => backing.info(event, message, context),
|
|
4234
|
+
warn: (event, message, error, context) => {
|
|
4235
|
+
const wrote = backing.warn(event, message, error, context);
|
|
4236
|
+
forward({
|
|
4237
|
+
level: "warn",
|
|
4238
|
+
event,
|
|
4239
|
+
message,
|
|
4240
|
+
error,
|
|
4241
|
+
context
|
|
4242
|
+
});
|
|
4243
|
+
return wrote;
|
|
4244
|
+
},
|
|
4245
|
+
error: (event, message, error, context) => {
|
|
4246
|
+
const wrote = backing.error(event, message, error, context);
|
|
4247
|
+
forward({
|
|
4248
|
+
level: "error",
|
|
4249
|
+
event,
|
|
4250
|
+
message,
|
|
4251
|
+
error,
|
|
4252
|
+
context
|
|
4253
|
+
});
|
|
4254
|
+
return wrote;
|
|
4255
|
+
},
|
|
4256
|
+
fault: (event, message, error, context) => {
|
|
4257
|
+
const wrote = backing.fault(event, message, error, context);
|
|
4258
|
+
forward({
|
|
4259
|
+
level: "fault",
|
|
4260
|
+
event,
|
|
4261
|
+
message,
|
|
4262
|
+
error,
|
|
4263
|
+
context
|
|
4264
|
+
});
|
|
4265
|
+
return wrote;
|
|
4266
|
+
},
|
|
4267
|
+
write: (input) => {
|
|
4268
|
+
const wrote = backing.write(input);
|
|
4269
|
+
forward(input);
|
|
4270
|
+
return wrote;
|
|
4271
|
+
},
|
|
4272
|
+
child: (context) => createChildPluginLogger(wrapped, context)
|
|
4273
|
+
};
|
|
4274
|
+
return wrapped;
|
|
4275
|
+
}
|
|
4276
|
+
function nextPluginOperationId(prefix = "op") {
|
|
4277
|
+
return `${normalizePluginLogSource(prefix)}-${process.pid}-${++operationSequence}`;
|
|
4278
|
+
}
|
|
4279
|
+
function withPluginLogContext(context, action) {
|
|
4280
|
+
const merged = mergePluginLogContext(correlationStorage.getStore(), context) ?? {};
|
|
4281
|
+
return correlationStorage.run(merged, action);
|
|
4282
|
+
}
|
|
4283
|
+
function lexists(file) {
|
|
4284
|
+
try {
|
|
4285
|
+
lstatSync(file);
|
|
4286
|
+
return true;
|
|
4287
|
+
} catch (error) {
|
|
4288
|
+
if (error.code === "ENOENT") return false;
|
|
4289
|
+
throw error;
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
function pathFromBaseUrl(baseUrl) {
|
|
4293
|
+
try {
|
|
4294
|
+
return baseUrl.includes("://") ? resolve(fileURLToPath(baseUrl)) : resolve(baseUrl);
|
|
4295
|
+
} catch {
|
|
4296
|
+
return null;
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
function samePath(left, right) {
|
|
4300
|
+
const a = resolve(left);
|
|
4301
|
+
const b = resolve(right);
|
|
4302
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
4303
|
+
}
|
|
4304
|
+
function isInside(root, target) {
|
|
4305
|
+
const rootResolved = resolve(root);
|
|
4306
|
+
const targetResolved = resolve(target);
|
|
4307
|
+
if (samePath(rootResolved, targetResolved)) return true;
|
|
4308
|
+
const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
|
|
4309
|
+
return process.platform === "win32" ? targetResolved.toLowerCase().startsWith(prefix.toLowerCase()) : targetResolved.startsWith(prefix);
|
|
4310
|
+
}
|
|
4311
|
+
//#endregion
|
|
3641
4312
|
//#region ../dsh-plugin-kit/src/host-rpc.ts
|
|
3642
4313
|
/** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */
|
|
3643
4314
|
function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
3644
4315
|
const bodyLimit = options.maxBodyBytes ?? 65536;
|
|
3645
4316
|
if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error("Invalid RPC body limit");
|
|
3646
4317
|
const isEndpoint = options.isEndpoint ?? ((endpoint) => /^[a-z.]+$/.test(endpoint));
|
|
4318
|
+
const log = options.logger;
|
|
3647
4319
|
return ctx.webServer.register({
|
|
3648
4320
|
kind: "prefix",
|
|
3649
4321
|
path: channel,
|
|
@@ -3654,29 +4326,54 @@ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
|
3654
4326
|
else res.writeHead(status);
|
|
3655
4327
|
res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? "rpc request failed" : ""));
|
|
3656
4328
|
};
|
|
4329
|
+
let endpoint = "";
|
|
3657
4330
|
try {
|
|
3658
4331
|
if (typeof ctx.connection.requestRejection !== "function") {
|
|
4332
|
+
log?.error("rpc.admission.unavailable", "Host RPC admission policy is unavailable.", void 0, {
|
|
4333
|
+
channel,
|
|
4334
|
+
status: 503
|
|
4335
|
+
});
|
|
3659
4336
|
reject(503);
|
|
3660
4337
|
return;
|
|
3661
4338
|
}
|
|
3662
4339
|
const rejection = ctx.connection.requestRejection(req);
|
|
3663
4340
|
if (rejection !== void 0) {
|
|
4341
|
+
log?.warn("rpc.admission.rejected", "Host RPC request rejected by connection policy.", void 0, {
|
|
4342
|
+
channel,
|
|
4343
|
+
status: rejection
|
|
4344
|
+
});
|
|
3664
4345
|
reject(rejection);
|
|
3665
4346
|
return;
|
|
3666
4347
|
}
|
|
3667
4348
|
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
3668
|
-
|
|
4349
|
+
endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
|
|
3669
4350
|
if (req.method !== "POST" || !isEndpoint(endpoint)) {
|
|
4351
|
+
log?.debug("rpc.route.rejected", "Host RPC route or method rejected.", {
|
|
4352
|
+
channel,
|
|
4353
|
+
endpoint: endpoint || null,
|
|
4354
|
+
method: req.method ?? null,
|
|
4355
|
+
status: 404
|
|
4356
|
+
});
|
|
3670
4357
|
reject(404);
|
|
3671
4358
|
return;
|
|
3672
4359
|
}
|
|
3673
4360
|
const contentType = String(req.headers["content-type"] ?? "").split(";", 1)[0]?.trim();
|
|
3674
4361
|
if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== "application/json") {
|
|
4362
|
+
log?.debug("rpc.content-type.rejected", "Host RPC content-type rejected.", {
|
|
4363
|
+
channel,
|
|
4364
|
+
endpoint,
|
|
4365
|
+
status: 415
|
|
4366
|
+
});
|
|
3675
4367
|
reject(415);
|
|
3676
4368
|
return;
|
|
3677
4369
|
}
|
|
3678
4370
|
const guarded = options.beforeBody?.(req, endpoint);
|
|
3679
4371
|
if (guarded) {
|
|
4372
|
+
log?.debug("rpc.guard.rejected", "Host RPC request rejected before body read.", {
|
|
4373
|
+
channel,
|
|
4374
|
+
endpoint,
|
|
4375
|
+
status: guarded.status
|
|
4376
|
+
});
|
|
3680
4377
|
reject(guarded.status, void 0, guarded.body);
|
|
3681
4378
|
return;
|
|
3682
4379
|
}
|
|
@@ -3690,6 +4387,11 @@ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
|
3690
4387
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3691
4388
|
size += buffer.length;
|
|
3692
4389
|
if (size > bodyLimit) {
|
|
4390
|
+
log?.warn("rpc.body.too-large", "Host RPC request exceeded body limit.", void 0, {
|
|
4391
|
+
channel,
|
|
4392
|
+
endpoint,
|
|
4393
|
+
maxBodyBytes: bodyLimit
|
|
4394
|
+
});
|
|
3693
4395
|
reject(413);
|
|
3694
4396
|
req.destroy();
|
|
3695
4397
|
return;
|
|
@@ -3700,11 +4402,26 @@ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
|
3700
4402
|
try {
|
|
3701
4403
|
body = JSON.parse(Buffer.concat(chunks).toString("utf8") || (options.emptyBodyAsNull ? "null" : ""));
|
|
3702
4404
|
} catch {
|
|
4405
|
+
log?.debug("rpc.body.invalid-json", "Host RPC request body is not valid JSON.", {
|
|
4406
|
+
channel,
|
|
4407
|
+
endpoint,
|
|
4408
|
+
status: 400
|
|
4409
|
+
});
|
|
3703
4410
|
reject(400);
|
|
3704
4411
|
return;
|
|
3705
4412
|
}
|
|
3706
|
-
|
|
4413
|
+
if (log) await withPluginLogContext({
|
|
4414
|
+
rpcChannel: channel,
|
|
4415
|
+
rpcEndpoint: endpoint,
|
|
4416
|
+
operationId: nextPluginOperationId("rpc")
|
|
4417
|
+
}, () => dispatch(endpoint, body, controller.signal, res));
|
|
4418
|
+
else await dispatch(endpoint, body, controller.signal, res);
|
|
3707
4419
|
} catch (error) {
|
|
4420
|
+
log?.error("rpc.dispatch.failed", "Unhandled host RPC handler error.", error, {
|
|
4421
|
+
channel,
|
|
4422
|
+
endpoint: endpoint || null,
|
|
4423
|
+
status: 500
|
|
4424
|
+
});
|
|
3708
4425
|
reject(500, error);
|
|
3709
4426
|
}
|
|
3710
4427
|
}
|
|
@@ -3725,19 +4442,41 @@ function sendHostRpcResponse(response, rpcId, result, noStore = true) {
|
|
|
3725
4442
|
}
|
|
3726
4443
|
/** Safe host RPC registration. Missing requestRejection fails closed. */
|
|
3727
4444
|
function registerHostRpc(ctx, channel, call, options = {}) {
|
|
4445
|
+
const log = options.logger;
|
|
3728
4446
|
return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {
|
|
3729
4447
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
4448
|
+
log?.debug("rpc.envelope.invalid", "Host RPC envelope must be a JSON object.", {
|
|
4449
|
+
channel,
|
|
4450
|
+
endpoint
|
|
4451
|
+
});
|
|
3730
4452
|
res.writeHead(400);
|
|
3731
4453
|
res.end();
|
|
3732
4454
|
return;
|
|
3733
4455
|
}
|
|
3734
4456
|
const request = body;
|
|
3735
4457
|
if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string" || options.requirePayload && !Object.hasOwn(request, "payload")) {
|
|
4458
|
+
log?.debug("rpc.envelope.invalid", "Host RPC envelope failed validation.", {
|
|
4459
|
+
channel,
|
|
4460
|
+
endpoint
|
|
4461
|
+
});
|
|
3736
4462
|
res.writeHead(400);
|
|
3737
4463
|
res.end();
|
|
3738
4464
|
return;
|
|
3739
4465
|
}
|
|
4466
|
+
const started = Date.now();
|
|
3740
4467
|
const raw = await call(endpoint, request.payload, signal);
|
|
4468
|
+
const durationMs = Date.now() - started;
|
|
4469
|
+
if (raw.ok) log?.debug("rpc.call.completed", "Host RPC call succeeded.", {
|
|
4470
|
+
channel,
|
|
4471
|
+
endpoint,
|
|
4472
|
+
durationMs
|
|
4473
|
+
});
|
|
4474
|
+
else log?.warn("rpc.call.error", "Host RPC call returned an application error.", void 0, {
|
|
4475
|
+
channel,
|
|
4476
|
+
endpoint,
|
|
4477
|
+
code: raw.error?.code ?? null,
|
|
4478
|
+
durationMs
|
|
4479
|
+
});
|
|
3741
4480
|
const result = raw.ok ? raw : {
|
|
3742
4481
|
...raw,
|
|
3743
4482
|
error: {
|
|
@@ -4794,7 +5533,9 @@ function registerSessionManagerRpc(root, transport, config, models, modelStore,
|
|
|
4794
5533
|
* A populated per-record tree is authoritative; the retained legacy source
|
|
4795
5534
|
* is an initial backup, not a current-data rollback after subsequent writes.
|
|
4796
5535
|
*/
|
|
4797
|
-
async function openCompatibleDomain(open, spec) {
|
|
5536
|
+
async function openCompatibleDomain(open, spec, options) {
|
|
5537
|
+
const log = options?.logger;
|
|
5538
|
+
const domainName = options?.domainName ?? (typeof spec.name === "string" ? spec.name : "plugin-domain");
|
|
4798
5539
|
const perRecord = {
|
|
4799
5540
|
...spec,
|
|
4800
5541
|
layout: "per-record"
|
|
@@ -4806,7 +5547,13 @@ async function openCompatibleDomain(open, spec) {
|
|
|
4806
5547
|
} finally {
|
|
4807
5548
|
if (!populated) await domain.close();
|
|
4808
5549
|
}
|
|
4809
|
-
if (populated)
|
|
5550
|
+
if (populated) {
|
|
5551
|
+
log?.debug("domain.opened", "Opened populated per-record storage domain.", {
|
|
5552
|
+
domain: domainName,
|
|
5553
|
+
version: spec.version
|
|
5554
|
+
});
|
|
5555
|
+
return domain;
|
|
5556
|
+
}
|
|
4810
5557
|
let mismatch;
|
|
4811
5558
|
for (const version of /* @__PURE__ */ new Set([spec.version, ...spec.compatibleVersions ?? []])) try {
|
|
4812
5559
|
await (await open({
|
|
@@ -4815,11 +5562,26 @@ async function openCompatibleDomain(open, spec) {
|
|
|
4815
5562
|
version,
|
|
4816
5563
|
compatibleVersions: []
|
|
4817
5564
|
})).close();
|
|
5565
|
+
log?.info("domain.probed", "Opened storage domain after legacy probe.", {
|
|
5566
|
+
domain: domainName,
|
|
5567
|
+
probedVersion: version,
|
|
5568
|
+
targetVersion: spec.version
|
|
5569
|
+
});
|
|
4818
5570
|
return open(perRecord);
|
|
4819
5571
|
} catch (error) {
|
|
4820
|
-
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "version-mismatch")
|
|
5572
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "version-mismatch") {
|
|
5573
|
+
log?.error("domain.open-failed", "Storage domain open failed during legacy probe.", error, {
|
|
5574
|
+
domain: domainName,
|
|
5575
|
+
probedVersion: version
|
|
5576
|
+
});
|
|
5577
|
+
throw error;
|
|
5578
|
+
}
|
|
4821
5579
|
mismatch = error;
|
|
4822
5580
|
}
|
|
5581
|
+
log?.error("domain.version-mismatch", "Storage domain version is incompatible.", mismatch, {
|
|
5582
|
+
domain: domainName,
|
|
5583
|
+
targetVersion: spec.version
|
|
5584
|
+
});
|
|
4823
5585
|
throw mismatch;
|
|
4824
5586
|
}
|
|
4825
5587
|
//#endregion
|
|
@@ -5117,7 +5879,13 @@ function setSharedModel(state, ctx, input, expectedRevision) {
|
|
|
5117
5879
|
if ((Boolean(row.provider) || Boolean(row.model)) && !modelRouteOverride(normalized)) return Promise.reject(/* @__PURE__ */ new Error("Invalid model route"));
|
|
5118
5880
|
const commit = state.tail.then(async () => {
|
|
5119
5881
|
if (state.closed || !state.domain) throw new Error("Auxiliary model settings are unloading; retry in a moment");
|
|
5120
|
-
if (expectedRevision !== state.data.revision)
|
|
5882
|
+
if (expectedRevision !== state.data.revision) {
|
|
5883
|
+
createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false }).warn("auxiliary-model.conflict", "Auxiliary model update rejected due to revision conflict.", void 0, {
|
|
5884
|
+
expectedRevision: Number(expectedRevision),
|
|
5885
|
+
currentRevision: state.data.revision
|
|
5886
|
+
});
|
|
5887
|
+
throw conflictError("Auxiliary model settings changed; reload and retry");
|
|
5888
|
+
}
|
|
5121
5889
|
const next = {
|
|
5122
5890
|
revision: state.data.revision + 1,
|
|
5123
5891
|
model: modelRouteOverride(normalized) ?? null
|
|
@@ -5127,6 +5895,11 @@ function setSharedModel(state, ctx, input, expectedRevision) {
|
|
|
5127
5895
|
model: next.model
|
|
5128
5896
|
});
|
|
5129
5897
|
state.data = next;
|
|
5898
|
+
createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false }).info("auxiliary-model.updated", "Updated global auxiliary model default.", {
|
|
5899
|
+
revision: next.revision,
|
|
5900
|
+
provider: next.model?.provider ?? null,
|
|
5901
|
+
model: next.model?.model ?? null
|
|
5902
|
+
});
|
|
5130
5903
|
notifyModelSettingsChanged(ctx.root, AUXILIARY_MODEL_NS, next.revision);
|
|
5131
5904
|
return next;
|
|
5132
5905
|
});
|
|
@@ -5134,6 +5907,7 @@ function setSharedModel(state, ctx, input, expectedRevision) {
|
|
|
5134
5907
|
return commit;
|
|
5135
5908
|
}
|
|
5136
5909
|
function initScope(state, ctx, scope) {
|
|
5910
|
+
const log = createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false });
|
|
5137
5911
|
const generation = ++state.generation;
|
|
5138
5912
|
let disposed = false;
|
|
5139
5913
|
scope.effect(() => () => {
|
|
@@ -5165,6 +5939,11 @@ function initScope(state, ctx, scope) {
|
|
|
5165
5939
|
};
|
|
5166
5940
|
state.error = void 0;
|
|
5167
5941
|
state.available = true;
|
|
5942
|
+
log.info("auxiliary-model.opened", "Shared auxiliary model store opened.", {
|
|
5943
|
+
generation,
|
|
5944
|
+
revision: state.data.revision,
|
|
5945
|
+
hasModel: Boolean(state.data.model)
|
|
5946
|
+
});
|
|
5168
5947
|
scope.effect(() => registerModelSettings(scope, {
|
|
5169
5948
|
ns: AUXILIARY_MODEL_NS,
|
|
5170
5949
|
title: {
|
|
@@ -5248,7 +6027,10 @@ function initScope(state, ctx, scope) {
|
|
|
5248
6027
|
message: `Unknown endpoint: ${endpoint}`
|
|
5249
6028
|
}
|
|
5250
6029
|
};
|
|
5251
|
-
}, {
|
|
6030
|
+
}, {
|
|
6031
|
+
isEndpoint: (endpoint) => /^[a-z.]+$/.test(endpoint),
|
|
6032
|
+
logger: log
|
|
6033
|
+
}));
|
|
5252
6034
|
createModelSettingsChanges(scope, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS);
|
|
5253
6035
|
scope.effect(() => () => {
|
|
5254
6036
|
if (state.generation === generation) {
|
|
@@ -5257,6 +6039,8 @@ function initScope(state, ctx, scope) {
|
|
|
5257
6039
|
}
|
|
5258
6040
|
});
|
|
5259
6041
|
})().catch((error) => {
|
|
6042
|
+
if (error && typeof error === "object" && error[RECOVERABLE]) log.debug("auxiliary-model.superseded", "Shared auxiliary model init superseded during open.", { generation });
|
|
6043
|
+
else log.error("auxiliary-model.init-failed", "Shared auxiliary model store failed to open.", error, { generation });
|
|
5260
6044
|
if (error && typeof error === "object") error.auxGeneration = generation;
|
|
5261
6045
|
if (state.generation === generation) {
|
|
5262
6046
|
state.available = false;
|
|
@@ -5280,6 +6064,7 @@ function teardown(state, ctx) {
|
|
|
5280
6064
|
try {
|
|
5281
6065
|
await state.fiber?.dispose();
|
|
5282
6066
|
} catch (error) {
|
|
6067
|
+
createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false }).fault("auxiliary-model.dispose-failed", "Shared auxiliary model owner fiber failed to dispose.", error);
|
|
5283
6068
|
try {
|
|
5284
6069
|
ctx.logger?.warn("auxiliary-model: fiber dispose failed", error);
|
|
5285
6070
|
} catch {}
|