From 0f8fc548f1907f8d68cc1bbc08a7f10e8b9f9c2e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 18 May 2026 13:12:49 +0900 Subject: [PATCH 1/4] fix(runtime-fallback): consume delegated bootstrap retry payload Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/runtime-fallback/index.test.ts | 1 + src/hooks/runtime-fallback/last-user-retry-parts.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/runtime-fallback/index.test.ts b/src/hooks/runtime-fallback/index.test.ts index 2f1949781..8e338bdbb 100644 --- a/src/hooks/runtime-fallback/index.test.ts +++ b/src/hooks/runtime-fallback/index.test.ts @@ -551,6 +551,7 @@ describe("runtime-fallback", () => { expect(promptBody?.tools?.question).toBe(false) expect(promptBody?.tools?.call_omo_agent).toBe(true) expect(promptBody?.parts?.[0]?.text).toContain("inspect src/tools/delegate-task") + expect(getDelegatedChildSessionBootstrap(sessionID)).toBeUndefined() }) test("should use persisted user prompt while preserving delegated bootstrap launch context", async () => { diff --git a/src/hooks/runtime-fallback/last-user-retry-parts.ts b/src/hooks/runtime-fallback/last-user-retry-parts.ts index 38a73eeec..c0cbf5657 100644 --- a/src/hooks/runtime-fallback/last-user-retry-parts.ts +++ b/src/hooks/runtime-fallback/last-user-retry-parts.ts @@ -54,8 +54,13 @@ export function getLastUserRetryPayload( return { retryParts } } + const bootstrapRetryParts = bootstrap?.retryParts ?? [] + if (bootstrapRetryParts.length > 0) { + clearDelegatedChildSessionBootstrap(sessionID) + } + return { - retryParts: bootstrap?.retryParts ?? [], + retryParts: bootstrapRetryParts, ...(bootstrap?.system ? { system: bootstrap.system } : {}), ...(bootstrap?.tools ? { tools: bootstrap.tools } : {}), } From f17623d450f1a8291c853ffed7791be0e818b482 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 18 May 2026 13:13:24 +0900 Subject: [PATCH 2/4] test(shared): isolate logger module in full suite Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/shared/logger.test.ts | 65 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/src/shared/logger.test.ts b/src/shared/logger.test.ts index 40057bf0e..ff85e493b 100644 --- a/src/shared/logger.test.ts +++ b/src/shared/logger.test.ts @@ -12,13 +12,7 @@ import * as fs from "fs" import * as os from "os" import * as path from "path" -import { - _flushForTesting, - _resetLoggerForTesting, - _setLoggerForTesting, - getLogFilePath, - log, -} from "./logger" +type LoggerModule = typeof import("./logger") const TEST_PREFIX = "oh-my-opencode-logger-test" @@ -29,23 +23,26 @@ function makeTempDir(): string { describe("#given the shared logger", () => { let tempDir: string let logFilePath: string + let loggerModule: LoggerModule - beforeEach(() => { + beforeEach(async () => { + mock.restore() tempDir = makeTempDir() logFilePath = path.join(tempDir, "log.txt") + loggerModule = await import(`./logger?test=${Date.now()}-${Math.random()}`) }) afterEach(() => { - _resetLoggerForTesting() + loggerModule._resetLoggerForTesting() fs.rmSync(tempDir, { recursive: true, force: true }) }) describe("#given log file size under threshold", () => { test("#when log() is called and flushed #then the file is not rotated", () => { - _setLoggerForTesting({ filePath: logFilePath, maxSizeBytes: 1024, maxBackups: 2 }) + loggerModule._setLoggerForTesting({ filePath: logFilePath, maxSizeBytes: 1024, maxBackups: 2 }) - log("small entry") - _flushForTesting() + loggerModule.log("small entry") + loggerModule._flushForTesting() expect(fs.existsSync(logFilePath)).toBe(true) expect(fs.existsSync(`${logFilePath}.1`)).toBe(false) @@ -54,13 +51,13 @@ describe("#given the shared logger", () => { describe("#given log file size over threshold", () => { test("#when next flush runs #then the file rotates to .1 and a fresh file is created", () => { - _setLoggerForTesting({ filePath: logFilePath, maxSizeBytes: 100, maxBackups: 2 }) + loggerModule._setLoggerForTesting({ filePath: logFilePath, maxSizeBytes: 100, maxBackups: 2 }) // Pre-fill the log file beyond the threshold so the next flush triggers rotation. fs.writeFileSync(logFilePath, "x".repeat(200)) - log("after rotation") - _flushForTesting() + loggerModule.log("after rotation") + loggerModule._flushForTesting() // flush() appends first, then rotates — the in-flight batch becomes part // of .1 so the post-flush primary is bounded to ≤ cap. The primary path @@ -75,26 +72,26 @@ describe("#given the shared logger", () => { expect(fs.existsSync(logFilePath)).toBe(false) // A subsequent log() recreates the primary on its flush. - log("after recreation") - _flushForTesting() + loggerModule.log("after recreation") + loggerModule._flushForTesting() expect(fs.existsSync(logFilePath)).toBe(true) expect(fs.readFileSync(logFilePath, "utf8")).toContain("after recreation") }) test("#when rotation happens repeatedly #then only maxBackups files are kept and the ladder shifts in order", () => { - _setLoggerForTesting({ filePath: logFilePath, maxSizeBytes: 100, maxBackups: 2 }) + loggerModule._setLoggerForTesting({ filePath: logFilePath, maxSizeBytes: 100, maxBackups: 2 }) // First rotation fs.writeFileSync(logFilePath, "first".repeat(50)) - log("entry-A") - _flushForTesting() + loggerModule.log("entry-A") + loggerModule._flushForTesting() expect(fs.existsSync(`${logFilePath}.1`)).toBe(true) expect(fs.existsSync(`${logFilePath}.2`)).toBe(false) // Second rotation fs.writeFileSync(logFilePath, "second".repeat(50)) - log("entry-B") - _flushForTesting() + loggerModule.log("entry-B") + loggerModule._flushForTesting() expect(fs.existsSync(`${logFilePath}.1`)).toBe(true) expect(fs.existsSync(`${logFilePath}.2`)).toBe(true) // The previous .1 (containing entry-A) should now live at .2 — assert the @@ -105,8 +102,8 @@ describe("#given the shared logger", () => { // Third rotation should drop the oldest (.2) and shift .1 -> .2 fs.writeFileSync(logFilePath, "third".repeat(50)) - log("entry-C") - _flushForTesting() + loggerModule.log("entry-C") + loggerModule._flushForTesting() expect(fs.existsSync(`${logFilePath}.1`)).toBe(true) expect(fs.existsSync(`${logFilePath}.2`)).toBe(true) expect(fs.existsSync(`${logFilePath}.3`)).toBe(false) @@ -125,10 +122,10 @@ describe("#given the shared logger", () => { // BUFFER_SIZE_LIMIT in logger.ts is 50 — past that, log() flushes // synchronously rather than scheduling a timer. A regression that drops // the inline flush in favor of always scheduling would only surface here. - _setLoggerForTesting({ filePath: logFilePath, maxSizeBytes: 1024 * 1024, maxBackups: 2 }) + loggerModule._setLoggerForTesting({ filePath: logFilePath, maxSizeBytes: 1024 * 1024, maxBackups: 2 }) for (let i = 0; i < 100; i += 1) { - log(`entry-${i}`) + loggerModule.log(`entry-${i}`) } // Note: no _flushForTesting() — relies on the inline flush at i=49 and i=99. @@ -141,20 +138,20 @@ describe("#given the shared logger", () => { describe("#given filesystem failures during flush", () => { test("#when the parent directory is missing #then append fails silently and does not throw", () => { - _setLoggerForTesting({ + loggerModule._setLoggerForTesting({ filePath: path.join(tempDir, "no-such-dir", "log.txt"), maxSizeBytes: 10, maxBackups: 2, }) expect(() => { - log("entry") - _flushForTesting() + loggerModule.log("entry") + loggerModule._flushForTesting() }).not.toThrow() }) test("#when rotation fails partway through #then log() does not throw and primary keeps the entry", () => { - _setLoggerForTesting({ filePath: logFilePath, maxSizeBytes: 10, maxBackups: 2 }) + loggerModule._setLoggerForTesting({ filePath: logFilePath, maxSizeBytes: 10, maxBackups: 2 }) // Pre-fill primary past the cap so rotateLogFileIfNeeded() actually triggers. fs.writeFileSync(logFilePath, "x".repeat(200)) @@ -165,8 +162,8 @@ describe("#given the shared logger", () => { fs.mkdirSync(`${logFilePath}.2`) expect(() => { - log("entry") - _flushForTesting() + loggerModule.log("entry") + loggerModule._flushForTesting() }).not.toThrow() // appendFileSync succeeded; rotation failed silently; the primary still holds @@ -178,8 +175,8 @@ describe("#given the shared logger", () => { describe("#given default configuration", () => { test("#when getLogFilePath is called #then it points at os.tmpdir()", () => { - _resetLoggerForTesting() - expect(getLogFilePath().startsWith(os.tmpdir())).toBe(true) + loggerModule._resetLoggerForTesting() + expect(loggerModule.getLogFilePath().startsWith(os.tmpdir())).toBe(true) }) }) }) From d5b1d3618c7fbe89b00d7b4d18bedf78238afbdb Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 18 May 2026 13:13:44 +0900 Subject: [PATCH 3/4] docs(changelog): add 4.2.1 blocker-4 entry Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55995339e..f2970e64d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Relanded BLOCKER-4 delegated child-session empty-history fallback. Runtime fallback now consumes the captured bootstrap prompt when a delegated child session fails before history is persisted, while preserving delegated system prompts and tool permissions for the retry. - Team Mode fresh-install diagnostics now log the resolved `team_mode` config and tool-registry team tool count, making #3893-style missing `team_*` registrations visible instead of silent. - Added a regression test proving a fresh minimal user config with `{ "team_mode": { "enabled": true } }` registers all 12 `team_*` tools. +### Documentation + +- Marked the v4.2.0 BLOCKER-4 known issue as resolved in v4.2.1. + ## [4.2.0] - 2026-05-15 ### Added @@ -43,5 +48,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Delegated child-session early-failure fallback (BLOCKER-4)**: PR #3825's `fac90d69f` was reverted by PR #4044 because its own regression test failed on clean root `bun test`. The delegate-task fallback bug for empty session history remains unaddressed in v4.2.0. Reland targets v4.2.1 once the regression test is stabilized against post-#4032 schema and the new gate semantics. See `docs/reference/known-issues.md` for details and workaround. - **First-prompt watchdog supersession history (L16)**: PR #3952 was superseded by PR #4051 (rebased over #4007/factory refactor with `internallyAbortedSessions` threading). The supersession represents conflict resolution, not a feature pivot. The final watchdog logic shipped via #4051 + `a130fa70d` covers subagent first-prompt silence past 90 seconds with cleanup via session.deleted. -[4.2.0]: https://github.com/code-yeongyu/oh-my-openagent/compare/v4.1.2...v4.2.0 [4.2.1]: https://github.com/code-yeongyu/oh-my-openagent/compare/v4.2.0...HEAD +[4.2.0]: https://github.com/code-yeongyu/oh-my-openagent/compare/v4.1.2...v4.2.0 From 971be27b7955daa63716607404a8f46334bcf3bb Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 18 May 2026 13:14:02 +0900 Subject: [PATCH 4/4] docs(known-issues): mark blocker-4 resolved Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- docs/reference/known-issues.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/known-issues.md b/docs/reference/known-issues.md index 035ae5b10..b096dca49 100644 --- a/docs/reference/known-issues.md +++ b/docs/reference/known-issues.md @@ -2,6 +2,10 @@ Tracks bugs that are present in the current release but have been intentionally deferred. Each entry should explain the symptom, the history, any workaround, and the planned resolution. +## v4.2.1 - Delegate-task early-failure-fallback (BLOCKER-4, resolved) + +BLOCKER-4 is resolved in v4.2.1. Delegated child sessions now retain the first prompt payload before dispatch and consume that bootstrap payload exactly once when runtime fallback must retry an empty-history child session. + ## v4.2.0 - Delegate-task early-failure-fallback (BLOCKER-4, deferred from PR #3825) ### Symptom