diff --git a/bun.lock b/bun.lock index a1086cc89..31ef61ee5 100644 --- a/bun.lock +++ b/bun.lock @@ -30,17 +30,17 @@ "zod": "^4.3.0", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "4.0.0", - "oh-my-opencode-darwin-x64": "4.0.0", - "oh-my-opencode-darwin-x64-baseline": "4.0.0", - "oh-my-opencode-linux-arm64": "4.0.0", - "oh-my-opencode-linux-arm64-musl": "4.0.0", - "oh-my-opencode-linux-x64": "4.0.0", - "oh-my-opencode-linux-x64-baseline": "4.0.0", - "oh-my-opencode-linux-x64-musl": "4.0.0", - "oh-my-opencode-linux-x64-musl-baseline": "4.0.0", - "oh-my-opencode-windows-x64": "4.0.0", - "oh-my-opencode-windows-x64-baseline": "4.0.0", + "oh-my-opencode-darwin-arm64": "4.1.0", + "oh-my-opencode-darwin-x64": "4.1.0", + "oh-my-opencode-darwin-x64-baseline": "4.1.0", + "oh-my-opencode-linux-arm64": "4.1.0", + "oh-my-opencode-linux-arm64-musl": "4.1.0", + "oh-my-opencode-linux-x64": "4.1.0", + "oh-my-opencode-linux-x64-baseline": "4.1.0", + "oh-my-opencode-linux-x64-musl": "4.1.0", + "oh-my-opencode-linux-x64-musl-baseline": "4.1.0", + "oh-my-opencode-windows-x64": "4.1.0", + "oh-my-opencode-windows-x64-baseline": "4.1.0", }, "peerDependencies": { "zod": "^4.0.0", @@ -248,27 +248,27 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@4.0.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-t7b65Oucpa5RrbRrkc37xlUu4jWPh3WgxSi5yhJlXHf0HuUIzmw4Lb+3Z/0ZzdcC/0pbdiPNnE7zxAlcZVZscQ=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@4.1.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-0GaAhgMLRdoftNs1OR0NCii6rhZUUSb5sURHWcVnlJ6Ndqcm1c9ftkPr6f6cDIg2F/vvjfxP14FULtEvAzLTMQ=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@4.0.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-KySGNCvz1yUBKR9euzFgRdlqmc5VnhPxNl6dbVbNrjBkoXjqdDgWK4+FuTjRnaoEH8I5qkpLenmDVYDWHUuxzA=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@4.1.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-4Zm/oX2OECaKFxYX8VPryfChFW8lem2q3Tn4sCrn7aE6AB8xWq6VCe9+O/mN3EW1Z1WJC/s8/yqwX6lit7Qsxw=="], - "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@4.0.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-BDzcEMxPCKD6/HIRBwCzQWUDgWLIXLjWazOVTYHb8ZsZS+UiFW9bPMs92Qj8NN8Nt1WzhTI91AxG1j+82MWdow=="], + "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@4.1.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-VHC9Zh/fzIMmeVbHblaVictDUfiQGByY3KDC2rkojEazTFB9WYcSGIp6W9RhxA/fg1OCGYskDIbdhmmEz2KDLQ=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@4.0.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-9CRfdF6ZDtBjTw2R1u6K12GVaUkWOrSavQ1+r2oHR05qzXKuFB9fL7cX6XFC1nJ0sdrzbPS7Wb95NTNT57f3sQ=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@4.1.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-4ou8Z2J9dquzCBKIZQ/UnIRxqou7lU2yW9dYy1Soh4XwRKFqTXAEmGNRVr/2uKBIPxu4cm3sfw5ixhIPYly3Eg=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@4.0.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-DtASaFt42Q7cGWniR8zAiMy+Lw1LQa715HBA9IceuqX5V+T5zKB+CdjAw+/TH2RSDe05sp0Jvr8sCgSxPvH6hg=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@4.1.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-NG2x8CMOP6E0UAAuJhWwfsr4rmQuXNkp4sOHftfeiq0IcecNuj10zkb73TkNhGrKJg5CXnuqMpew1p80KuEl7w=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@4.0.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-CNkI+nMK5tk94cDXSe9y5MZNTI/sSof2qGy69pUZz9kG6NMqXx6QeqCx36J66xacW3Oxa7Pvvcu+7heaNpL9Bw=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@4.1.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-8ljrAKgweTc1XrdkSR+UluYasb8flJTVBfqyamKTVRc7I8tIskkrp43/QSLCjJRT6v7Ejx1PYe9MOGDPQBrlMQ=="], - "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@4.0.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-rFpWgevtWNdflrTjcsFXUuELwKZGY6jOJ0HaXm1WZ406gswXfqR8VEUlrUYps6toycstZsdPuQXGSgbD8SfYKQ=="], + "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@4.1.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-oPPHWAUiYvoykozC3JQl91PGilY7bN0GCdTN+Dq+oj8cdLnfXjULwtdUm9y3gpN9yWy9d/IB7McvM2pFCKy6Bw=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@4.0.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-hyCZ9GCi2RiqI1gqH9S8VZPqDz+c0EUT6ne1spTZBMS6qseSPOIztn+ZoplBXAgsOvEGum45ydcYFjkvLaqNFA=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@4.1.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-FAiyeKOO+38B3XR0vb3MiBnCgEl9un42KClQ/3ggCPV4f5uMzu5tVa6rlPHMATxryl4uk6ysdFeWr4XXN3ci6A=="], - "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@4.0.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-u8w5WP5eGH0kUnQKHJFgVzQaf/sE6cOctIDzEr04D2k2A3khRmItwaJ+0o84GeXTjCnXtAeX2SFn1hut+H6Rgg=="], + "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@4.1.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-9DChW9j0iCx3xO/oL2LXZmKaR8L/sYH6Se0v9zv7RS9kU4gxvS9Gd4kzJoFnSKiw/2xUpjjkHk/SPxsoytvQ3A=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@4.0.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-+aRlStPSkaHs9awlrhHQ4/em5SmyHQVbNY15y4dfs/OHKdFtDbFQDrS22dqqpBOHmucESf96mEzAs9RGfUvzOA=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@4.1.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-F6X1elfOA0rOSC8nB+Od4/9sHz0NNv6hIiZ6XkuqPBSWD5DymtjYjHIRKfumR9mYxkDOVp4ajU4hdLtO3tcxgA=="], - "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@4.0.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-VonuZat8PK3TKPufpjQsrkIy3L1+N5yVBl/na43n1E6GNrLe5Qv3TBqfDFewAS/Ogv2P3uh5FIn3v7DhF8Bh+A=="], + "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@4.1.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-91RFjv+sGJqQ/tgun0sV+xpkOcgoJ8Pv/mTN5qPyivJtIsTceSUqoJyHyz3tmpyckdKLl46NmzF1DLWCRUIwCA=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/hooks/atlas/boulder-continuation-injector.test.ts b/src/hooks/atlas/boulder-continuation-injector.test.ts index 902bfe78f..b04e03915 100644 --- a/src/hooks/atlas/boulder-continuation-injector.test.ts +++ b/src/hooks/atlas/boulder-continuation-injector.test.ts @@ -168,6 +168,11 @@ describe("injectBoulderContinuation", () => { body?: { model?: { providerID: string; modelID: string } variant?: string + noReply?: boolean + parts?: Array<{ + synthetic?: boolean + metadata?: Record + }> } }> = [] const promptAsyncMock = mock(async (request: unknown) => { @@ -219,5 +224,9 @@ describe("injectBoulderContinuation", () => { modelID: "claude-sonnet-4-20250514", }) expect(capturedRequests[0]?.body?.variant).toBe("max") + expect(capturedRequests[0]?.body?.noReply).toBeUndefined() + const promptPart = capturedRequests[0]?.body?.parts?.[0] + expect(promptPart?.synthetic).toBe(true) + expect(promptPart?.metadata?.compaction_continue).toBe(true) }) }) diff --git a/src/hooks/atlas/boulder-continuation-injector.ts b/src/hooks/atlas/boulder-continuation-injector.ts index 8fc401b06..6037351de 100644 --- a/src/hooks/atlas/boulder-continuation-injector.ts +++ b/src/hooks/atlas/boulder-continuation-injector.ts @@ -4,7 +4,7 @@ import { resolveRegisteredAgentName, } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" -import { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" +import { createInternalAgentContinuationTextPart, resolveInheritedPromptTools } from "../../shared" import { HOOK_NAME } from "./hook-name" import { BOULDER_CONTINUATION_PROMPT } from "./system-reminder-templates" import { resolveRecentPromptContextForSession } from "./recent-model-resolver" @@ -89,7 +89,7 @@ export async function injectBoulderContinuation(input: { ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}), - parts: [createInternalAgentTextPart(prompt)], + parts: [createInternalAgentContinuationTextPart(prompt)], }, query: { directory: ctx.directory }, }) diff --git a/src/hooks/atlas/idle-event.test.ts b/src/hooks/atlas/idle-event.test.ts index 61f9204bf..ae3971b27 100644 --- a/src/hooks/atlas/idle-event.test.ts +++ b/src/hooks/atlas/idle-event.test.ts @@ -71,8 +71,26 @@ describe("handleAtlasSessionIdle completion nudge", () => { writeBoulderState(testDirectory, boulder) - const promptRequests: Array<{ body?: { parts?: Array<{ text?: string }> } }> = [] - const promptAsyncMock = mock(async (request: { body?: { parts?: Array<{ text?: string }> } }) => { + const promptRequests: Array<{ + body?: { + noReply?: boolean + parts?: Array<{ + text?: string + synthetic?: boolean + metadata?: Record + }> + } + }> = [] + const promptAsyncMock = mock(async (request: { + body?: { + noReply?: boolean + parts?: Array<{ + text?: string + synthetic?: boolean + metadata?: Record + }> + } + }) => { promptRequests.push(request) return { data: {} } }) @@ -118,6 +136,9 @@ describe("handleAtlasSessionIdle completion nudge", () => { expect(promptText).toContain("- 1 Parse input: 1m 1s") expect(promptText).toContain("- 2 Save output: 4s") expect(promptText).not.toContain("{ELAPSED_HUMAN}") + expect(promptRequests[0]?.body?.noReply).toBeUndefined() + expect(promptRequests[0]?.body?.parts?.[0]?.synthetic).toBe(true) + expect(promptRequests[0]?.body?.parts?.[0]?.metadata?.compaction_continue).toBe(true) const persistedState = getState(SESSION_ID) expect(persistedState.boulderCompletionNudgedAt?.[workId]).toBeNumber() diff --git a/src/hooks/atlas/idle-event.ts b/src/hooks/atlas/idle-event.ts index b4803bb5e..118ed4e2d 100644 --- a/src/hooks/atlas/idle-event.ts +++ b/src/hooks/atlas/idle-event.ts @@ -16,7 +16,7 @@ import { } from "../../features/claude-code-session-state" import { getLastAgentFromSession } from "./session-last-agent" import { isSessionInBoulderLineage } from "./boulder-session-lineage" -import { createInternalAgentTextPart } from "../../shared" +import { createInternalAgentContinuationTextPart } from "../../shared" import { getAgentConfigKey } from "../../shared/agent-display-names" import { log } from "../../shared/logger" import { settleAfterSessionIdle } from "../shared/session-idle-settle" @@ -287,7 +287,7 @@ export async function handleAtlasSessionIdle(input: { path: { id: sessionID }, body: { agent: atlasAgent, - parts: [createInternalAgentTextPart(prompt)], + parts: [createInternalAgentContinuationTextPart(prompt)], }, query: { directory: ctx.directory }, }) diff --git a/src/hooks/ralph-loop/continuation-prompt-injector.test.ts b/src/hooks/ralph-loop/continuation-prompt-injector.test.ts index bc70c859a..bc15f4734 100644 --- a/src/hooks/ralph-loop/continuation-prompt-injector.test.ts +++ b/src/hooks/ralph-loop/continuation-prompt-injector.test.ts @@ -61,15 +61,33 @@ describe("ralph-loop continuation prompt injector", () => { test("#given inherited message agent has ZWSP prefix #when injecting continuation prompt #then promptAsync receives normalized agent", async () => { // given - let promptBody: { agent?: string } | undefined + let promptBody: { agent?: string; noReply?: boolean } | undefined + let promptPart: + | { + text: string + synthetic?: boolean + metadata?: Record + } + | undefined const ctx = { client: { session: { messages: async () => ({ data: [{ info: { agent: "\u200bSisyphus - Ultraworker" } }], }), - promptAsync: async (input: { body: { agent?: string } }) => { + promptAsync: async (input: { + body: { + agent?: string + noReply?: boolean + parts?: Array<{ + text: string + synthetic?: boolean + metadata?: Record + }> + } + }) => { promptBody = input.body + promptPart = input.body.parts?.[0] return {} }, }, @@ -87,6 +105,9 @@ describe("ralph-loop continuation prompt injector", () => { // then expect(promptBody?.agent).toBe("sisyphus") expect(promptBody?.agent).not.toContain("\u200b") + expect(promptBody?.noReply).toBeUndefined() + expect(promptPart?.synthetic).toBe(true) + expect(promptPart?.metadata?.compaction_continue).toBe(true) }) test("#given inherited message agent has no ZWSP prefix #when injecting continuation prompt #then promptAsync receives normalized agent", async () => { diff --git a/src/hooks/ralph-loop/continuation-prompt-injector.ts b/src/hooks/ralph-loop/continuation-prompt-injector.ts index 4fd66c7d1..dec80c5c6 100644 --- a/src/hooks/ralph-loop/continuation-prompt-injector.ts +++ b/src/hooks/ralph-loop/continuation-prompt-injector.ts @@ -4,7 +4,7 @@ import { findNearestMessageWithFields } from "../../features/hook-message-inject import { getMessageDir } from "./message-storage-directory" import { withTimeout } from "./with-timeout" import { - createInternalAgentTextPart, + createInternalAgentContinuationTextPart, isRecord, normalizeSDKResponse, resolveInheritedPromptTools, @@ -126,7 +126,7 @@ export async function injectContinuationPrompt( ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}), - parts: [createInternalAgentTextPart(options.prompt)], + parts: [createInternalAgentContinuationTextPart(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 1c2c40c08..e7ef870d1 100644 --- a/src/hooks/session-recovery/resume.test.ts +++ b/src/hooks/session-recovery/resume.test.ts @@ -74,7 +74,14 @@ describe("session-recovery resume", () => { expect(promptBody?.variant).toBe("max") expect(promptBody?.tools).toEqual({ question: false, bash: true }) expect(Array.isArray(promptBody?.parts)).toBe(true) - const firstPart = (promptBody?.parts as Array<{ text?: string }>)?.[0] + const firstPart = (promptBody?.parts as Array<{ + text?: string + synthetic?: boolean + metadata?: Record + }>)?.[0] expect(firstPart?.text).toContain(OMO_INTERNAL_INITIATOR_MARKER) + expect(firstPart?.synthetic).toBe(true) + expect(firstPart?.metadata?.compaction_continue).toBe(true) + expect(promptBody?.noReply).toBeUndefined() }) }) diff --git a/src/hooks/session-recovery/resume.ts b/src/hooks/session-recovery/resume.ts index 6c42b6315..136ea16b4 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 { createInternalAgentTextPart, resolveInheritedPromptTools } from "../../shared" +import { createInternalAgentContinuationTextPart, resolveInheritedPromptTools } from "../../shared" const RECOVERY_RESUME_TEXT = "[session recovered - continuing previous task]" @@ -35,7 +35,7 @@ export async function resumeSession(client: Client, config: ResumeConfig): Promi await client.session.promptAsync({ path: { id: config.sessionID }, body: { - parts: [createInternalAgentTextPart(RECOVERY_RESUME_TEXT)], + parts: [createInternalAgentContinuationTextPart(RECOVERY_RESUME_TEXT)], agent: config.agent, ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts b/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts index 56dd7cb4e..8ab033ed7 100644 --- a/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts +++ b/src/hooks/todo-continuation-enforcer/continuation-injection.test.ts @@ -46,7 +46,14 @@ describe("injectContinuation", () => { test("inherits tools from resolved message info when reinjecting", async () => { // given let capturedTools: Record | undefined - let capturedText: string | undefined + let capturedPart: + | { + text: string + synthetic?: boolean + metadata?: Record + } + | undefined + let capturedNoReply: boolean | undefined const ctx = { directory: "/tmp/test", client: { @@ -55,11 +62,18 @@ describe("injectContinuation", () => { promptAsync: async (input: { body: { tools?: Record - parts?: Array<{ type: string; text: string }> + noReply?: boolean + parts?: Array<{ + type: string + text: string + synthetic?: boolean + metadata?: Record + }> } }) => { capturedTools = input.body.tools - capturedText = input.body.parts?.[0]?.text + capturedNoReply = input.body.noReply + capturedPart = input.body.parts?.[0] return {} }, }, @@ -83,7 +97,10 @@ describe("injectContinuation", () => { // then expect(capturedTools).toEqual({ question: false, bash: true }) - expect(capturedText).toContain(OMO_INTERNAL_INITIATOR_MARKER) + expect(capturedNoReply).toBeUndefined() + expect(capturedPart?.text).toContain(OMO_INTERNAL_INITIATOR_MARKER) + expect(capturedPart?.synthetic).toBe(true) + expect(capturedPart?.metadata?.compaction_continue).toBe(true) }) test("skips injection when agent is plan (prevents Plan Mode infinite loop)", async () => { diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection.ts b/src/hooks/todo-continuation-enforcer/continuation-injection.ts index d1ca73a1f..47bfaead6 100644 --- a/src/hooks/todo-continuation-enforcer/continuation-injection.ts +++ b/src/hooks/todo-continuation-enforcer/continuation-injection.ts @@ -6,7 +6,7 @@ import { resolveRegisteredAgentName, } from "../../features/claude-code-session-state" import { - createInternalAgentTextPart, + createInternalAgentContinuationTextPart, normalizeSDKResponse, resolveInheritedPromptTools, } from "../../shared" @@ -191,7 +191,7 @@ ${todoList}` ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), ...(inheritedTools ? { tools: inheritedTools } : {}), - parts: [createInternalAgentTextPart(prompt)], + parts: [createInternalAgentContinuationTextPart(prompt)], }, query: { directory: ctx.directory }, }) diff --git a/src/plugin/event.model-fallback-pin-agent.test.ts b/src/plugin/event.model-fallback-pin-agent.test.ts index b9adbdfee..4b21106cf 100644 --- a/src/plugin/event.model-fallback-pin-agent.test.ts +++ b/src/plugin/event.model-fallback-pin-agent.test.ts @@ -18,14 +18,26 @@ function setupConnectedProviderCacheMocks(): void { type PromptBody = { path: { id: string } body: { - parts: Array<{ type: "text"; text: string }> + parts: Array<{ + type: "text" + text: string + synthetic?: boolean + metadata?: Record + }> agent?: string model?: { providerID: string; modelID: string } variant?: string + noReply?: boolean } query: { directory: string } } +function expectSyntheticContinuation(body: PromptBody["body"]): void { + expect(body.noReply).toBeUndefined() + expect(body.parts[0]?.synthetic).toBe(true) + expect(body.parts[0]?.metadata?.compaction_continue).toBe(true) +} + describe("createEventHandler - model-fallback auto-continuation pins agent/model/variant", () => { const createHandler = (args?: { hooks?: any @@ -127,6 +139,7 @@ describe("createEventHandler - model-fallback auto-continuation pins agent/model providerID: "anthropic", modelID: "claude-opus-4-7", }) + expectSyntheticContinuation(body) }) test("pins agent/model on promptAsync body when continuing after session.error fallback", async () => { @@ -167,6 +180,7 @@ describe("createEventHandler - model-fallback auto-continuation pins agent/model providerID: "anthropic", modelID: "claude-opus-4-7", }) + expectSyntheticContinuation(body) }) test("pins agent/model on fallback prompt() body when promptAsync is not available (session.status)", async () => { @@ -223,6 +237,7 @@ describe("createEventHandler - model-fallback auto-continuation pins agent/model providerID: "anthropic", modelID: "claude-opus-4-7", }) + expectSyntheticContinuation(body) }) test("pins variant from agent config when present", async () => { @@ -268,5 +283,6 @@ describe("createEventHandler - model-fallback auto-continuation pins agent/model expect(promptAsyncBodies.length).toBe(1) const body = promptAsyncBodies[0]!.body expect(body.variant).toBe("thinking") + expectSyntheticContinuation(body) }) }) diff --git a/src/plugin/event.test.ts b/src/plugin/event.test.ts index a89f59ce9..e089d9ddd 100644 --- a/src/plugin/event.test.ts +++ b/src/plugin/event.test.ts @@ -1470,6 +1470,15 @@ describe("createEventHandler - session recovery compaction", () => { const sessionID = "ses_recovery_compaction" setMainSession(sessionID) const callOrder: string[] = [] + const promptBodies: Array<{ + body?: { + noReply?: boolean + parts?: Array<{ + synthetic?: boolean + metadata?: Record + }> + } + }> = [] const eventHandler = createEventHandler({ ctx: asEventHandlerContext({ @@ -1481,8 +1490,9 @@ describe("createEventHandler - session recovery compaction", () => { callOrder.push("summarize") return {} }, - prompt: async () => { + prompt: async (input: { body?: { noReply?: boolean; parts?: Array<{ synthetic?: boolean; metadata?: Record }> } }) => { callOrder.push("prompt") + promptBodies.push(input) return {} }, }, @@ -1513,12 +1523,24 @@ describe("createEventHandler - session recovery compaction", () => { }, })) expect(callOrder).toEqual(["summarize", "prompt"]) + expect(promptBodies[0]?.body?.noReply).toBeUndefined() + expect(promptBodies[0]?.body?.parts?.[0]?.synthetic).toBe(true) + expect(promptBodies[0]?.body?.parts?.[0]?.metadata?.compaction_continue).toBe(true) }) it("sends continue even if compaction fails", async () => { const sessionID = "ses_recovery_compaction_fail" setMainSession(sessionID) const callOrder: string[] = [] + const promptBodies: Array<{ + body?: { + noReply?: boolean + parts?: Array<{ + synthetic?: boolean + metadata?: Record + }> + } + }> = [] const eventHandler = createEventHandler({ ctx: asEventHandlerContext({ @@ -1530,8 +1552,9 @@ describe("createEventHandler - session recovery compaction", () => { callOrder.push("summarize") throw new Error("compaction failed") }, - prompt: async () => { + prompt: async (input: { body?: { noReply?: boolean; parts?: Array<{ synthetic?: boolean; metadata?: Record }> } }) => { callOrder.push("prompt") + promptBodies.push(input) return {} }, }, @@ -1562,6 +1585,9 @@ describe("createEventHandler - session recovery compaction", () => { }, })) expect(callOrder).toEqual(["summarize", "prompt"]) + expect(promptBodies[0]?.body?.noReply).toBeUndefined() + expect(promptBodies[0]?.body?.parts?.[0]?.synthetic).toBe(true) + expect(promptBodies[0]?.body?.parts?.[0]?.metadata?.compaction_continue).toBe(true) }) it("continues dispatching later event hooks when an earlier hook throws", async () => { diff --git a/src/plugin/event.ts b/src/plugin/event.ts index ade05ea93..474a81ff0 100644 --- a/src/plugin/event.ts +++ b/src/plugin/event.ts @@ -25,7 +25,7 @@ import { clearBackgroundOutputConsumptionsForTaskSession, restoreBackgroundOutputConsumption, } from "../shared/background-output-consumption"; -import { resetMessageCursor } from "../shared"; +import { createInternalAgentContinuationTextPart, resetMessageCursor } from "../shared"; import { getAgentConfigKey } from "../shared/agent-display-names"; import { readConnectedProvidersCache } from "../shared/connected-providers-cache"; import { invalidateContextWindowUsageCache } from "../shared/dynamic-truncator"; @@ -162,7 +162,12 @@ export function createEventHandler(args: { promptAsync?: (input: { path: { id: string }; body: { - parts: Array<{ type: "text"; text: string }>; + parts: Array<{ + type: "text"; + text: string; + synthetic?: boolean; + metadata?: Record; + }>; agent?: string; model?: { providerID: string; modelID: string }; variant?: string; @@ -172,7 +177,12 @@ export function createEventHandler(args: { prompt: (input: { path: { id: string }; body: { - parts: Array<{ type: "text"; text: string }>; + parts: Array<{ + type: "text"; + text: string; + synthetic?: boolean; + metadata?: Record; + }>; agent?: string; model?: { providerID: string; modelID: string }; variant?: string; @@ -383,7 +393,7 @@ export function createEventHandler(args: { ...(launchAgent ? { agent: launchAgent } : {}), ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), - parts: [{ type: "text" as const, text: "continue" }], + parts: [createInternalAgentContinuationTextPart("continue")], }, query: { directory: pluginContext.directory }, }; @@ -772,7 +782,7 @@ export function createEventHandler(args: { await pluginContext.client.session .prompt({ path: { id: sessionID }, - body: { parts: [{ type: "text", text: "continue" }] }, + body: { parts: [createInternalAgentContinuationTextPart("continue")] }, query: { directory: pluginContext.directory }, }) .catch(() => {}); diff --git a/src/shared/internal-initiator-marker.test.ts b/src/shared/internal-initiator-marker.test.ts index cc1035dd8..8ea765c25 100644 --- a/src/shared/internal-initiator-marker.test.ts +++ b/src/shared/internal-initiator-marker.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { OMO_INTERNAL_INITIATOR_MARKER, + createInternalAgentContinuationTextPart, createInternalAgentTextPart, stripInternalInitiatorMarkers, } from "./internal-initiator-marker" @@ -19,6 +20,18 @@ describe("internal-initiator-marker", () => { expect(part.text).toBe(`Hello world\n${OMO_INTERNAL_INITIATOR_MARKER}`) }) + test("#given regular internal text #when creating a text part #then leaves it visible as a normal message part", () => { + // given + const text = "Visible notification" + + // when + const part = createInternalAgentTextPart(text) + + // then + expect("synthetic" in part).toBe(false) + expect("metadata" in part).toBe(false) + }) + test("#given text already ending with the marker #when creating a text part #then does not duplicate the marker", () => { // given const text = `Already marked\n${OMO_INTERNAL_INITIATOR_MARKER}` @@ -71,6 +84,22 @@ describe("internal-initiator-marker", () => { }) }) + describe("createInternalAgentContinuationTextPart", () => { + test("#given continuation text #when creating a text part #then marks it as an agent continuation", () => { + // given + const text = "Continue the loop" + + // when + const part = createInternalAgentContinuationTextPart(text) + + // then + expect(part.type).toBe("text") + expect(part.text).toBe(`Continue the loop\n${OMO_INTERNAL_INITIATOR_MARKER}`) + expect(part.synthetic).toBe(true) + expect(part.metadata.compaction_continue).toBe(true) + }) + }) + describe("stripInternalInitiatorMarkers", () => { test("#given text with no markers #when stripping #then returns text trimmed at the end", () => { // given diff --git a/src/shared/internal-initiator-marker.ts b/src/shared/internal-initiator-marker.ts index 7e810a15e..e37a4ef88 100644 --- a/src/shared/internal-initiator-marker.ts +++ b/src/shared/internal-initiator-marker.ts @@ -16,3 +16,16 @@ export function createInternalAgentTextPart(text: string): { text: `${cleanText}\n${OMO_INTERNAL_INITIATOR_MARKER}`, } } + +export function createInternalAgentContinuationTextPart(text: string): { + type: "text" + text: string + synthetic: true + metadata: { compaction_continue: true } +} { + return { + ...createInternalAgentTextPart(text), + synthetic: true, + metadata: { compaction_continue: true }, + } +}