Skip to content

Commit ef05da4

Browse files
author
scotthuang
committed
fix(acp): preserve thread-bound ACP delivery
1 parent 3c3efdb commit ef05da4

5 files changed

Lines changed: 95 additions & 15 deletions

File tree

packages/acp-core/src/session-interaction-mode.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,21 @@ describe("requiresInternalAcpSessionEffects", () => {
7777
true,
7878
],
7979
[
80-
"parent-owned background ACP sessions",
80+
"parent-owned one-shot background ACP sessions",
8181
{
8282
acp: { mode: "oneshot" },
8383
spawnedBy: parentKey,
8484
},
8585
true,
8686
],
87+
[
88+
"parent-owned persistent ACP sessions",
89+
{
90+
acp: { mode: "persistent" },
91+
spawnedBy: parentKey,
92+
},
93+
false,
94+
],
8795
[
8896
"interactive ACP sessions",
8997
{

packages/acp-core/src/session-interaction-mode.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@ type SessionInteractionEntry = {
88
spawnedBy?: string;
99
parentSessionKey?: string;
1010
hubDelegated?: HubDelegatedSessionMeta | null;
11-
acp?: unknown;
11+
acp?: { mode?: unknown } | null;
1212
};
1313

14-
/** Hub-delegated and parent-owned ACP child sessions must stay off user-visible chat surfaces. */
14+
/** Background ACP child runs must stay off user-visible chat surfaces. */
1515
export function requiresInternalAcpSessionEffects(entry?: SessionInteractionEntry | null): boolean {
1616
const hubDelegatedOwner = normalizeOptionalString(entry?.hubDelegated?.ownerSessionKey);
17-
const hubDelegated = Boolean(hubDelegatedOwner);
18-
return hubDelegated || isParentOwnedBackgroundAcpSession(entry);
17+
if (hubDelegatedOwner) {
18+
return true;
19+
}
20+
if (!isParentOwnedBackgroundAcpSession(entry)) {
21+
return false;
22+
}
23+
return entry?.acp?.mode !== "persistent";
1924
}
2025

2126
function resolveAcpSessionInteractionMode(

scripts/plugin-sdk-surface-report.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
7676
"approval-reply-runtime": 1,
7777
"config-runtime": 123,
7878
"config-contracts": 1,
79-
"config-types": 415,
79+
"config-types": 416,
8080
"config-schema": 3,
8181
"reply-dedupe": 1,
8282
"inbound-reply-dispatch": 33,
@@ -161,11 +161,11 @@ let publicDeprecatedExportsByEntrypointBudget;
161161
try {
162162
budgets = {
163163
publicEntrypoints: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_ENTRYPOINTS", 320),
164-
publicExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", 10301),
164+
publicExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", 10302),
165165
publicFunctionExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS", 5171),
166166
publicDeprecatedExports: readBudgetEnv(
167167
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS",
168-
3244,
168+
3245,
169169
),
170170
publicWildcardReexports: readBudgetEnv(
171171
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_WILDCARD_REEXPORTS",

scripts/protocol-gen-swift.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,14 @@ const DEFAULTED_OPTIONAL_INIT_PARAM_ENTRIES: readonly [string, readonly string[]
8787
["ModelChoice", ["available"]],
8888
];
8989

90-
const DEFAULTED_OPTIONAL_INIT_PARAMS: Record<string, Set<string>> = Object.fromEntries(
91-
DEFAULTED_OPTIONAL_INIT_PARAM_ENTRIES.map(([name, params]) => [name, new Set(params)]),
92-
);
90+
const DEFAULTED_OPTIONAL_INIT_PARAMS: Record<string, Set<string>> = {};
91+
for (const [name, params] of DEFAULTED_OPTIONAL_INIT_PARAM_ENTRIES) {
92+
const existing = DEFAULTED_OPTIONAL_INIT_PARAMS[name] ?? new Set<string>();
93+
for (const param of params) {
94+
existing.add(param);
95+
}
96+
DEFAULTED_OPTIONAL_INIT_PARAMS[name] = existing;
97+
}
9398

9499
const header = `// Generated by scripts/protocol-gen-swift.ts — do not edit by hand\n// swiftlint:disable file_length\nimport Foundation\n\npublic let GATEWAY_PROTOCOL_VERSION = ${PROTOCOL_VERSION}\npublic let GATEWAY_MIN_PROTOCOL_VERSION = ${MIN_CLIENT_PROTOCOL_VERSION}\n\nprivate struct GatewayAnyCodingKey: CodingKey, Hashable {\n let stringValue: String\n let intValue: Int?\n\n init?(stringValue: String) {\n self.stringValue = stringValue\n self.intValue = nil\n }\n\n init?(intValue: Int) {\n self.stringValue = String(intValue)\n self.intValue = intValue\n }\n}\n\npublic enum ErrorCode: String, Codable, Sendable {\n${Object.values(
95100
ErrorCodes,
@@ -235,6 +240,23 @@ function stringEnumCases(schema: JsonSchema): string[] | undefined {
235240
return cases.length === variants.length ? cases : undefined;
236241
}
237242

243+
function isDefaultedOptionalInitParam(structName: string, key: string): boolean {
244+
const defaults = DEFAULTED_OPTIONAL_INIT_PARAMS[structName];
245+
if (!defaults) {
246+
return false;
247+
}
248+
if (defaults.has(key)) {
249+
return true;
250+
}
251+
const normalizedKey = safeName(key);
252+
for (const defaultKey of defaults) {
253+
if (safeName(defaultKey) === normalizedKey) {
254+
return true;
255+
}
256+
}
257+
return false;
258+
}
259+
238260
function swiftInitializerParam(params: {
239261
structName: string;
240262
key: string;
@@ -248,8 +270,7 @@ function swiftInitializerParam(params: {
248270
return `${params.name}: ${type}`;
249271
}
250272
const defaultNil =
251-
params.key === "agentId" ||
252-
(DEFAULTED_OPTIONAL_INIT_PARAMS[params.structName]?.has(params.key) ?? false);
273+
params.key === "agentId" || isDefaultedOptionalInitParam(params.structName, params.key);
253274
return `${params.name}: ${type}?${defaultNil ? " = nil" : ""}`;
254275
}
255276

src/gateway/server-methods/agent.test.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2243,8 +2243,8 @@ describe("gateway agent handler", () => {
22432243
state: "idle",
22442244
lastActivityAt: Date.now(),
22452245
},
2246-
spawnedBy: "agent:main:webchat:main",
2247-
parentSessionKey: "agent:main:webchat:main",
2246+
spawnedBy: "agent:main:telegram:123",
2247+
parentSessionKey: "agent:main:telegram:123",
22482248
hubDelegated: {
22492249
ownerSessionKey: "agent:main:webchat:main",
22502250
createdAt: Date.now(),
@@ -2260,6 +2260,8 @@ describe("gateway agent handler", () => {
22602260
message: "你好",
22612261
agentId: "main",
22622262
sessionKey: "agent:main:main",
2263+
replyChannel: "telegram",
2264+
to: "123",
22632265
deliver: true,
22642266
bestEffortDeliver: true,
22652267
idempotencyKey: "test-hub-delegated-metadata-internal",
@@ -2279,6 +2281,50 @@ describe("gateway agent handler", () => {
22792281
expect(callArgs.deliver).not.toBe(true);
22802282
});
22812283

2284+
it("keeps parent-owned thread-bound ACP session delivery visible", async () => {
2285+
mockMainSessionEntry({
2286+
acp: {
2287+
backend: "acpx",
2288+
agent: "codex",
2289+
runtimeSessionName: "runtime-thread-bound",
2290+
mode: "persistent",
2291+
state: "idle",
2292+
lastActivityAt: Date.now(),
2293+
},
2294+
spawnedBy: "agent:main:telegram:123",
2295+
parentSessionKey: "agent:main:telegram:123",
2296+
lastChannel: "telegram",
2297+
lastTo: "123",
2298+
});
2299+
mocks.agentCommand.mockResolvedValue({
2300+
payloads: [{ text: "thread reply" }],
2301+
meta: { durationMs: 100 },
2302+
});
2303+
2304+
await invokeAgent(
2305+
{
2306+
message: "thread-bound resume",
2307+
agentId: "main",
2308+
sessionKey: "agent:main:main",
2309+
deliver: true,
2310+
bestEffortDeliver: true,
2311+
idempotencyKey: "test-thread-bound-acp-visible-delivery",
2312+
},
2313+
{
2314+
reqId: "thread-bound-acp-visible-delivery",
2315+
client: backendGatewayClient(),
2316+
context: makeContext(),
2317+
},
2318+
);
2319+
2320+
const callArgs = await waitForAgentCommandCall<{
2321+
sessionEffects?: string;
2322+
deliver?: boolean;
2323+
}>();
2324+
expect(callArgs.sessionEffects).not.toBe("internal");
2325+
expect(callArgs.deliver).toBe(true);
2326+
});
2327+
22822328
it("rejects public transcriptMessage overrides", async () => {
22832329
primeMainAgentRun({ cfg: mocks.loadConfigReturn });
22842330
mocks.agentCommand.mockClear();

0 commit comments

Comments
 (0)