From 850fb0378e26f98df8bcc0a0f7203eae66f02fae Mon Sep 17 00:00:00 2001 From: Maxim Harizanov Date: Wed, 18 Feb 2026 21:54:20 +0200 Subject: [PATCH] fix(copilot): mark internal hook injections as agent-initiated Apply the internal initiator marker to automated continuation, recovery, babysitter, stop-hook, and hook-message injections so Copilot attribution consistently sets x-initiator=agent for system-generated prompts. --- src/features/hook-message-injector/injector.ts | 4 ++-- src/hooks/atlas/boulder-continuation-injector.ts | 4 ++-- .../handlers/session-event-handler.ts | 4 ++-- src/hooks/ralph-loop/continuation-prompt-injector.ts | 8 ++++++-- src/hooks/session-recovery/resume.test.ts | 4 ++++ src/hooks/session-recovery/resume.ts | 4 ++-- .../continuation-injection.test.ts | 11 ++++++++++- .../continuation-injection.ts | 8 ++++++-- src/hooks/unstable-agent-babysitter/index.test.ts | 3 +++ .../unstable-agent-babysitter-hook.ts | 4 ++-- 10 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/features/hook-message-injector/injector.ts b/src/features/hook-message-injector/injector.ts index 97ee8a7fa..8f4e0d57b 100644 --- a/src/features/hook-message-injector/injector.ts +++ b/src/features/hook-message-injector/injector.ts @@ -5,7 +5,7 @@ import { MESSAGE_STORAGE, PART_STORAGE } from "./constants" import type { MessageMeta, OriginalMessageContext, TextPart, ToolPermission } from "./types" import { log } from "../../shared/logger" import { isSqliteBackend } from "../../shared/opencode-storage-detection" -import { normalizeSDKResponse } from "../../shared" +import { createInternalAgentTextPart, normalizeSDKResponse } from "../../shared" export interface StoredMessage { agent?: string @@ -331,7 +331,7 @@ export function injectHookMessage( const textPart: TextPart = { id: partID, type: "text", - text: hookContent, + text: createInternalAgentTextPart(hookContent).text, synthetic: true, time: { start: now, diff --git a/src/hooks/atlas/boulder-continuation-injector.ts b/src/hooks/atlas/boulder-continuation-injector.ts index 68ce30643..289668b4b 100644 --- a/src/hooks/atlas/boulder-continuation-injector.ts +++ b/src/hooks/atlas/boulder-continuation-injector.ts @@ -1,7 +1,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { BackgroundManager } from "../../features/background-agent" import { log } from "../../shared/logger" -import { resolveInheritedPromptTools } from "../../shared" +import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" import { HOOK_NAME } from "./hook-name" import { BOULDER_CONTINUATION_PROMPT } from "./system-reminder-templates" import { resolveRecentPromptContextForSession } from "./recent-model-resolver" @@ -53,7 +53,7 @@ export async function injectBoulderContinuation(input: { agent: agent ?? "atlas", ...(promptContext.model !== undefined ? { model: promptContext.model } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}), - parts: [{ type: "text", text: prompt }], + parts: [createInternalAgentTextPart(prompt)], }, query: { directory: ctx.directory }, }) diff --git a/src/hooks/claude-code-hooks/handlers/session-event-handler.ts b/src/hooks/claude-code-hooks/handlers/session-event-handler.ts index 6c6d3a584..4c845004c 100644 --- a/src/hooks/claude-code-hooks/handlers/session-event-handler.ts +++ b/src/hooks/claude-code-hooks/handlers/session-event-handler.ts @@ -3,7 +3,7 @@ import { loadClaudeHooksConfig } from "../config" import { loadPluginExtendedConfig } from "../config-loader" import { executeStopHooks, type StopContext } from "../stop" import type { PluginConfig } from "../types" -import { isHookDisabled, log } from "../../../shared" +import { createInternalAgentTextPart, isHookDisabled, log } from "../../../shared" import { clearSessionHookState, sessionErrorState, @@ -94,7 +94,7 @@ export function createSessionEventHandler(ctx: PluginInput, config: PluginConfig .prompt({ path: { id: sessionID }, body: { - parts: [{ type: "text", text: stopResult.injectPrompt }], + parts: [createInternalAgentTextPart(stopResult.injectPrompt)], }, query: { directory: ctx.directory }, }) diff --git a/src/hooks/ralph-loop/continuation-prompt-injector.ts b/src/hooks/ralph-loop/continuation-prompt-injector.ts index 1d1260b5d..bfe18d83d 100644 --- a/src/hooks/ralph-loop/continuation-prompt-injector.ts +++ b/src/hooks/ralph-loop/continuation-prompt-injector.ts @@ -3,7 +3,11 @@ import { log } from "../../shared/logger" import { findNearestMessageWithFields } from "../../features/hook-message-injector" import { getMessageDir } from "./message-storage-directory" import { withTimeout } from "./with-timeout" -import { normalizeSDKResponse, resolveInheritedPromptTools } from "../../shared" +import { + createInternalAgentTextPart, + normalizeSDKResponse, + resolveInheritedPromptTools, +} from "../../shared" type MessageInfo = { agent?: string @@ -64,7 +68,7 @@ export async function injectContinuationPrompt( ...(agent !== undefined ? { agent } : {}), ...(model !== undefined ? { model } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}), - parts: [{ type: "text", text: options.prompt }], + parts: [createInternalAgentTextPart(options.prompt)], }, query: { directory: options.directory }, }) diff --git a/src/hooks/session-recovery/resume.test.ts b/src/hooks/session-recovery/resume.test.ts index 20d76263d..fff669984 100644 --- a/src/hooks/session-recovery/resume.test.ts +++ b/src/hooks/session-recovery/resume.test.ts @@ -1,6 +1,7 @@ declare const require: (name: string) => any const { describe, expect, test } = require("bun:test") import { extractResumeConfig, resumeSession } from "./resume" +import { OMO_INTERNAL_INITIATOR_MARKER } from "../../shared/internal-initiator-marker" import type { MessageData } from "./types" describe("session-recovery resume", () => { @@ -44,5 +45,8 @@ describe("session-recovery resume", () => { // then expect(ok).toBe(true) expect(promptBody?.tools).toEqual({ question: false, bash: true }) + expect(Array.isArray(promptBody?.parts)).toBe(true) + const firstPart = (promptBody?.parts as Array<{ text?: string }>)?.[0] + expect(firstPart?.text).toContain(OMO_INTERNAL_INITIATOR_MARKER) }) }) diff --git a/src/hooks/session-recovery/resume.ts b/src/hooks/session-recovery/resume.ts index 2d5805b77..e5d187d79 100644 --- a/src/hooks/session-recovery/resume.ts +++ b/src/hooks/session-recovery/resume.ts @@ -1,6 +1,6 @@ import type { createOpencodeClient } from "@opencode-ai/sdk" import type { MessageData, ResumeConfig } from "./types" -import { resolveInheritedPromptTools } from "../../shared" +import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" const RECOVERY_RESUME_TEXT = "[session recovered - continuing previous task]" @@ -30,7 +30,7 @@ export async function resumeSession(client: Client, config: ResumeConfig): Promi await client.session.promptAsync({ path: { id: config.sessionID }, body: { - parts: [{ type: "text", text: RECOVERY_RESUME_TEXT }], + parts: [createInternalAgentTextPart(RECOVERY_RESUME_TEXT)], agent: config.agent, model: config.model, ...(inheritedTools ? { tools: inheritedTools } : {}), diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts b/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts index f8c019a97..0b628ca1e 100644 --- a/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts +++ b/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts @@ -2,18 +2,26 @@ declare const require: (name: string) => any const { describe, expect, test } = require("bun:test") import { injectContinuation } from "./continuation-injection" +import { OMO_INTERNAL_INITIATOR_MARKER } from "../../shared/internal-initiator-marker" describe("injectContinuation", () => { test("inherits tools from resolved message info when reinjecting", async () => { // given let capturedTools: Record | undefined + let capturedText: string | undefined const ctx = { directory: "/tmp/test", client: { session: { todo: async () => ({ data: [{ id: "1", content: "todo", status: "pending", priority: "high" }] }), - promptAsync: async (input: { body: { tools?: Record } }) => { + promptAsync: async (input: { + body: { + tools?: Record + parts?: Array<{ type: string; text: string }> + } + }) => { capturedTools = input.body.tools + capturedText = input.body.parts?.[0]?.text return {} }, }, @@ -37,5 +45,6 @@ describe("injectContinuation", () => { // then expect(capturedTools).toEqual({ question: false, bash: true }) + expect(capturedText).toContain(OMO_INTERNAL_INITIATOR_MARKER) }) }) diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection.ts b/src/hooks/todo-continuation-enforcer/continuation-injection.ts index ba60d23b0..23b11ba68 100644 --- a/src/hooks/todo-continuation-enforcer/continuation-injection.ts +++ b/src/hooks/todo-continuation-enforcer/continuation-injection.ts @@ -1,7 +1,11 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { BackgroundManager } from "../../features/background-agent" -import { normalizeSDKResponse, resolveInheritedPromptTools } from "../../shared" +import { + createInternalAgentTextPart, + normalizeSDKResponse, + resolveInheritedPromptTools, +} from "../../shared" import { findNearestMessageWithFields, findNearestMessageWithFieldsFromSDK, @@ -151,7 +155,7 @@ ${todoList}` agent: agentName, ...(model !== undefined ? { model } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}), - parts: [{ type: "text", text: prompt }], + parts: [createInternalAgentTextPart(prompt)], }, query: { directory: ctx.directory }, }) diff --git a/src/hooks/unstable-agent-babysitter/index.test.ts b/src/hooks/unstable-agent-babysitter/index.test.ts index 355072b56..8dd6fa038 100644 --- a/src/hooks/unstable-agent-babysitter/index.test.ts +++ b/src/hooks/unstable-agent-babysitter/index.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test" import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state" import type { BackgroundTask } from "../../features/background-agent" +import { OMO_INTERNAL_INITIATOR_MARKER } from "../../shared/internal-initiator-marker" import { createUnstableAgentBabysitterHook } from "./index" const projectDir = process.cwd() @@ -93,6 +94,7 @@ describe("unstable-agent-babysitter hook", () => { expect(text).toContain("background_output") expect(text).toContain("background_cancel") expect(text).toContain("deep thought") + expect(text).toContain(OMO_INTERNAL_INITIATOR_MARKER) }) test("fires reminder for hung minimax task", async () => { @@ -128,6 +130,7 @@ describe("unstable-agent-babysitter hook", () => { expect(text).toContain("background_output") expect(text).toContain("background_cancel") expect(text).toContain("minimax thought") + expect(text).toContain(OMO_INTERNAL_INITIATOR_MARKER) }) test("does not remind stable model tasks", async () => { diff --git a/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts b/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts index dba65f8da..9bfdbb01a 100644 --- a/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts +++ b/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts @@ -1,7 +1,7 @@ import type { BackgroundManager } from "../../features/background-agent" import { getMainSessionID, getSessionAgent } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" -import { resolveInheritedPromptTools } from "../../shared" +import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" import { buildReminder, extractMessages, @@ -158,7 +158,7 @@ export function createUnstableAgentBabysitterHook(ctx: BabysitterContext, option ...(agent ? { agent } : {}), ...(model ? { model } : {}), ...(tools ? { tools } : {}), - parts: [{ type: "text", text: reminder }], + parts: [createInternalAgentTextPart(reminder)], }, query: { directory: ctx.directory }, })