diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index c5d64f7c655..7bca081c10b 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -8255,7 +8255,7 @@ describe("runCodexAppServerAttempt", () => { expect(llmInput).toHaveBeenCalledTimes(1); expect(llmOutput).toHaveBeenCalledTimes(1); expect(agentEnd).toHaveBeenCalledTimes(1); - const [llmOutputPayload] = mockCall(llmOutput, "llm_output") as [ + const [llmOutputPayload] = llmOutput.mock.calls[0] as [ { assistantTexts?: string[]; harnessId?: string; @@ -8273,8 +8273,8 @@ describe("runCodexAppServerAttempt", () => { expect(llmOutputPayload.resolvedRef).toBe("codex/gpt-5.4-codex"); expect(llmOutputPayload.harnessId).toBe("codex"); expect(llmOutputPayload.runId).toBe("run-1"); - expect(llmOutputPayload.sessionId).toBe("session-1"); - const [agentEndPayload] = mockCall(agentEnd, "agent_end") as [ + expect(llmOutputPayload.sessionId).toBe(sessionId); + const [agentEndPayload] = agentEnd.mock.calls[0] as [ { error?: string; messages?: Array<{ role?: string }>; success?: boolean }, unknown, ]; diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 3d9bf638494..3c28575b7b0 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -74,7 +74,7 @@ import { import { resolveSlackThreadTargets } from "../../threading.js"; import type { SlackMessageEvent } from "../../types.js"; import { normalizeSlackAllowOwnerEntry } from "../allow-list.js"; -import { resolveStorePath, updateLastRoute } from "../config.runtime.js"; +import { updateLastRoute } from "../config.runtime.js"; import { recordInboundSession } from "../conversation.runtime.js"; import { escapeSlackMrkdwn } from "../mrkdwn.js"; import { @@ -359,10 +359,6 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag : undefined; if (prepared.isDirectMessage) { - const sessionCfg = cfg.session; - const storePath = resolveStorePath(sessionCfg?.store, { - agentId: route.agentId, - }); const pinnedMainDmOwner = resolvePinnedMainDmOwnerFromAllowlist({ dmScope: cfg.session?.dmScope, allowFrom: ctx.allowFrom, @@ -384,7 +380,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag ); } else { await updateLastRoute({ - storePath, + agentId: route.agentId, sessionKey: inboundLastRouteSessionKey, deliveryContext: { channel: "slack", diff --git a/src/agents/embedded-agent-runner/run.ts b/src/agents/embedded-agent-runner/run.ts index 3f0715e0a9d..9f536dc212e 100644 --- a/src/agents/embedded-agent-runner/run.ts +++ b/src/agents/embedded-agent-runner/run.ts @@ -1469,7 +1469,6 @@ export async function runEmbeddedAgent( durationMs: Date.now() - started, agentMeta: buildErrorAgentMeta({ sessionId: activeSessionId, - sessionFile: activeSessionFile, provider, model: model.id, contextTokens: ctxInfo.tokens, @@ -1795,7 +1794,6 @@ export async function runEmbeddedAgent( durationMs: Date.now() - started, agentMeta: buildErrorAgentMeta({ sessionId: activeSessionId, - sessionFile: activeSessionFile, provider, model: model.id, contextTokens: ctxInfo.tokens, @@ -2324,7 +2322,6 @@ export async function runEmbeddedAgent( durationMs: Date.now() - started, agentMeta: buildErrorAgentMeta({ sessionId: sessionIdUsed, - sessionFile: activeSessionFile, provider, model: model.id, contextTokens: ctxInfo.tokens, @@ -2357,7 +2354,6 @@ export async function runEmbeddedAgent( durationMs: Date.now() - started, agentMeta: buildErrorAgentMeta({ sessionId: sessionIdUsed, - sessionFile: activeSessionFile, provider, model: model.id, contextTokens: ctxInfo.tokens, @@ -2448,7 +2444,6 @@ export async function runEmbeddedAgent( durationMs: Date.now() - started, agentMeta: buildErrorAgentMeta({ sessionId: sessionIdUsed, - sessionFile: activeSessionFile, provider, model: model.id, contextTokens: ctxInfo.tokens, @@ -2489,7 +2484,6 @@ export async function runEmbeddedAgent( durationMs: Date.now() - started, agentMeta: buildErrorAgentMeta({ sessionId: sessionIdUsed, - sessionFile: activeSessionFile, provider, model: model.id, contextTokens: ctxInfo.tokens, diff --git a/src/agents/subagent-announce-delivery.ts b/src/agents/subagent-announce-delivery.ts index 2ea6dac62fb..fd2c95ab37c 100644 --- a/src/agents/subagent-announce-delivery.ts +++ b/src/agents/subagent-announce-delivery.ts @@ -114,6 +114,21 @@ async function resolveQueueEmbeddedAgentMessageOutcome( ); } +async function runAnnounceAgentCall(params: { + agentParams: Record; + expectFinal?: boolean; + timeoutMs?: number; +}): Promise { + return await subagentAnnounceDeliveryDeps.dispatchGatewayMethodInProcess( + "agent", + params.agentParams, + { + expectFinal: params.expectFinal, + timeoutMs: params.timeoutMs, + }, + ); +} + function formatQueueWakeFailureError( fallback: string, outcome: EmbeddedAgentQueueMessageOutcome, diff --git a/src/auto-reply/reply/completion-delivery-policy.ts b/src/auto-reply/reply/completion-delivery-policy.ts index 40d0b9199cf..5498eb8abd0 100644 --- a/src/auto-reply/reply/completion-delivery-policy.ts +++ b/src/auto-reply/reply/completion-delivery-policy.ts @@ -74,6 +74,9 @@ function inferCompletionChatTypeFromTarget(to: string | undefined): CompletionCh if (normalized.startsWith("group:")) { return "group"; } + if (normalized.endsWith("@g.us")) { + return "group"; + } if (normalized.startsWith("channel:") || normalized.startsWith("thread:")) { return "channel"; } diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index eab6faf226d..886e7557fce 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -2794,10 +2794,6 @@ async function updateCommandInternal(opts: UpdateCommandOptions): Promise if (timeoutMs === null) { return; } - if (opts.dryRun !== true) { - await disableCurrentOpenClawUpdateLaunchdJob().catch(() => undefined); - assertConfigWriteAllowedInCurrentMode(); - } const updateStepTimeoutMs = timeoutMs ?? DEFAULT_UPDATE_STEP_TIMEOUT_MS; let root = await resolveUpdateRoot(); @@ -2910,6 +2906,10 @@ async function updateCommandInternal(opts: UpdateCommandOptions): Promise defaultRuntime.exit(1); return; } + if (opts.dryRun !== true) { + await disableCurrentOpenClawUpdateLaunchdJob().catch(() => undefined); + assertConfigWriteAllowedInCurrentMode(); + } const installKind = updateStatus.installKind; const switchToGit = requestedChannel === "dev" && installKind !== "git"; diff --git a/src/commands/doctor/legacy/cron.test.ts b/src/commands/doctor/legacy/cron.test.ts index c019e65cd4c..5ee4e02876c 100644 --- a/src/commands/doctor/legacy/cron.test.ts +++ b/src/commands/doctor/legacy/cron.test.ts @@ -105,6 +105,13 @@ function requirePersistedJob(jobs: Array>, index: number return job; } +function requireRecord(value: unknown, label: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`expected ${label}`); + } + return value as Record; +} + function expectNoteContaining(message: string, title: string): void { expect( noteMock.mock.calls.some( @@ -174,7 +181,7 @@ describe("maybeRepairLegacyCronStore", () => { state: {}, }, ]); - const prompter = makePrompter(true); + const prompter = makePrompter(false); await maybeRepairLegacyCronStore({ cfg: { @@ -189,7 +196,7 @@ describe("maybeRepairLegacyCronStore", () => { prompter, }); - expect(prompter.confirm).not.toHaveBeenCalled(); + expect(prompter.confirm).toHaveBeenCalledOnce(); expectNoteContaining("Cron model overrides detected", "Cron"); expectNoteContaining("2 jobs set `payload.model`", "Cron"); expectNoteContaining("Provider namespaces: anthropic=1, openai=1", "Cron"); diff --git a/src/commands/doctor/legacy/cron.ts b/src/commands/doctor/legacy/cron.ts index 8ec6362c085..b238c273073 100644 --- a/src/commands/doctor/legacy/cron.ts +++ b/src/commands/doctor/legacy/cron.ts @@ -357,7 +357,7 @@ export async function maybeRepairLegacyCronStore(params: { if (rawJobs.length === 0 && !hasLegacyStoreFile && !hasLegacyStateSidecar && !hasLegacyRunLogs) { return; } - noteCronModelOverrides({ cfg: params.cfg, jobs: rawJobs, storePath }); + noteCronModelOverrides({ cfg: params.cfg, jobs: rawJobs, storePath: legacyStorePath }); const normalized = normalizeStoredCronJobs(rawJobs); const legacyWebhook = normalizeOptionalString(params.cfg.cron?.webhook); diff --git a/src/cron/isolated-agent.session-identity.test.ts b/src/cron/isolated-agent.session-identity.test.ts index d9433449114..9316c2c449b 100644 --- a/src/cron/isolated-agent.session-identity.test.ts +++ b/src/cron/isolated-agent.session-identity.test.ts @@ -9,8 +9,6 @@ import { makeJob, seedCronSessionRows, seedMainRouteSession, - writeSessionStore, - writeSessionStoreEntries, } from "./isolated-agent.test-harness.js"; import { DEFAULT_AGENT_TURN_PAYLOAD, @@ -25,8 +23,7 @@ import { setupRunCronIsolatedAgentTurnSuite } from "./isolated-agent/run.suite-h import { dispatchCronDeliveryMock, mockRunCronFallbackPassthrough, - runEmbeddedAgentMock, - updateSessionStoreMock, + runEmbeddedPiAgentMock, } from "./isolated-agent/run.test-harness.js"; import { normalizeCronJobCreate } from "./normalize.js"; import type { CronJob } from "./types.js"; @@ -158,12 +155,9 @@ describe("runCronIsolatedAgentTurn session identity", () => { await withTempHome(async (home) => { const deps = makeDeps(); const boundSessionKey = "agent:main:telegram:direct:42"; - const originalSessionFile = path.join(home, "bound-session.jsonl"); - const rotatedSessionFile = path.join(home, "bound-session-rotated.jsonl"); - const storePath = await writeSessionStoreEntries(home, { + await seedCronSessionRows(home, { [boundSessionKey]: { sessionId: "bound-session", - sessionFile: originalSessionFile, updatedAt: Date.now(), lastInteractionAt: Date.now() - 1_000, systemSent: true, @@ -175,7 +169,6 @@ describe("runCronIsolatedAgentTurn session identity", () => { durationMs: 5, agentMeta: { sessionId: "bound-session-rotated", - sessionFile: rotatedSessionFile, provider: "anthropic", model: "claude-opus-4-6", compactionCount: 1, @@ -183,12 +176,6 @@ describe("runCronIsolatedAgentTurn session identity", () => { }, }, }); - updateSessionStoreMock.mockImplementation(async (targetStorePath, update) => { - const raw = await fs.readFile(targetStorePath, "utf-8"); - const store = JSON.parse(raw) as Record; - update(store); - await fs.writeFile(targetStorePath, JSON.stringify(store, null, 2), "utf-8"); - }); const currentBoundJob = normalizeCronJobCreate( { ...makeJob(DEFAULT_AGENT_TURN_PAYLOAD), @@ -199,7 +186,7 @@ describe("runCronIsolatedAgentTurn session identity", () => { ) as CronJob; const res = await runCronIsolatedAgentTurn({ - cfg: makeCfg(home, storePath), + cfg: makeCfg(home), deps, job: currentBoundJob, message: DEFAULT_MESSAGE, @@ -213,14 +200,10 @@ describe("runCronIsolatedAgentTurn session identity", () => { expect.objectContaining({ sessionId: "bound-session-rotated" }), ); - const finalPersist = updateSessionStoreMock.mock.calls.at(-1); - expect(finalPersist?.[0]).toBe(storePath); - const persistedStore: Record = {}; - (finalPersist?.[1] as (store: typeof persistedStore) => void)(persistedStore); - expect(persistedStore[boundSessionKey]).toEqual( + const persisted = await readSessionEntry("main", boundSessionKey); + expect(persisted).toEqual( expect.objectContaining({ sessionId: "bound-session-rotated", - sessionFile: rotatedSessionFile, usageFamilyKey: boundSessionKey, usageFamilySessionIds: ["bound-session", "bound-session-rotated"], }), diff --git a/src/cron/isolated-agent/run-session-state.test.ts b/src/cron/isolated-agent/run-session-state.test.ts index 01310f28eec..9e0740eedec 100644 --- a/src/cron/isolated-agent/run-session-state.test.ts +++ b/src/cron/isolated-agent/run-session-state.test.ts @@ -171,7 +171,6 @@ describe("createPersistCronSessionEntry", () => { const cronSession = makeCronSession( makeSessionEntry({ sessionId: "bound-session", - sessionFile: "/tmp/bound-session.jsonl", }), ); const changed = adoptCronRunSessionMetadata({ @@ -179,37 +178,31 @@ describe("createPersistCronSessionEntry", () => { sessionKey: "agent:main:telegram:direct:42", runMeta: { sessionId: "bound-session-rotated", - sessionFile: "/tmp/bound-session-rotated.jsonl", }, }); - const updateSessionStore = vi.fn( - async (_storePath, update: (store: Record) => void) => { - const store: Record = {}; - update(store); - expect(store["agent:main:telegram:direct:42"]).toEqual({ - sessionId: "bound-session-rotated", - sessionFile: "/tmp/bound-session-rotated.jsonl", - usageFamilyKey: "agent:main:telegram:direct:42", - usageFamilySessionIds: ["bound-session", "bound-session-rotated"], - updatedAt: 1000, - systemSent: true, - }); - }, - ); + const persistSessionRow = vi.fn(async (sessionKey: string, entry: SessionEntry) => { + expect(sessionKey).toBe("agent:main:telegram:direct:42"); + expect(entry).toEqual({ + sessionId: "bound-session-rotated", + usageFamilyKey: "agent:main:telegram:direct:42", + usageFamilySessionIds: ["bound-session", "bound-session-rotated"], + updatedAt: 1000, + systemSent: true, + }); + }); expect(changed).toBe(true); const persist = createPersistCronSessionEntry({ isFastTestEnv: false, cronSession, agentSessionKey: "agent:main:telegram:direct:42", - updateSessionStore, + persistSessionRow, }); await persist(); expect(cronSession.store["agent:main:telegram:direct:42"]).toEqual({ sessionId: "bound-session-rotated", - sessionFile: "/tmp/bound-session-rotated.jsonl", usageFamilyKey: "agent:main:telegram:direct:42", usageFamilySessionIds: ["bound-session", "bound-session-rotated"], updatedAt: 1000, diff --git a/src/cron/isolated-agent/run-session-state.ts b/src/cron/isolated-agent/run-session-state.ts index c9eb5cc9475..b2d04c265a7 100644 --- a/src/cron/isolated-agent/run-session-state.ts +++ b/src/cron/isolated-agent/run-session-state.ts @@ -73,18 +73,16 @@ export function adoptCronRunSessionMetadata(params: { sessionKey: string; runMeta?: { sessionId?: string; - sessionFile?: string; }; }): boolean { const nextSessionId = normalizeSessionField(params.runMeta?.sessionId); - const nextSessionFile = normalizeSessionField(params.runMeta?.sessionFile); - if (!nextSessionFile) { + if (!nextSessionId) { return false; } let changed = false; const previousSessionId = params.entry.sessionId; - if (nextSessionId && nextSessionId !== previousSessionId) { + if (nextSessionId !== previousSessionId) { params.entry.sessionId = nextSessionId; params.entry.usageFamilyKey = params.entry.usageFamilyKey ?? params.sessionKey; params.entry.usageFamilySessionIds = Array.from( @@ -97,11 +95,6 @@ export function adoptCronRunSessionMetadata(params: { changed = true; } - if (nextSessionFile !== params.entry.sessionFile) { - params.entry.sessionFile = nextSessionFile; - changed = true; - } - return changed; }