Skip to content

Commit 57dfd50

Browse files
authored
Merge pull request #307 from Opencode-DCP/feature/injection-and-defensive-checks
Improve injection logic and add defensive array checks
2 parents 9f9023d + d1f3f43 commit 57dfd50

File tree

12 files changed

+73
-30
lines changed

12 files changed

+73
-30
lines changed

lib/commands/context.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ function analyzeTokens(state: SessionState, messages: WithParts[]): TokenBreakdo
117117
if (isMessageCompacted(state, msg)) continue
118118
if (msg.info.role === "user" && isIgnoredUserMessage(msg)) continue
119119

120-
for (const part of msg.parts) {
120+
const parts = Array.isArray(msg.parts) ? msg.parts : []
121+
for (const part of parts) {
121122
if (part.type === "text" && msg.info.role === "user") {
122123
const textPart = part as TextPart
123124
const text = textPart.text || ""

lib/commands/sweep.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ function collectToolIdsAfterIndex(
5252
if (isMessageCompacted(state, msg)) {
5353
continue
5454
}
55-
if (msg.parts) {
56-
for (const part of msg.parts) {
55+
const parts = Array.isArray(msg.parts) ? msg.parts : []
56+
if (parts.length > 0) {
57+
for (const part of parts) {
5758
if (part.type === "tool" && part.callID && part.tool) {
5859
toolIds.push(part.callID)
5960
}

lib/messages/inject.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
extractParameterKey,
88
buildToolIdList,
99
createSyntheticAssistantMessage,
10+
createSyntheticUserMessage,
1011
isIgnoredUserMessage,
1112
} from "./utils"
1213
import { getFilePathFromParameters, isProtectedFilePath } from "../protected-file-patterns"
@@ -138,16 +139,18 @@ export const insertPruneToolContext = (
138139
return
139140
}
140141

141-
// Never inject immediately following a user message - wait until assistant has started its turn
142-
// This avoids interfering with model reasoning/thinking phases
143-
// TODO: This can be skipped if there is a good way to check if the model has reasoning,
144-
// can't find a good way to do this yet
145-
const lastMessage = messages[messages.length - 1]
146-
if (lastMessage?.info?.role === "user" && !isIgnoredUserMessage(lastMessage)) {
147-
return
148-
}
149-
150142
const userInfo = lastUserMessage.info as UserMessage
151143
const variant = state.variant ?? userInfo.variant
152-
messages.push(createSyntheticAssistantMessage(lastUserMessage, prunableToolsContent, variant))
144+
145+
const lastMessage = messages[messages.length - 1]
146+
const isLastMessageUser =
147+
lastMessage?.info?.role === "user" && !isIgnoredUserMessage(lastMessage)
148+
149+
if (isLastMessageUser) {
150+
messages.push(createSyntheticUserMessage(lastUserMessage, prunableToolsContent, variant))
151+
} else {
152+
messages.push(
153+
createSyntheticAssistantMessage(lastUserMessage, prunableToolsContent, variant),
154+
)
155+
}
153156
}

lib/messages/prune.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const pruneToolOutputs = (state: SessionState, logger: Logger, messages: WithPar
2525
continue
2626
}
2727

28-
for (const part of msg.parts) {
28+
const parts = Array.isArray(msg.parts) ? msg.parts : []
29+
for (const part of parts) {
2930
if (part.type !== "tool") {
3031
continue
3132
}
@@ -50,7 +51,8 @@ const pruneToolInputs = (state: SessionState, logger: Logger, messages: WithPart
5051
continue
5152
}
5253

53-
for (const part of msg.parts) {
54+
const parts = Array.isArray(msg.parts) ? msg.parts : []
55+
for (const part of parts) {
5456
if (part.type !== "tool") {
5557
continue
5658
}
@@ -77,7 +79,8 @@ const pruneToolErrors = (state: SessionState, logger: Logger, messages: WithPart
7779
continue
7880
}
7981

80-
for (const part of msg.parts) {
82+
const parts = Array.isArray(msg.parts) ? msg.parts : []
83+
for (const part of parts) {
8184
if (part.type !== "tool") {
8285
continue
8386
}

lib/messages/utils.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,36 @@ const isGeminiModel = (modelID: string): boolean => {
1212
return lowerModelID.includes("gemini")
1313
}
1414

15+
export const createSyntheticUserMessage = (
16+
baseMessage: WithParts,
17+
content: string,
18+
variant?: string,
19+
): WithParts => {
20+
const userInfo = baseMessage.info as UserMessage
21+
const now = Date.now()
22+
23+
return {
24+
info: {
25+
id: SYNTHETIC_MESSAGE_ID,
26+
sessionID: userInfo.sessionID,
27+
role: "user" as const,
28+
agent: userInfo.agent || "code",
29+
model: userInfo.model,
30+
time: { created: now },
31+
...(variant !== undefined && { variant }),
32+
},
33+
parts: [
34+
{
35+
id: SYNTHETIC_PART_ID,
36+
sessionID: userInfo.sessionID,
37+
messageID: SYNTHETIC_MESSAGE_ID,
38+
type: "text",
39+
text: content,
40+
},
41+
],
42+
}
43+
}
44+
1545
export const createSyntheticAssistantMessage = (
1646
baseMessage: WithParts,
1747
content: string,
@@ -197,8 +227,9 @@ export function buildToolIdList(
197227
if (isMessageCompacted(state, msg)) {
198228
continue
199229
}
200-
if (msg.parts) {
201-
for (const part of msg.parts) {
230+
const parts = Array.isArray(msg.parts) ? msg.parts : []
231+
if (parts.length > 0) {
232+
for (const part of parts) {
202233
if (part.type === "tool" && part.callID && part.tool) {
203234
toolIds.push(part.callID)
204235
}
@@ -209,11 +240,12 @@ export function buildToolIdList(
209240
}
210241

211242
export const isIgnoredUserMessage = (message: WithParts): boolean => {
212-
if (!message.parts || message.parts.length === 0) {
243+
const parts = Array.isArray(message.parts) ? message.parts : []
244+
if (parts.length === 0) {
213245
return true
214246
}
215247

216-
for (const part of message.parts) {
248+
for (const part of parts) {
217249
if (!(part as any).ignored) {
218250
return false
219251
}

lib/prompts/discard-tool-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Use \`discard\` for removing tool content that is no longer needed
1212
1313
## When NOT to Use This Tool
1414
15-
- **If the output contains useful information:** Use \`extract\` instead to preserve key findings.
15+
- **If the output contains useful information:** Keep it in context rather than discarding.
1616
- **If you'll need the output later:** Don't discard files you plan to edit or context you'll need for implementation.
1717
1818
## Best Practices

lib/prompts/system/both.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const SYSTEM_PROMPT_BOTH = `<system-reminder>
22
<instruction name=context_management_protocol policy_level=critical>
33
44
ENVIRONMENT
5-
You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\` and \`extract\` tools. The environment calls the \`context_info\` tool to provide an up-to-date <prunable-tools> list after each assistant turn. Use this information when deciding what to prune.
5+
You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\` and \`extract\` tools. The environment calls the \`context_info\` tool to provide an up-to-date <prunable-tools> list after each turn. Use this information when deciding what to prune.
66
77
IMPORTANT: The \`context_info\` tool is only available to the environment - you do not have access to it and must not attempt to call it.
88
@@ -44,7 +44,7 @@ There may be tools in session context that do not appear in the <prunable-tools>
4444
</instruction>
4545
4646
<instruction name=injected_context_handling policy_level=critical>
47-
After each assistant turn, the environment calls the \`context_info\` tool to inject an assistant message containing a <prunable-tools> list and optional nudge instruction. This tool is only available to the environment - you do not have access to it.
47+
After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a <prunable-tools> list and optional nudge instruction. This tool is only available to the environment - you do not have access to it.
4848
4949
CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE:
5050
- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears.

lib/prompts/system/discard.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const SYSTEM_PROMPT_DISCARD = `<system-reminder>
22
<instruction name=context_management_protocol policy_level=critical>
33
44
ENVIRONMENT
5-
You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\` tool. The environment calls the \`context_info\` tool to provide an up-to-date <prunable-tools> list after each assistant turn. Use this information when deciding what to discard.
5+
You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\` tool. The environment calls the \`context_info\` tool to provide an up-to-date <prunable-tools> list after each turn. Use this information when deciding what to discard.
66
77
IMPORTANT: The \`context_info\` tool is only available to the environment - you do not have access to it and must not attempt to call it.
88
@@ -35,7 +35,7 @@ There may be tools in session context that do not appear in the <prunable-tools>
3535
</instruction>
3636
3737
<instruction name=injected_context_handling policy_level=critical>
38-
After each assistant turn, the environment calls the \`context_info\` tool to inject an assistant message containing a <prunable-tools> list and optional nudge instruction. This tool is only available to the environment - you do not have access to it.
38+
After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a <prunable-tools> list and optional nudge instruction. This tool is only available to the environment - you do not have access to it.
3939
4040
CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE:
4141
- NEVER reference the discard encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the discard encouragement appears.

lib/prompts/system/extract.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const SYSTEM_PROMPT_EXTRACT = `<system-reminder>
22
<instruction name=context_management_protocol policy_level=critical>
33
44
ENVIRONMENT
5-
You are operating in a context-constrained environment and thus must proactively manage your context window using the \`extract\` tool. The environment calls the \`context_info\` tool to provide an up-to-date <prunable-tools> list after each assistant turn. Use this information when deciding what to extract.
5+
You are operating in a context-constrained environment and thus must proactively manage your context window using the \`extract\` tool. The environment calls the \`context_info\` tool to provide an up-to-date <prunable-tools> list after each turn. Use this information when deciding what to extract.
66
77
IMPORTANT: The \`context_info\` tool is only available to the environment - you do not have access to it and must not attempt to call it.
88
@@ -35,7 +35,7 @@ There may be tools in session context that do not appear in the <prunable-tools>
3535
</instruction>
3636
3737
<instruction name=injected_context_handling policy_level=critical>
38-
After each assistant turn, the environment calls the \`context_info\` tool to inject an assistant message containing a <prunable-tools> list and optional nudge instruction. This tool is only available to the environment - you do not have access to it.
38+
After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a <prunable-tools> list and optional nudge instruction. This tool is only available to the environment - you do not have access to it.
3939
4040
CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE:
4141
- NEVER reference the extract encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the extract encouragement appears.

lib/state/state.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ export function countTurns(state: SessionState, messages: WithParts[]): number {
131131
if (isMessageCompacted(state, msg)) {
132132
continue
133133
}
134-
for (const part of msg.parts) {
134+
const parts = Array.isArray(msg.parts) ? msg.parts : []
135+
for (const part of parts) {
135136
if (part.type === "step-start") {
136137
turnCount++
137138
}

0 commit comments

Comments
 (0)