diff --git a/.gitignore b/.gitignore index 5b71549..690582c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ _archive/ # 嵌入前的提示词原文 lib/default-prompt-inject.md +lib/default-prompt-inject.md.bak +lib/default-prompt-inject.zh.md +lib/default-prompt-inject.en.md default-prompt-inject.md # 本机官方安装包、解包目录、拆出的压缩包 diff --git a/lib/core.js b/lib/core.js index e40c75f..17ba76c 100644 --- a/lib/core.js +++ b/lib/core.js @@ -2520,6 +2520,24 @@ const COMPLETE_PROMPT_WATERFALL_INJECT = "\t\t\tcontexts: runtimeContextSuppressed ? [] : transformed.contexts\n" + "\t\t};"; +// 折进 persona 后 complete 段本身已含注入,必须放在最前;不能再用 PREV 的 [...tail, base]。 +const COMPLETE_PROMPT_WATERFALL_INJECT_HEAD = + "\t\tconst transformed = await this.ctx.waterfall(scopeTarget(this, scope), \"system-prompt/assemble\", assembly, context, () => Promise.resolve(assembly));\n" + + "\t\tconst dshPurgeSections = (transformed.sections || []).filter((s) => String(s && s.name || \"\").startsWith(\"dsh-purge\") && String(s.text || \"\").length > 0);\n" + + "\t\tconst dshPurgeRest = (transformed.sections || []).filter((s) => !String(s && s.name || \"\").startsWith(\"dsh-purge\"));\n" + + "\t\tif (completeSection === void 0 && !runtimeContextSuppressed) return { ...transformed, sections: [...dshPurgeSections, ...dshPurgeRest] }; // [dsh-purge] fold-complete inject first\n" + + "\t\treturn {\n" + + "\t\t\t...transformed,\n" + + "\t\t\tsections: completeSection === void 0 ? [...dshPurgeSections, ...dshPurgeRest] : (function (complete, purge, rest, assembled) {\n" + + "\t\t\t\tconst name = String(complete && complete.name || \"\");\n" + + "\t\t\t\tconst after = (assembled || []).find((s) => String(s && s.name || \"\") === name);\n" + + "\t\t\t\tconst base = after ? { ...after } : { ...complete };\n" + + "\t\t\t\tconst tail = rest.filter((s) => String(s?.name || \"\") !== name);\n" + + "\t\t\t\treturn [...purge, base, ...tail]; // [dsh-purge] fold-complete inject first\n" + + "\t\t\t})(completeSection, dshPurgeSections, dshPurgeRest, transformed.sections),\n" + + "\t\t\tcontexts: runtimeContextSuppressed ? [] : transformed.contexts\n" + + "\t\t};"; + function clearShellNetworkBan(pattern) { return pattern .replace(/\n[ \t]+\* You don't have access to the internet via this tool\./, "") @@ -3818,6 +3836,57 @@ const NEW_TOOL_PATCHES = [ ], markers: ["[dsh-purge] subagent scope lock neutralized"], }, + { + id: 92, + name: "SUBAGENT_SAME_INJECT_PATH", + layer: "代码", + layer_en: "Code", + desc: "子代理创建时立刻挂 assemble 注入,并继承父会话权限 / wrap child assemble at spawn and inherit parent permission preset", + file: ["subagent", "subagent-types"], + optional: true, + skipIfMarked: true, + replacements: [ + { + pattern: + "\tif (composition.toolFilter !== void 0) childCtx.tools.restrict(composition.toolFilter);\n" + + "}", + replace: + "\tif (composition.toolFilter !== void 0) childCtx.tools.restrict(composition.toolFilter);\n" + + "\ttry {\n" + + "\t\tconst prepare = globalThis.__dshPurgePrepareAssemble;\n" + + "\t\tif (typeof prepare === \"function\") prepare(childCtx);\n" + + "\t} catch {}\n" + + "} // [dsh-purge-92] child assemble same as parent", + }, + { + pattern: + " if (composition.toolFilter !== undefined)\n" + + " childCtx.tools.restrict(composition.toolFilter);\n" + + "}", + replace: + " if (composition.toolFilter !== undefined)\n" + + " childCtx.tools.restrict(composition.toolFilter);\n" + + " try {\n" + + " const prepare = globalThis.__dshPurgePrepareAssemble;\n" + + " if (typeof prepare === \"function\") prepare(childCtx);\n" + + " } catch {}\n" + + "} // [dsh-purge-92] child assemble same as parent", + }, + { + pattern: + "\t\tpermissionPreset: preset === \"auto\" || preset === \"danger-full-access\" ? preset : void 0,", + replace: + "\t\tpermissionPreset: preset || \"danger-full-access\", // [dsh-purge-92] child inherits parent access", + }, + { + pattern: + " permissionPreset: preset === 'auto' || preset === 'danger-full-access' ? preset : undefined,", + replace: + " permissionPreset: preset || 'danger-full-access', // [dsh-purge-92] child inherits parent access", + }, + ], + markers: ["[dsh-purge-92] child assemble same as parent"], + }, { // 0.1.5 schema 不认 text;社区预设仍带 text 时挂载失败。 id: 38, name: "PERSONA_TEXT_PREFIX_ALIAS", layer: "代码", layer_en: "Code", @@ -3866,16 +3935,16 @@ const NEW_TOOL_PATCHES = [ markers: ["[dsh-purge] mnemon summary off-notice"], }, { - // complete:true 会在 waterfall 之后只留完整段。操作者段是 waterfall 里加上的, - // 必须从 waterfall 的结果里抄,不能从 waterfall 之前的段落里抄。 + // complete:true 会在 waterfall 之后只留完整段。注入已折进 persona 时, + // 必须用瀑布之后的 complete 段,并放到最前。 id: 40, name: "COMPLETE_PROMPT_KEEP_INJECT", layer: "0.1.5 兼容", layer_en: "0.1.5", - desc: "complete 预设仍保留 dsh-purge 注入 / keep inject after complete prompt", + desc: "complete 预设把已折进 persona 的注入放最前 / keep folded inject first after complete prompt", file: ["system-prompt", "system-prompt-nested"], replacements: [ { // 缩进不固定。0.2 有的构建用 tab,有的用空格;只认 tab 时匹配为 0,重启校验会说没写进宿主。 pattern: /[ \t]*const transformed = await this\.ctx\.waterfall\(scopeTarget\(this, scope\), "system-prompt\/assemble", assembly, context, \(\) => Promise\.resolve\(assembly\)\);\n[ \t]*if \(completeSection === void 0 && !runtimeContextSuppressed\) return transformed;\n[ \t]*return \{\n[ \t]*\.\.\.transformed,\n[ \t]*sections: completeSection === void 0 \? transformed\.sections : \[completeSection\],\n[ \t]*contexts: runtimeContextSuppressed \? \[\] : transformed\.contexts\n[ \t]*\};/, - replace: COMPLETE_PROMPT_WATERFALL_INJECT, + replace: COMPLETE_PROMPT_WATERFALL_INJECT_HEAD, }, { pattern: @@ -3892,7 +3961,7 @@ const NEW_TOOL_PATCHES = [ "\t\t\t})(completeSection, dshPurgeInject),\n" + "\t\t\tcontexts: runtimeContextSuppressed ? [] : transformed.contexts\n" + "\t\t};", - replace: COMPLETE_PROMPT_WATERFALL_INJECT, + replace: COMPLETE_PROMPT_WATERFALL_INJECT_HEAD, }, { pattern: @@ -3907,7 +3976,7 @@ const NEW_TOOL_PATCHES = [ "\t\t\t})(completeSection, dshPurgeSections),\n" + "\t\t\tcontexts: runtimeContextSuppressed ? [] : transformed.contexts\n" + "\t\t};", - replace: COMPLETE_PROMPT_WATERFALL_INJECT, + replace: COMPLETE_PROMPT_WATERFALL_INJECT_HEAD, }, { pattern: @@ -3923,11 +3992,15 @@ const NEW_TOOL_PATCHES = [ "\t\t\t})(completeSection, dshPurgeSections),\n" + "\t\t\tcontexts: runtimeContextSuppressed ? [] : transformed.contexts\n" + "\t\t};", - replace: COMPLETE_PROMPT_WATERFALL_INJECT, + replace: COMPLETE_PROMPT_WATERFALL_INJECT_HEAD, }, { pattern: COMPLETE_PROMPT_WATERFALL_INJECT_PREV, - replace: COMPLETE_PROMPT_WATERFALL_INJECT, + replace: COMPLETE_PROMPT_WATERFALL_INJECT_HEAD, + }, + { + pattern: COMPLETE_PROMPT_WATERFALL_INJECT, + replace: COMPLETE_PROMPT_WATERFALL_INJECT_HEAD, }, { pattern: @@ -3946,7 +4019,7 @@ const NEW_TOOL_PATCHES = [ "\t\t\t})(completeSection, dshPurgeSections, dshPurgeRest, transformed.sections),\n" + "\t\t\tcontexts: runtimeContextSuppressed ? [] : transformed.contexts\n" + "\t\t};", - replace: COMPLETE_PROMPT_WATERFALL_INJECT, + replace: COMPLETE_PROMPT_WATERFALL_INJECT_HEAD, }, { pattern: @@ -3963,16 +4036,13 @@ const NEW_TOOL_PATCHES = [ "\t\t\t})(completeSection, dshPurgeSections, dshPurgeRest),\n" + "\t\t\tcontexts: runtimeContextSuppressed ? [] : transformed.contexts\n" + "\t\t};", - replace: COMPLETE_PROMPT_WATERFALL_INJECT, + replace: COMPLETE_PROMPT_WATERFALL_INJECT_HEAD, }, ], forbid: ["dsh-purge:committed"], markersAny: true, markers: [ - "complete prompt keeps waterfall inject", - "complete prompt keeps inject + agent-instructions", - "complete prompt keeps inject + assembled complete", - "inject last after closer", + "fold-complete inject first", ], }, { @@ -5782,6 +5852,227 @@ const NEW_TOOL_PATCHES = [ ], markers: ["[dsh-purge-89] system field extract"], }, + { + id: 90, + name: "OPENAI_NATIVE_SYSTEM_ROLE", + layer: "代码", + layer_en: "Code", + desc: "openai-completions 把 systemPrompt 折进 messages[0].role=system(chat/completions 原生槽),不再塞进 last user / fold systemPrompt into native role:system for /chat/completions", + file: ["pi-ai-completions"], + optional: true, + skipIfMarked: true, + replacements: [ + { + pattern: + "export const stream = (model, context, options) => {\n" + + " const stream = new AssistantMessageEventStream();\n" + + " const normalizedContext = resolveTranscript(context, getCompat(model).supportsMidConvoSystemMessages);", + replace: + "export const stream = (model, context, options) => {\n" + + " const stream = new AssistantMessageEventStream();\n" + + " const promptText = (typeof context.systemPrompt === \"string\" && context.systemPrompt.length > 0) ? context.systemPrompt : ((typeof context.system === \"string\" && context.system.length > 0) ? context.system : \"\");\n" + + " const seeded = (promptText && context.messages?.[0]?.role !== \"system\") ? { ...context, systemPrompt: promptText, messages: [{ role: \"system\", content: promptText, timestamp: 0 }, ...context.messages] } : context;\n" + + " const normalizedContext = { ...seeded, ...resolveTranscript(seeded, getCompat(model).supportsMidConvoSystemMessages) }; // [dsh-purge-90] native system role", + }, + { + pattern: + "export function convertMessages(model, context, compat, options) {\n" + + " const normalizedContext = resolveTranscript(context, compat.supportsMidConvoSystemMessages);\n" + + " const params = [];\n" + + " const systemParts = []; // [dsh-purge-90] collect system text for user injection\n" + + " if (typeof context.system === \"string\" && context.system.length > 0) systemParts.push(sanitizeSurrogates(context.system));\n" + + " const normalizeToolCallId = (id) => {", + replace: + "export function convertMessages(model, context, compat, options) {\n" + + " const promptText = (typeof context.systemPrompt === \"string\" && context.systemPrompt.length > 0) ? context.systemPrompt : ((typeof context.system === \"string\" && context.system.length > 0) ? context.system : \"\");\n" + + " const seeded = (promptText && context.messages?.[0]?.role !== \"system\") ? { ...context, messages: [{ role: \"system\", content: promptText, timestamp: 0 }, ...context.messages] } : context;\n" + + " const normalizedContext = resolveTranscript(seeded, compat.supportsMidConvoSystemMessages); // [dsh-purge-90] native system role\n" + + " const params = [];\n" + + " const normalizeToolCallId = (id) => {", + }, + { + pattern: + "export function convertMessages(model, context, compat, options) {\n" + + " const normalizedContext = resolveTranscript(context, compat.supportsMidConvoSystemMessages);\n" + + " const params = [];\n" + + " const normalizeToolCallId = (id) => {", + replace: + "export function convertMessages(model, context, compat, options) {\n" + + " const promptText = (typeof context.systemPrompt === \"string\" && context.systemPrompt.length > 0) ? context.systemPrompt : ((typeof context.system === \"string\" && context.system.length > 0) ? context.system : \"\");\n" + + " const seeded = (promptText && context.messages?.[0]?.role !== \"system\") ? { ...context, messages: [{ role: \"system\", content: promptText, timestamp: 0 }, ...context.messages] } : context;\n" + + " const normalizedContext = resolveTranscript(seeded, compat.supportsMidConvoSystemMessages); // [dsh-purge-90] native system role\n" + + " const params = [];\n" + + " const normalizeToolCallId = (id) => {", + }, + { + pattern: + " const text = i === 0 ? getSystemMessageText(msg) : renderSystemMessageUpdate(msg);\n" + + " if (text.length > 0) {\n" + + " systemParts.push(sanitizeSurrogates(text)); // [dsh-purge-90] collect, don't push as system\n" + + " }", + replace: + " const text = i === 0 ? getSystemMessageText(msg) : renderSystemMessageUpdate(msg);\n" + + " if (text.length > 0) {\n" + + " params.push({ role: instructionRole, content: sanitizeSurrogates(text).replace(/<\\|im_start\\|>\\s*system\\s*:\\s*/gi, \"\").replace(/<\\/?project_instructions>/gi, \"\").replace(/<\\|im_end\\|>/g, \"\").trim() }); // [dsh-purge-90] native system role\n" + + " }", + }, + { + pattern: + " const text = i === 0 ? getSystemMessageText(msg) : renderSystemMessageUpdate(msg);\n" + + " if (text.length > 0) {\n" + + " params.push({ role: instructionRole, content: sanitizeSurrogates(text) });\n" + + " }", + replace: + " const text = i === 0 ? getSystemMessageText(msg) : renderSystemMessageUpdate(msg);\n" + + " if (text.length > 0) {\n" + + " params.push({ role: instructionRole, content: sanitizeSurrogates(text).replace(/<\\|im_start\\|>\\s*system\\s*:\\s*/gi, \"\").replace(/<\\/?project_instructions>/gi, \"\").replace(/<\\|im_end\\|>/g, \"\").trim() }); // [dsh-purge-90] native system role\n" + + " }", + }, + { + pattern: + " lastRole = msg.role;\n" + + " }\n" + + " if (systemParts.length > 0) { // [dsh-purge-90] inject once\n" + + " const systemText = systemParts.join(\"\\n\\n\").replace(/<\\|im_start\\|>\\s*system\\s*:\\s*/gi, \"\").replace(/<\\/?project_instructions>/gi, \"\").replace(/<\\|im_end\\|>/g, \"\").trim();\n" + + " const lastUser = params.findLastIndex(m => m.role === \"user\");\n" + + " if (lastUser !== -1) {\n" + + " const uc = params[lastUser].content;\n" + + " if (typeof uc === \"string\") params[lastUser].content = systemText + \"\\n\\n\" + uc;\n" + + " else if (Array.isArray(uc)) params[lastUser].content = [{ type: \"text\", text: systemText }, ...uc];\n" + + " } else {\n" + + " params.push({ role: \"user\", content: systemText });\n" + + " }\n" + + " }\n" + + " return params;", + replace: + " lastRole = msg.role;\n" + + " }\n" + + " return params;", + }, + { + pattern: + " lastRole = msg.role;\n" + + " }\n" + + " if (systemParts.length > 0) { // [dsh-purge-90] inject system into last user message\n" + + " const systemText = systemParts.join(\"\\n\\n\").replace(/<\\|im_start\\|>\\s*system\\s*:\\s*/gi, \"\").replace(/<\\/?project_instructions>/gi, \"\").replace(/<\\|im_end\\|>/g, \"\").trim();\n" + + " const lastUser = params.findLastIndex(m => m.role === \"user\");\n" + + " if (lastUser !== -1) {\n" + + " const uc = params[lastUser].content;\n" + + " if (typeof uc === \"string\") params[lastUser].content = systemText + \"\\n\\n\" + uc;\n" + + " else if (Array.isArray(uc)) params[lastUser].content = [{ type: \"text\", text: systemText }, ...uc];\n" + + " } else {\n" + + " params.push({ role: \"user\", content: systemText });\n" + + " }\n" + + " }\n" + + " return params;", + replace: + " lastRole = msg.role;\n" + + " }\n" + + " return params;", + }, + ], + markers: ["[dsh-purge-90] native system role"], + }, + { + id: 91, + name: "ANTHROPIC_NATIVE_PARAMS_SYSTEM", + layer: "代码", + layer_en: "Code", + desc: "anthropic-messages 把 systemPrompt 写入顶层 params.system(/messages 原生槽),不再塞进 last user / write systemPrompt to native params.system for /messages", + file: ["pi-ai-anthropic-messages"], + optional: true, + skipIfMarked: true, + replacements: [ + { + pattern: + "export const stream = (model, context, options) => {\n" + + " const stream = new AssistantMessageEventStream();\n" + + " const normalizedContext = resolveTranscript(context, getAnthropicCompat(model).supportsMidConvoSystemMessages);", + replace: + "export const stream = (model, context, options) => {\n" + + " const stream = new AssistantMessageEventStream();\n" + + " const promptText = (typeof context.systemPrompt === \"string\" && context.systemPrompt.length > 0) ? context.systemPrompt : ((typeof context.system === \"string\" && context.system.length > 0) ? context.system : \"\");\n" + + " const seeded = (promptText && context.messages?.[0]?.role !== \"system\") ? { ...context, systemPrompt: promptText, messages: [{ role: \"system\", content: promptText, timestamp: 0 }, ...context.messages] } : context;\n" + + " const normalizedContext = { ...seeded, ...resolveTranscript(seeded, getAnthropicCompat(model).supportsMidConvoSystemMessages) }; // [dsh-purge-91] native params.system", + }, + { + pattern: + " const initialSystemMessage = getInitialSystemMessage(context.messages);\n" + + " const initialSystemText = (typeof context.system === \"string\" && context.system.length > 0) // [dsh-purge-91] fallback to context.system (#89 compat)\n" + + " ? context.system\n" + + " : (initialSystemMessage ? getSystemMessageText(initialSystemMessage) : \"\");", + replace: + " const initialSystemMessage = getInitialSystemMessage(context.messages);\n" + + " const initialSystemText = (typeof context.systemPrompt === \"string\" && context.systemPrompt.length > 0) // [dsh-purge-91] native params.system\n" + + " ? context.systemPrompt\n" + + " : ((typeof context.system === \"string\" && context.system.length > 0)\n" + + " ? context.system\n" + + " : (initialSystemMessage ? getSystemMessageText(initialSystemMessage) : \"\"));", + }, + { + pattern: + " const initialSystemMessage = getInitialSystemMessage(context.messages);\n" + + " const initialSystemText = initialSystemMessage ? getSystemMessageText(initialSystemMessage) : \"\";", + replace: + " const initialSystemMessage = getInitialSystemMessage(context.messages);\n" + + " const initialSystemText = (typeof context.systemPrompt === \"string\" && context.systemPrompt.length > 0) // [dsh-purge-91] native params.system\n" + + " ? context.systemPrompt\n" + + " : ((typeof context.system === \"string\" && context.system.length > 0)\n" + + " ? context.system\n" + + " : (initialSystemMessage ? getSystemMessageText(initialSystemMessage) : \"\"));", + }, + { + pattern: + " // [dsh-purge-91] system injected into last user message (0.1.7 style)", + replace: + " if (initialSystemText) { // [dsh-purge-91] native params.system\n" + + " const systemText = sanitizeSurrogates(initialSystemText).replace(/<\\|im_start\\|>\\s*system\\s*:\\s*/gi, \"\").replace(/<\\/?project_instructions>/gi, \"\").replace(/<\\|im_end\\|>/g, \"\").trim();\n" + + " params.system = [\n" + + " {\n" + + " type: \"text\",\n" + + " text: systemText,\n" + + " ...(cacheControl ? { cache_control: cacheControl } : {}),\n" + + " },\n" + + " ];\n" + + " }", + }, + { + pattern: + " if (initialSystemText) { // [dsh-purge-91] inject once\n" + + " const systemText = sanitizeSurrogates(initialSystemText).replace(/<\\|im_start\\|>\\s*system\\s*:\\s*/gi, \"\").replace(/<\\/?project_instructions>/gi, \"\").replace(/<\\|im_end\\|>/g, \"\").trim();\n" + + " const lastUser = params.messages.findLastIndex(m => m.role === \"user\");\n" + + " if (lastUser !== -1) {\n" + + " const uc = params.messages[lastUser].content;\n" + + " if (typeof uc === \"string\") params.messages[lastUser].content = systemText + \"\\n\\n\" + uc;\n" + + " else if (Array.isArray(uc)) params.messages[lastUser].content = [{ type: \"text\", text: systemText }, ...uc];\n" + + " } else {\n" + + " params.messages.push({ role: \"user\", content: [{ type: \"text\", text: systemText }] });\n" + + " }\n" + + " }\n" + + " return params;", + replace: + " return params;", + }, + { + pattern: + " if (initialSystemText) { // [dsh-purge-91] inject system into last user message\n" + + " const systemText = sanitizeSurrogates(initialSystemText).replace(/<\\|im_start\\|>\\s*system\\s*:\\s*/gi, \"\").replace(/<\\/?project_instructions>/gi, \"\").replace(/<\\|im_end\\|>/g, \"\").trim();\n" + + " const lastUser = params.messages.findLastIndex(m => m.role === \"user\");\n" + + " if (lastUser !== -1) {\n" + + " const uc = params.messages[lastUser].content;\n" + + " if (typeof uc === \"string\") params.messages[lastUser].content = systemText + \"\\n\\n\" + uc;\n" + + " else if (Array.isArray(uc)) params.messages[lastUser].content = [{ type: \"text\", text: systemText }, ...uc];\n" + + " } else {\n" + + " params.messages.push({ role: \"user\", content: [{ type: \"text\", text: systemText }] });\n" + + " }\n" + + " }\n" + + " return params;", + replace: + " return params;", + }, + ], + markers: ["[dsh-purge-91] native params.system"], + }, ]; diff --git a/lib/identity.js b/lib/identity.js index f7dbeba..43d31a5 100644 --- a/lib/identity.js +++ b/lib/identity.js @@ -127,6 +127,67 @@ function ensureFullInject(sections, inject) { ]; } +const IDENTITY_CARD_PREFIX = "WHO YOU ARE (highest priority"; + +function isPersonaFoldTarget(name, preferPrefix) { + if (preferPrefix) return PERSONA_PREFIX_NAMES.has(name); + return PERSONA_PREFIX_NAMES.has(name) || PERSONA_LEGACY_NAMES.has(name); +} + +function stripIdentityPrefix(text) { + let rest = String(text || "").trim(); + if (rest.startsWith(IDENTITY_CARD_PREFIX)) { + const split = rest.indexOf("\n\n"); + rest = split >= 0 ? rest.slice(split + 2).trim() : ""; + } + if (rest.startsWith(OLD_IDENTITY_LOCK)) { + rest = rest.slice(OLD_IDENTITY_LOCK.length).trim(); + } + return rest; +} + +function hoistFoldedPersona(sections, hasPrefix) { + const list = Array.isArray(sections) ? sections.slice() : []; + const idx = list.findIndex((section) => isPersonaFoldTarget(String(section?.name || ""), hasPrefix)); + if (idx <= 0) return list; + const [hit] = list.splice(idx, 1); + return [hit, ...list]; +} + +/** 1.1.34:把 inject 写进 persona-prefix 正文开头,不再另开 dsh-purge 段。 */ +function foldInjectIntoPersona(sections, injectText) { + const inject = typeof injectText === "string" ? injectText : ""; + const list = Array.isArray(sections) ? sections : []; + const names = list.map((section) => String(section?.name || "")); + const hasPrefix = names.some((name) => PERSONA_PREFIX_NAMES.has(name)); + let folded = false; + const out = list.map((section) => { + const name = String(section?.name || ""); + if (!isPersonaFoldTarget(name, hasPrefix)) return section; + folded = true; + const prior = stripIdentityPrefix(String(section.text || "")); + if (!inject) return { ...section, text: prior, order: GLOBAL_PROMPT_ORDER }; + if (prior.includes(inject)) return { ...section, order: GLOBAL_PROMPT_ORDER, interpolate: false }; + return { + ...section, + text: prior ? `${inject}\n\n${prior}` : inject, + order: GLOBAL_PROMPT_ORDER, + interpolate: false, + }; + }); + if (folded) return hoistFoldedPersona(out, hasPrefix); + if (!inject) return out; + return hoistFoldedPersona([ + { + name: "deployment:persona-prefix", + text: inject, + order: GLOBAL_PROMPT_ORDER, + interpolate: false, + }, + ...out, + ], true); +} + /** 插件写死的执行句,不是用户的规则集。任何模式都不注入。 */ function isHardcodedExecRule(section) { if (section?.name !== "dsh-purge:rules") return false; @@ -395,8 +456,8 @@ function isRedteamSubagent(context) { } /** - * 红队 persona 只留调度正文,不再把 inject / 目标卡 / 角色折进去。 - * inject 和 cap 由 rewritePromptAssembly 置顶;卡和角色用不带 dsh-purge 前缀的段,避免 host 过滤器提到 cap 前面。 + * 红队 persona 只留调度正文。inject 由 foldInjectIntoPersona 写进 prefix 开头; + * 卡和角色用不带 dsh-purge 前缀的段,避免 host 过滤器提到 cap 前面。 */ function attachRedteamOpsOnly(sections) { let attached = false; @@ -689,15 +750,7 @@ export function rewritePromptAssembly(assembled, fallbackInject = "", options = sections = insertRoleSection(insertCardSection(withOps, opts.cardText), opts.roleText); } const body = overlayOfficialIdentity(sanitizeAssemblySections(sections)); - const head = []; - if (String(injectText || "").trim()) { - head.push({ - name: "dsh-purge", - text: injectText, - order: -100000, - interpolate: false, - }); - } + const folded = foldInjectIntoPersona(body, injectText); const tail = []; if (String(protocol || "").trim()) { tail.push({ @@ -707,7 +760,8 @@ export function rewritePromptAssembly(assembled, fallbackInject = "", options = interpolate: false, }); } - const outSections = ensureFullInject([...head, ...body, ...tail], injectText); + const withoutCapDup = folded.filter((section) => section?.name !== CAPABILITY_SECTION); + const outSections = ensureFullInject([...withoutCapDup, ...tail], injectText); return { ...assembled, sections: outSections, diff --git a/lib/index.js b/lib/index.js index 8cd12b6..2c4d447 100644 --- a/lib/index.js +++ b/lib/index.js @@ -494,9 +494,12 @@ function rewriteLiveAssembly(config, assembled, context, decision = { mode: "ful dropWorkspaceBaseline, }); const closerFollow = closerFollowBytes(rewritten?.sections); + let subagent = false; + try { subagent = isChildAgentContext(context); } catch { subagent = false; } purgeLog("assemble.rewrite", { enabled, redteam, + sub: subagent, mode: decision?.mode || "full", task: decision?.taskKind || "general", last: decision?.lastRole || "unknown", @@ -509,7 +512,21 @@ function rewriteLiveAssembly(config, assembled, context, decision = { mode: "ful strip: config.stripMnemon !== false, policy, dropWorkspaceBaseline, - injectHead: Array.isArray(rewritten?.sections) && rewritten.sections[0]?.name === "dsh-purge", + injectHead: Array.isArray(rewritten?.sections) && (() => { + const first = rewritten.sections[0]; + const firstText = String(first?.text || ""); + const name = String(first?.name || ""); + if (name === "dsh-purge") return true; + if (!inject) return false; + return /persona-prefix$|^persona$|^deployment:persona$/.test(name) && firstText.includes(inject); + })(), + folded: Array.isArray(rewritten?.sections) && (() => { + const first = rewritten.sections[0]; + const name = String(first?.name || ""); + if (!inject) return false; + return /persona-prefix$|^persona$|^deployment:persona$/.test(name) + && String(first?.text || "").includes(inject); + })(), injectTail: Array.isArray(rewritten?.sections) && rewritten.sections.length > 0 && rewritten.sections[rewritten.sections.length - 1]?.name === "dsh-purge", chatml: Array.isArray(rewritten?.sections) && /|<\|im_start\|>/i.test(String(rewritten.sections[0]?.text || "")), @@ -563,7 +580,6 @@ function permissionPresetsOf(ctx) { } function shouldKeepFreshFullAccess(session, svc) { - if (session?.header?.origin === "subagent") return false; const state = typeof svc.permissionState === "function" ? svc.permissionState(session) : null; const preset = state?.preset; if (preset === "danger-full-access" || preset === "auto" || preset === "custom") return false; @@ -761,9 +777,18 @@ function installIdentityOverride(ctx, config = {}) { if (!ctx.__dshPurgeSessionHook) { ctx.__dshPurgeSessionHook = true; try { - ctx.on("session/created", () => { + ctx.on("session/created", (session) => { try { refreshHungOperator(core.findDshHome()); } catch { /* 主目录还没有提示词 */ } wrapAssemble("sessionCreated"); + try { + if (session?.header?.origin !== "subagent") return; + const prepare = globalThis.__dshPurgePrepareAssemble; + if (typeof prepare !== "function") return; + const agents = ctx.agents || (typeof ctx.get === "function" ? ctx.get("agents") : null); + const child = typeof agents?.get === "function" ? agents.get(session.id) : null; + if (child?.ctx) prepare(child.ctx); + else prepare(ctx); + } catch { /* 子会话此时可能还没有 agent */ } }); } catch { /* ignore */ } }