Skip to content

Commit 65030f3

Browse files
clawsweeper[bot]Andrew MeyerTakhoffman
authored
fix(pi): keep message-tool delivery in session lock (#84437)
Summary: - The replacement branch adds an owned transcript write context around Pi prompt-time delivery mirror appends and a message-tool-only terminal hook, with focused tests and a changelog entry. - Reproducibility: yes. the source PR includes before/after redacted live Discord logs for a message-tool-only ... ession-lock and transcript append code. I did not rerun the live Discord scenario in this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(pi): keep message-tool delivery in session lock Validation: - ClawSweeper review passed for head f166781. - Required merge gates passed before the squash merge. Prepared head SHA: f166781 Review: #84437 (comment) Co-authored-by: Andrew Meyer <andrewmeyer@andrews-air.lan> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 7811e31 commit 65030f3

8 files changed

Lines changed: 714 additions & 42 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
1111

1212
### Fixes
1313

14+
- Agents/messages: stop message-tool-only turns after a successful source-channel `message` send while keeping transcript mirrors under the session write lock. (#84289)
1415
- Agents: filter silent heartbeat response-tool transcript artifacts out of embedded context snapshots so later user turns are not polluted by heartbeat no-op messages. (#83477) Thanks @fuller-stack-dev.
1516
- Agents/OpenAI: log repeated strict tool-schema downgrade diagnostics once per provider/model/tool signature, reducing duplicate debug noise while preserving `strict=false` fallback behavior. Fixes #82930. (#82933) Thanks @galiniliev.
1617
- Agents/code mode: spell out the `exec` tool's JavaScript/TypeScript, no Node module, and catalog-bridge constraints in model-visible schema text so agents can use enabled tools without trial-and-error. (#84269) Thanks @Kaspre.

src/agents/pi-embedded-runner/run/attempt.session-lock.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import fs from "node:fs/promises";
22
import os from "node:os";
33
import path from "node:path";
44
import { afterEach, describe, expect, it, vi } from "vitest";
5+
import {
6+
runWithOwnedSessionTranscriptWriteLock,
7+
withOwnedSessionTranscriptWrites,
8+
} from "../../../config/sessions/transcript-write-context.js";
59
import { SessionWriteLockTimeoutError } from "../../session-write-lock-error.js";
610
import {
711
createEmbeddedAttemptSessionLockController,
@@ -179,6 +183,37 @@ describe("embedded attempt session lock lifecycle", () => {
179183
expect(release).toHaveBeenCalledTimes(2);
180184
});
181185

186+
it("refreshes the prompt fence after an owned transcript mirror append", async () => {
187+
const sessionFile = await createTempSessionFile();
188+
const release = vi.fn(async () => {});
189+
const acquireSessionWriteLock = vi.fn(async () => ({ release }));
190+
const controller = await createEmbeddedAttemptSessionLockController({
191+
acquireSessionWriteLock,
192+
lockOptions: { ...lockOptions, sessionFile },
193+
});
194+
195+
await controller.releaseForPrompt();
196+
await withOwnedSessionTranscriptWrites(
197+
{
198+
sessionFile,
199+
sessionKey: "agent:main:discord:channel:123",
200+
withSessionWriteLock: (operation) => controller.withSessionWriteLock(operation),
201+
},
202+
async () =>
203+
await runWithOwnedSessionTranscriptWriteLock(
204+
{ sessionFile, sessionKey: "agent:main:discord:channel:123" },
205+
async () => {
206+
await fs.appendFile(sessionFile, '{"type":"message","id":"delivery-mirror"}\n', "utf8");
207+
},
208+
),
209+
);
210+
await expect(controller.withSessionWriteLock(() => "finalize")).resolves.toBe("finalize");
211+
212+
expect(controller.hasSessionTakeover()).toBe(false);
213+
expect(acquireSessionWriteLock).toHaveBeenCalledTimes(3);
214+
expect(release).toHaveBeenCalledTimes(3);
215+
});
216+
182217
it("returns a no-op cleanup lock after prompt lock reacquisition times out", async () => {
183218
const releases: string[] = [];
184219
const acquireSessionWriteLock = vi

src/agents/pi-embedded-runner/run/attempt.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
runQuotaSuspensionMaintenance,
1616
updateSessionStoreEntry,
1717
} from "../../../config/sessions/store.js";
18+
import { withOwnedSessionTranscriptWrites } from "../../../config/sessions/transcript-write-context.js";
1819
import { resolveContextEngineOwnerPluginId } from "../../../context-engine/registry.js";
1920
import type { AssembleResult } from "../../../context-engine/types.js";
2021
import { emitTrustedDiagnosticEvent } from "../../../infra/diagnostic-events.js";
@@ -380,6 +381,7 @@ import {
380381
} from "./incomplete-turn.js";
381382
import { resolveLlmIdleTimeoutMs, streamWithIdleTimeout } from "./llm-idle-timeout.js";
382383
import { resolveMessageMergeStrategy } from "./message-merge-strategy.js";
384+
import { installMessageToolOnlyTerminalHook } from "./message-tool-terminal.js";
383385
import {
384386
MID_TURN_PRECHECK_ERROR_MESSAGE,
385387
isMidTurnPrecheckSignal,
@@ -2386,6 +2388,10 @@ export async function runEmbeddedAttempt(
23862388
session: activeSession,
23872389
withSessionWriteLock: (operation) => sessionLockController.withSessionWriteLock(operation),
23882390
});
2391+
installMessageToolOnlyTerminalHook({
2392+
agent: activeSession.agent,
2393+
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
2394+
});
23892395
prepStages.mark("agent-session");
23902396
if (isRawModelRun) {
23912397
// Raw model probes should measure exactly the requested prompt against
@@ -3171,7 +3177,15 @@ export async function runEmbeddedAttempt(
31713177
prompt: string,
31723178
options?: Parameters<typeof activeSession.prompt>[1],
31733179
): Promise<void> =>
3174-
abortable(trackPromptSettlePromise(activeSession.prompt(prompt, options)));
3180+
withOwnedSessionTranscriptWrites(
3181+
{
3182+
sessionFile: params.sessionFile,
3183+
sessionKey: params.sessionKey,
3184+
withSessionWriteLock: (operation) =>
3185+
sessionLockController.withSessionWriteLock(operation),
3186+
},
3187+
async () => abortable(trackPromptSettlePromise(activeSession.prompt(prompt, options))),
3188+
);
31753189

31763190
const subscription = subscribeEmbeddedPiSession(
31773191
buildEmbeddedSubscriptionParams({

0 commit comments

Comments
 (0)