Skip to content

Commit b4e7dae

Browse files
authored
Merge branch 'main' into fix/qmd-binary-warning
2 parents f153d6a + 2849e61 commit b4e7dae

16 files changed

+571
-262
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
3232
- Memory/QMD: keep `qmd embed` active in `search` mode too, so BM25-first setups still build a complete index for later vector and hybrid retrieval. (#54509) Thanks @hnshah and @vincentkoc.
3333
- Memory/QMD: point `QMD_CONFIG_DIR` at the nested `xdg-config/qmd` directory so per-agent collection config resolves correctly. (#39078) Thanks @smart-tinker and @vincentkoc.
3434
- Memory/QMD: include deduplicated default plus per-agent `memorySearch.extraPaths` when building QMD custom collections, so shared and agent-specific extra roots both get indexed consistently. (#57315) Thanks @Vitalcheffe and @vincentkoc.
35+
- Memory/session indexer: include `.jsonl.reset.*` and `.jsonl.deleted.*` transcripts in the memory host session scan while still excluding `.jsonl.bak.*` compaction backups and lock files, so memory search sees archived session history without duplicating stale snapshots. Thanks @hclsys and @vincentkoc.
3536
- Agents/sandbox: honor `tools.sandbox.tools.alsoAllow`, let explicit sandbox re-allows remove matching built-in default-deny tools, and keep sandbox explain/error guidance aligned with the effective sandbox tool policy. (#54492) Thanks @ngutman.
3637
- LINE/ACP: add current-conversation binding and inbound binding-routing parity so `/acp spawn ... --thread here`, configured ACP bindings, and active conversation-bound ACP sessions work on LINE like the other conversation channels.
3738
- LINE/markdown: preserve underscores inside Latin, Cyrillic, and CJK words when stripping markdown, while still removing standalone `_italic_` markers on the shared text-runtime path used by LINE and TTS. (#47465) Thanks @jackjin1997.

src/acp/control-plane/manager.core.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import type { OpenClawConfig } from "../../config/config.js";
33
import { logVerbose } from "../../globals.js";
44
import { normalizeAgentId } from "../../routing/session-key.js";
55
import { isAcpSessionKey } from "../../sessions/session-key-utils.js";
6-
import { createTaskRecord, updateTaskStateByRunId } from "../../tasks/task-registry.js";
6+
import {
7+
createTaskRecord,
8+
markTaskRunningByRunId,
9+
markTaskTerminalByRunId,
10+
} from "../../tasks/task-registry.js";
711
import type { DeliveryContext } from "../../utils/delivery-context.js";
812
import {
913
AcpRuntimeError,
@@ -144,8 +148,6 @@ type BackgroundTaskContext = {
144148
task: string;
145149
};
146150

147-
type BackgroundTaskStatePatch = Omit<Parameters<typeof updateTaskStateByRunId>[0], "runId">;
148-
149151
export class AcpSessionManager {
150152
private readonly actorQueue = new SessionActorQueue();
151153
private readonly actorTailBySession = this.actorQueue.getTailMapForTesting();
@@ -786,8 +788,7 @@ export class AcpSessionManager {
786788
);
787789
}
788790
if (taskContext) {
789-
this.updateBackgroundTaskState(taskContext.runId, {
790-
status: "running",
791+
this.markBackgroundTaskRunning(taskContext.runId, {
791792
lastEventAt: Date.now(),
792793
progressSummary: taskProgressSummary || null,
793794
});
@@ -832,7 +833,7 @@ export class AcpSessionManager {
832833
});
833834
if (taskContext) {
834835
const terminalResult = resolveBackgroundTaskTerminalResult(taskProgressSummary);
835-
this.updateBackgroundTaskState(taskContext.runId, {
836+
this.markBackgroundTaskTerminal(taskContext.runId, {
836837
status: "succeeded",
837838
endedAt: Date.now(),
838839
lastEventAt: Date.now(),
@@ -871,7 +872,7 @@ export class AcpSessionManager {
871872
errorCode: acpError.code,
872873
});
873874
if (taskContext) {
874-
this.updateBackgroundTaskState(taskContext.runId, {
875+
this.markBackgroundTaskTerminal(taskContext.runId, {
875876
status: resolveBackgroundTaskFailureStatus(acpError),
876877
endedAt: Date.now(),
877878
lastEventAt: Date.now(),
@@ -1898,11 +1899,46 @@ export class AcpSessionManager {
18981899
}
18991900
}
19001901

1901-
private updateBackgroundTaskState(runId: string, patch: BackgroundTaskStatePatch): void {
1902+
private markBackgroundTaskRunning(
1903+
runId: string,
1904+
params: {
1905+
lastEventAt?: number;
1906+
progressSummary?: string | null;
1907+
},
1908+
): void {
1909+
try {
1910+
markTaskRunningByRunId({
1911+
runId,
1912+
lastEventAt: params.lastEventAt,
1913+
progressSummary: params.progressSummary,
1914+
});
1915+
} catch (error) {
1916+
logVerbose(`acp-manager: failed updating background task for ${runId}: ${String(error)}`);
1917+
}
1918+
}
1919+
1920+
private markBackgroundTaskTerminal(
1921+
runId: string,
1922+
params: {
1923+
status: "succeeded" | "failed" | "timed_out";
1924+
endedAt: number;
1925+
lastEventAt?: number;
1926+
error?: string;
1927+
progressSummary?: string | null;
1928+
terminalSummary?: string | null;
1929+
terminalOutcome?: "succeeded" | "blocked" | null;
1930+
},
1931+
): void {
19021932
try {
1903-
updateTaskStateByRunId({
1904-
...patch,
1933+
markTaskTerminalByRunId({
19051934
runId,
1935+
status: params.status,
1936+
endedAt: params.endedAt,
1937+
lastEventAt: params.lastEventAt,
1938+
error: params.error,
1939+
progressSummary: params.progressSummary,
1940+
terminalSummary: params.terminalSummary,
1941+
terminalOutcome: params.terminalOutcome,
19061942
});
19071943
} catch (error) {
19081944
logVerbose(`acp-manager: failed updating background task for ${runId}: ${String(error)}`);

src/agents/acp-spawn-parent-stream.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { onAgentEvent } from "../infra/agent-events.js";
66
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
77
import { enqueueSystemEvent } from "../infra/system-events.js";
88
import { scopedHeartbeatWakeOptions } from "../routing/session-key.js";
9-
import { updateTaskStateByRunId } from "../tasks/task-registry.js";
9+
import { recordTaskProgressByRunId } from "../tasks/task-registry.js";
1010

1111
const DEFAULT_STREAM_FLUSH_MS = 2_500;
1212
const DEFAULT_NO_OUTPUT_NOTICE_MS = 60_000;
@@ -204,7 +204,7 @@ export function startAcpSpawnParentStreamRelay(params: {
204204
wake();
205205
};
206206
const emitStartNotice = () => {
207-
updateTaskStateByRunId({
207+
recordTaskProgressByRunId({
208208
runId,
209209
lastEventAt: Date.now(),
210210
eventSummary: "Started.",
@@ -271,7 +271,7 @@ export function startAcpSpawnParentStreamRelay(params: {
271271
return;
272272
}
273273
stallNotified = true;
274-
updateTaskStateByRunId({
274+
recordTaskProgressByRunId({
275275
runId,
276276
lastEventAt: Date.now(),
277277
eventSummary: `No output for ${Math.round(noOutputNoticeMs / 1000)}s. It may be waiting for input.`,
@@ -317,7 +317,7 @@ export function startAcpSpawnParentStreamRelay(params: {
317317

318318
if (stallNotified) {
319319
stallNotified = false;
320-
updateTaskStateByRunId({
320+
recordTaskProgressByRunId({
321321
runId,
322322
lastEventAt: Date.now(),
323323
eventSummary: "Resumed output.",

src/agents/subagent-registry-lifecycle.ts

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
22
import { defaultRuntime } from "../runtime.js";
33
import { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
4-
import { updateTaskDeliveryByRunId, updateTaskStateByRunId } from "../tasks/task-registry.js";
4+
import {
5+
markTaskTerminalByRunId,
6+
setTaskRunDeliveryStatusByRunId,
7+
} from "../tasks/task-registry.js";
58
import { normalizeDeliveryContext } from "../utils/delivery-context.js";
69
import {
710
captureSubagentCompletionReply,
@@ -154,7 +157,7 @@ export function createSubagentRegistryLifecycleController(params: {
154157
entry: SubagentRunRecord;
155158
reason: "retry-limit" | "expiry";
156159
}) => {
157-
updateTaskDeliveryByRunId({
160+
setTaskRunDeliveryStatusByRunId({
158161
runId: giveUpParams.runId,
159162
deliveryStatus: "failed",
160163
});
@@ -270,7 +273,7 @@ export function createSubagentRegistryLifecycleController(params: {
270273
return;
271274
}
272275
if (didAnnounce) {
273-
updateTaskDeliveryByRunId({
276+
setTaskRunDeliveryStatusByRunId({
274277
runId,
275278
deliveryStatus: "delivered",
276279
});
@@ -326,7 +329,7 @@ export function createSubagentRegistryLifecycleController(params: {
326329
}
327330

328331
if (deferredDecision.kind === "give-up") {
329-
updateTaskDeliveryByRunId({
332+
setTaskRunDeliveryStatusByRunId({
330333
runId,
331334
deliveryStatus: "failed",
332335
});
@@ -377,26 +380,27 @@ export function createSubagentRegistryLifecycleController(params: {
377380
});
378381
};
379382

380-
void params.runSubagentAnnounceFlow({
381-
childSessionKey: entry.childSessionKey,
382-
childRunId: entry.runId,
383-
requesterSessionKey: entry.requesterSessionKey,
384-
requesterOrigin,
385-
requesterDisplayKey: entry.requesterDisplayKey,
386-
task: entry.task,
387-
timeoutMs: params.subagentAnnounceTimeoutMs,
388-
cleanup: entry.cleanup,
389-
roundOneReply: entry.frozenResultText ?? undefined,
390-
fallbackReply: entry.fallbackFrozenResultText ?? undefined,
391-
waitForCompletion: false,
392-
startedAt: entry.startedAt,
393-
endedAt: entry.endedAt,
394-
label: entry.label,
395-
outcome: entry.outcome,
396-
spawnMode: entry.spawnMode,
397-
expectsCompletionMessage: entry.expectsCompletionMessage,
398-
wakeOnDescendantSettle: entry.wakeOnDescendantSettle === true,
399-
})
383+
void params
384+
.runSubagentAnnounceFlow({
385+
childSessionKey: entry.childSessionKey,
386+
childRunId: entry.runId,
387+
requesterSessionKey: entry.requesterSessionKey,
388+
requesterOrigin,
389+
requesterDisplayKey: entry.requesterDisplayKey,
390+
task: entry.task,
391+
timeoutMs: params.subagentAnnounceTimeoutMs,
392+
cleanup: entry.cleanup,
393+
roundOneReply: entry.frozenResultText ?? undefined,
394+
fallbackReply: entry.fallbackFrozenResultText ?? undefined,
395+
waitForCompletion: false,
396+
startedAt: entry.startedAt,
397+
endedAt: entry.endedAt,
398+
label: entry.label,
399+
outcome: entry.outcome,
400+
spawnMode: entry.spawnMode,
401+
expectsCompletionMessage: entry.expectsCompletionMessage,
402+
wakeOnDescendantSettle: entry.wakeOnDescendantSettle === true,
403+
})
400404
.then((didAnnounce) => {
401405
finalizeAnnounceCleanup(didAnnounce);
402406
})
@@ -458,7 +462,7 @@ export function createSubagentRegistryLifecycleController(params: {
458462
if (mutated) {
459463
params.persist();
460464
}
461-
updateTaskStateByRunId({
465+
markTaskTerminalByRunId({
462466
runId: entry.runId,
463467
status:
464468
completeParams.outcome.status === "ok"

src/channels/plugins/target-parsing.test.ts

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,96 @@
11
import { beforeEach, describe, expect, it } from "vitest";
2-
import { setDefaultChannelPluginRegistryForTests } from "../../commands/channel-test-helpers.js";
32
import { setActivePluginRegistry } from "../../plugins/runtime.js";
43
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
54
import { parseExplicitTargetForChannel } from "./target-parsing.js";
65

6+
function parseTelegramTargetForTest(raw: string): {
7+
to: string;
8+
threadId?: number;
9+
chatType?: "direct" | "group";
10+
} {
11+
const trimmed = raw
12+
.trim()
13+
.replace(/^telegram:/i, "")
14+
.replace(/^tg:/i, "");
15+
const prefixedTopic = /^group:([^:]+):topic:(\d+)$/i.exec(trimmed);
16+
if (prefixedTopic) {
17+
return {
18+
to: prefixedTopic[1],
19+
threadId: Number.parseInt(prefixedTopic[2], 10),
20+
chatType: "group",
21+
};
22+
}
23+
const topic = /^([^:]+):topic:(\d+)$/i.exec(trimmed);
24+
if (topic) {
25+
return {
26+
to: topic[1],
27+
threadId: Number.parseInt(topic[2], 10),
28+
chatType: topic[1].startsWith("-") ? "group" : "direct",
29+
};
30+
}
31+
return {
32+
to: trimmed,
33+
chatType: trimmed.startsWith("-") ? "group" : undefined,
34+
};
35+
}
36+
37+
function setMinimalTargetParsingRegistry(): void {
38+
setActivePluginRegistry(
39+
createTestRegistry([
40+
{
41+
pluginId: "telegram",
42+
plugin: {
43+
id: "telegram",
44+
meta: {
45+
id: "telegram",
46+
label: "Telegram",
47+
selectionLabel: "Telegram",
48+
docsPath: "/channels/telegram",
49+
blurb: "test stub",
50+
},
51+
capabilities: { chatTypes: ["direct", "group"] },
52+
config: {
53+
listAccountIds: () => [],
54+
resolveAccount: () => ({}),
55+
},
56+
messaging: {
57+
parseExplicitTarget: ({ raw }: { raw: string }) => parseTelegramTargetForTest(raw),
58+
},
59+
},
60+
source: "test",
61+
},
62+
{
63+
pluginId: "demo-target",
64+
source: "test",
65+
plugin: {
66+
id: "demo-target",
67+
meta: {
68+
id: "demo-target",
69+
label: "Demo Target",
70+
selectionLabel: "Demo Target",
71+
docsPath: "/channels/demo-target",
72+
blurb: "test stub",
73+
},
74+
capabilities: { chatTypes: ["direct"] },
75+
config: {
76+
listAccountIds: () => [],
77+
resolveAccount: () => ({}),
78+
},
79+
messaging: {
80+
parseExplicitTarget: ({ raw }: { raw: string }) => ({
81+
to: raw.trim().toUpperCase(),
82+
chatType: "direct" as const,
83+
}),
84+
},
85+
},
86+
},
87+
]),
88+
);
89+
}
90+
791
describe("parseExplicitTargetForChannel", () => {
892
beforeEach(() => {
9-
setDefaultChannelPluginRegistryForTests();
93+
setMinimalTargetParsingRegistry();
1094
});
1195

1296
it("parses Telegram targets via the registered channel plugin contract", () => {
@@ -22,36 +106,6 @@ describe("parseExplicitTargetForChannel", () => {
22106
});
23107

24108
it("parses registered non-bundled channel targets via the active plugin contract", () => {
25-
setActivePluginRegistry(
26-
createTestRegistry([
27-
{
28-
pluginId: "demo-target",
29-
source: "test",
30-
plugin: {
31-
id: "demo-target",
32-
meta: {
33-
id: "demo-target",
34-
label: "Demo Target",
35-
selectionLabel: "Demo Target",
36-
docsPath: "/channels/demo-target",
37-
blurb: "test stub",
38-
},
39-
capabilities: { chatTypes: ["direct"] },
40-
config: {
41-
listAccountIds: () => [],
42-
resolveAccount: () => ({}),
43-
},
44-
messaging: {
45-
parseExplicitTarget: ({ raw }: { raw: string }) => ({
46-
to: raw.trim().toUpperCase(),
47-
chatType: "direct" as const,
48-
}),
49-
},
50-
},
51-
},
52-
]),
53-
);
54-
55109
expect(parseExplicitTargetForChannel("demo-target", "team-room")).toEqual({
56110
to: "TEAM-ROOM",
57111
chatType: "direct",

src/commands/tasks.test.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,6 @@ const taskFixture = {
7272
createdAt: Date.parse("2026-03-29T10:00:00.000Z"),
7373
lastEventAt: Date.parse("2026-03-29T10:00:10.000Z"),
7474
progressSummary: "No output for 60s. It may be waiting for input.",
75-
recentEvents: [
76-
{
77-
at: Date.parse("2026-03-29T10:00:10.000Z"),
78-
kind: "progress",
79-
summary: "No output for 60s. It may be waiting for input.",
80-
},
81-
],
8275
} as const;
8376

8477
beforeAll(async () => {
@@ -180,7 +173,6 @@ describe("tasks commands", () => {
180173
expect(runtimeLogs.join("\n")).toContain(
181174
"progressSummary: No output for 60s. It may be waiting for input.",
182175
);
183-
expect(runtimeLogs.join("\n")).toContain("recentEvent[0]: 2026-03-29T10:00:10.000Z progress");
184176
});
185177

186178
it("updates notify policy for an existing task", async () => {

0 commit comments

Comments
 (0)