feat: stream live subagent progress in task events (#121549)

* feat(tasks): stream live subagent activity

* fix(tasks): correlate edit activity from start events

* fix(gateway): sanitize live task activity

* test(reply): align custody harnesses with lifecycle

* fix(outbound): preserve post-send recovery evidence

* fix(types): align reply harnesses with current contracts

* fix(tasks): preserve activity liveness and SDK parity

* test(sdk): avoid unsafe task summary assertion

* chore(contracts): refresh Plugin SDK API baseline
This commit is contained in:
Peter Steinberger
2026-08-10 18:37:58 -07:00
committed by GitHub
parent b6b937d6ac
commit b985d5a1ed
23 changed files with 842 additions and 51 deletions
@@ -9651,6 +9651,8 @@ public struct TaskSummary: Codable, Sendable {
public let endedat: AnyCodable?
public let toolusecount: Int?
public let lasttoolname: String?
public let lastactivity: String?
public let diffstat: [String: AnyCodable]?
public let progresssummary: String?
public let terminalsummary: String?
public let error: String?
@@ -9680,6 +9682,8 @@ public struct TaskSummary: Codable, Sendable {
endedat: AnyCodable? = nil,
toolusecount: Int? = nil,
lasttoolname: String? = nil,
lastactivity: String? = nil,
diffstat: [String: AnyCodable]? = nil,
progresssummary: String? = nil,
terminalsummary: String? = nil,
error: String? = nil,
@@ -9708,6 +9712,8 @@ public struct TaskSummary: Codable, Sendable {
self.endedat = endedat
self.toolusecount = toolusecount
self.lasttoolname = lasttoolname
self.lastactivity = lastactivity
self.diffstat = diffstat
self.progresssummary = progresssummary
self.terminalsummary = terminalsummary
self.error = error
@@ -9738,6 +9744,8 @@ public struct TaskSummary: Codable, Sendable {
case endedat = "endedAt"
case toolusecount = "toolUseCount"
case lasttoolname = "lastToolName"
case lastactivity = "lastActivity"
case diffstat = "diffStat"
case progresssummary = "progressSummary"
case terminalsummary = "terminalSummary"
case error
+18 -18
View File
@@ -53,7 +53,7 @@
{"closureHash":"ff3c4616cd6212a6d698831a8b287ad87e3968c8663f4090d095bb30ec40fc1c","declaration":"export function abortAndDrainAgentHarnessRun(params: { sessionId: string; sessionKey?: string; settleMs?: number; forceClear?: boolean; reason?: string; }): Promise<AbortAndDrainEmbeddedAgentRunResult>;","entrypoint":"agent-harness","exportName":"abortAndDrainAgentHarnessRun","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"58989e3fa91eecf2e05202e36548e6e64a3b9219e83154bd99866c851604e34e","declaration":"export function createAgentToolResultMiddlewareRunner(ctx: AgentToolResultMiddlewareContext, handlers?: AgentToolResultMiddleware[]): { applyToolResultMiddleware(event: AgentToolResultMiddlewareEvent): Promise<OpenClawAgentToolResult>; };","entrypoint":"agent-harness","exportName":"createAgentToolResultMiddlewareRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"b8f6355d7ad1700aceecddaa6e9ccf43091a792d5b481a1ee3f55294a881a67d","declaration":"export function createCodexAppServerToolResultExtensionRunner(ctx: CodexAppServerExtensionContext, factories?: CodexAppServerExtensionFactory[]): { applyToolResultExtensions(event: CodexAppServerToolResultEvent): Promise<AgentToolResult<unknown>>; };","entrypoint":"agent-harness","exportName":"createCodexAppServerToolResultExtensionRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"9cfaaa0bb0d08eb8d47932492a7874fb138705087508d7b361b049da2c2e350b","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"b90c27beade426e42f74cf9b74850c039c4a547814a671812532820be090441d","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"13649ee853485319e7449fda25c106b3f3f53d3090e53cdbb28e220a8529eb80","declaration":"export function disposeRegisteredAgentHarnesses(): Promise<void>;","entrypoint":"agent-harness","exportName":"disposeRegisteredAgentHarnesses","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"f255a91162ece4c239bb8fa745efa14a1054bc024a291e9954f75f239b2b87c3","declaration":"export function resolveActiveEmbeddedRunSessionId(sessionKey: string): string | undefined;","entrypoint":"agent-harness","exportName":"resolveActiveEmbeddedRunSessionId","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"27d9f61df9ccf68615cb2002a6da891e36ab0c07623bc37c4d07aa980b446881","declaration":"export function resolveWebSearchToolPolicy(params: WebSearchToolPolicyParams): WebSearchToolPolicyResolution;","entrypoint":"agent-harness","exportName":"resolveWebSearchToolPolicy","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
@@ -1097,7 +1097,7 @@
{"closureHash":"9555a73ba2717bba0bf94888017be4cecef2c51cc3887ed34f8d8ef3c79d6abf","declaration":"export const DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS: number;","entrypoint":"channel-message","exportName":"DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"}
{"closureHash":"3a2df3ab656149630c004d2d9f71b8ceda99aa81eb6b72b0cc7b3982ed0ce044","declaration":"export const DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS: 8;","entrypoint":"channel-message","exportName":"DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"}
{"closureHash":"38c735a14c65067aa7825390d1c5c8876c651d0603e84980dfaf947f5235747a","declaration":"export const INGRESS_CLAIM_PROCESS_ID: string;","entrypoint":"channel-message","exportName":"INGRESS_CLAIM_PROCESS_ID","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"}
{"closureHash":"d420856f586028947a5239f5f44c78f13d611039f1e5292438eae0b5e1f4133e","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-message","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"}
{"closureHash":"cbac4698e50d1e7e9e18298245eb0c77dce31fbe315f07a62b3adbc38ba1e095","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-message","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"}
{"closureHash":"2c52eff200a5b9bd2d93ee72209c2626bbe165f59b538591db661b94773023b0","declaration":"export type AgentPlanStep = AgentPlanStep;","entrypoint":"channel-message","exportName":"AgentPlanStep","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"}
{"closureHash":"1e8bcb2c8d8f5327f5b81b8c217f722f090dccba21ba3943324010f4f80a5a50","declaration":"export type AgentPlanStepStatus = AgentPlanStepStatus;","entrypoint":"channel-message","exportName":"AgentPlanStepStatus","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"}
{"closureHash":"b036fc26e5330dfd7da1af8839bb3c23e85283b9e16e252a659dd4bb0c35cd5e","declaration":"export type ChannelDeliveryStreamingConfig = ChannelDeliveryStreamingConfig;","entrypoint":"channel-message","exportName":"ChannelDeliveryStreamingConfig","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"}
@@ -1226,7 +1226,7 @@
{"closureHash":"9555a73ba2717bba0bf94888017be4cecef2c51cc3887ed34f8d8ef3c79d6abf","declaration":"export const DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS: number;","entrypoint":"channel-outbound","exportName":"DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"}
{"closureHash":"3a2df3ab656149630c004d2d9f71b8ceda99aa81eb6b72b0cc7b3982ed0ce044","declaration":"export const DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS: 8;","entrypoint":"channel-outbound","exportName":"DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"}
{"closureHash":"38c735a14c65067aa7825390d1c5c8876c651d0603e84980dfaf947f5235747a","declaration":"export const INGRESS_CLAIM_PROCESS_ID: string;","entrypoint":"channel-outbound","exportName":"INGRESS_CLAIM_PROCESS_ID","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"}
{"closureHash":"d420856f586028947a5239f5f44c78f13d611039f1e5292438eae0b5e1f4133e","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-outbound","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"}
{"closureHash":"cbac4698e50d1e7e9e18298245eb0c77dce31fbe315f07a62b3adbc38ba1e095","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-outbound","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"}
{"closureHash":"2c52eff200a5b9bd2d93ee72209c2626bbe165f59b538591db661b94773023b0","declaration":"export type AgentPlanStep = AgentPlanStep;","entrypoint":"channel-outbound","exportName":"AgentPlanStep","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"}
{"closureHash":"1e8bcb2c8d8f5327f5b81b8c217f722f090dccba21ba3943324010f4f80a5a50","declaration":"export type AgentPlanStepStatus = AgentPlanStepStatus;","entrypoint":"channel-outbound","exportName":"AgentPlanStepStatus","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"}
{"closureHash":"b036fc26e5330dfd7da1af8839bb3c23e85283b9e16e252a659dd4bb0c35cd5e","declaration":"export type ChannelDeliveryStreamingConfig = ChannelDeliveryStreamingConfig;","entrypoint":"channel-outbound","exportName":"ChannelDeliveryStreamingConfig","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"}
@@ -2283,7 +2283,7 @@
{"closureHash":"a8874cc3b2cb522f6ca38ba5e353ec290aad0495c56087396ecaeb1cee9981b6","declaration":"export type NodeMatchCandidate = NodeMatchCandidate;","entrypoint":"gateway-runtime","exportName":"NodeMatchCandidate","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"c970e76e8d4d8e5fe4b81cb9efe47392d390f826eeb22e409d178473e6d28bed","declaration":"export type NodeSession = NodeSession;","entrypoint":"gateway-runtime","exportName":"NodeSession","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"6cfef265e2aebd53c1e7ce29cb204228ac175b07f9d2f22b861e98cac0eaee4a","declaration":"export type NormalizedPluginNodeCapabilityUrl = NormalizedPluginNodeCapabilityUrl;","entrypoint":"gateway-runtime","exportName":"NormalizedPluginNodeCapabilityUrl","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"type","recordType":"export"}
{"closureHash":"dd294389ffc2b0c46e1bd68d402db6122f5347dfecebe220f23ccd9a9c4e1ca5","declaration":"export class GatewayClient {\n #client: BaseGatewayClient;\n constructor(opts: GatewayClientOptions);\n start(): void;\n stop(): void;\n stopAndWait(opts?: {\n timeoutMs?: number;\n }): Promise<void>;\n request<T = Record<string, unknown>>(method: string, params?: unknown, opts?: GatewayClientRequestOptions): Promise<T>;\n getConnectionMetadata(): GatewayClientConnectionMetadata;\n updateNodeManifest(manifest: {\n caps: string[];\n commands: string[];\n }): void;\n}","entrypoint":"gateway-runtime","exportName":"GatewayClient","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"class","recordType":"export"}
{"closureHash":"86ece317b25cb30a863fdde765e8218283b391b6c2ee73bacb29c3915c8748d3","declaration":"export class GatewayClient {\n #client: BaseGatewayClient;\n constructor(opts: GatewayClientOptions);\n start(): void;\n stop(): void;\n stopAndWait(opts?: {\n timeoutMs?: number;\n }): Promise<void>;\n request<T = Record<string, unknown>>(method: string, params?: unknown, opts?: GatewayClientRequestOptions): Promise<T>;\n getConnectionMetadata(): GatewayClientConnectionMetadata;\n updateNodeManifest(manifest: {\n caps: string[];\n commands: string[];\n }): void;\n}","entrypoint":"gateway-runtime","exportName":"GatewayClient","importSpecifier":"openclaw/plugin-sdk/gateway-runtime","kind":"class","recordType":"export"}
{"category":null,"entrypoint":"group-access","importSpecifier":"openclaw/plugin-sdk/group-access","recordType":"module"}
{"closureHash":"b6b34cd3844debbe7cda17ad7703e4fcffe4489789ae1763df8f8ab027150203","declaration":"export function evaluateGroupRouteAccessForPolicy(params: { groupPolicy: GroupPolicy; routeAllowlistConfigured: boolean; routeMatched: boolean; routeEnabled?: boolean; }): GroupRouteAccessDecision;","entrypoint":"group-access","exportName":"evaluateGroupRouteAccessForPolicy","importSpecifier":"openclaw/plugin-sdk/group-access","kind":"function","recordType":"export"}
{"closureHash":"bf1a463ce98bf23d639d6e80112ffca836df6d63049bae6fef2a11a2ef1bb74c","declaration":"export function evaluateMatchedGroupAccessForPolicy(params: { groupPolicy: GroupPolicy; allowlistConfigured: boolean; allowlistMatched: boolean; requireMatchInput?: boolean; hasMatchInput?: boolean; }): MatchedGroupAccessDecision;","entrypoint":"group-access","exportName":"evaluateMatchedGroupAccessForPolicy","importSpecifier":"openclaw/plugin-sdk/group-access","kind":"function","recordType":"export"}
@@ -2354,7 +2354,7 @@
{"closureHash":"98e0cc16352b7dfcceb4957f8cd2acfe3c2b3acb223c42a5cfb885e088fcb04c","declaration":"export function resolveInboundReplyDispatchCounts(result: ChannelTurnDispatchResultLike): Record<ReplyDispatchKind, number>;","entrypoint":"inbound-reply-dispatch","exportName":"resolveInboundReplyDispatchCounts","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"}
{"closureHash":"9d503d05e408ddd1a1c373e3c4485b1f9acff4c240fdc241840a2cfa8b55c3a7","declaration":"export function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: RunChannelTurnParams<TRaw, TDispatchResult, ChannelProviderOwnedMessageSendingDeliveryAdapter>): Promise<ChannelTurnResult<TDispatchResult>>;\nexport function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: ChannelInboundEventRunnerParams<TRaw, TDispatchResult>): Promise<ChannelTurnResult<TDispatchResult>>;","entrypoint":"inbound-reply-dispatch","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"}
{"closureHash":"2c68496a174872b31aaf1ac8fad2789deb79d490c5ffbcb157325f1c95d1b51f","declaration":"export function runPreparedInboundReply<TDispatchResult>(params: PreparedChannelTurn<TDispatchResult>): Promise<ChannelTurnResult<TDispatchResult>>;","entrypoint":"inbound-reply-dispatch","exportName":"runPreparedInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"}
{"closureHash":"d420856f586028947a5239f5f44c78f13d611039f1e5292438eae0b5e1f4133e","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"inbound-reply-dispatch","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"const","recordType":"export"}
{"closureHash":"cbac4698e50d1e7e9e18298245eb0c77dce31fbe315f07a62b3adbc38ba1e095","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"inbound-reply-dispatch","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"const","recordType":"export"}
{"closureHash":"278dc8a410230012ddbc3552ec621a224e6f7c5dd4cd3904c943ac98dd4259e4","declaration":"export type AssembledInboundReply = AssembledChannelTurn;","entrypoint":"inbound-reply-dispatch","exportName":"AssembledInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"}
{"closureHash":"3de0d6d1bb8f9a83bd1c6e57aa8618e3fea71054236cd530c555e348bd0b7702","declaration":"export type ChannelBotLoopProtectionFacts = ChannelBotLoopProtectionFacts;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelBotLoopProtectionFacts","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"}
{"closureHash":"f0da845bbdf49b30f6718a4b7ff012a0536deb98f65418583555dc90825797ca","declaration":"export type ChannelInboundDroppedHistoryOptions = ChannelTurnDroppedHistoryOptions;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelInboundDroppedHistoryOptions","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"}
@@ -3189,19 +3189,19 @@
{"closureHash":"50394942e0a454c742e80a4f3c8739c80258b387ade498eb5aed66df887108be","declaration":"export function callMeetingBrowserProxyOnNode(params: { runtime: PluginRuntime; adapter: NodeAdapter; nodeId: string; } & MeetingBrowserRequestParams): Promise<unknown>;","entrypoint":"meeting-runtime","exportName":"callMeetingBrowserProxyOnNode","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"c5fcd1d157edcb4773235adfaf8051e3ec7ca428cb7916b6d55f2511affab56c","declaration":"export function convertMeetingBridgeAudioForStt(audio: Buffer, audioFormat: MeetingRealtimeAudioFormat): Buffer;","entrypoint":"meeting-runtime","exportName":"convertMeetingBridgeAudioForStt","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"aa9063c27c5ff714d3653571c4a2a71e9d2eea5609b161f03e4738e15411358b","declaration":"export function convertMeetingTtsAudioForBridge(audio: Buffer, sampleRate: number, audioFormat: MeetingRealtimeAudioFormat, outputFormat?: string, platformName?: string): Buffer;","entrypoint":"meeting-runtime","exportName":"convertMeetingTtsAudioForBridge","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"c70707c4e2171a912f22899350eea3d71a5969eacd50dc1d59f06f3bb86acbd9","declaration":"export function createLocalMeetingRealtimeAudioTransport(params: { inputCommand: string[]; outputCommand: string[]; bargeInInputCommand?: string[]; bargeInRmsThreshold: number; bargeInPeakThreshold: number; bargeInCooldownMs: number; logger: RuntimeLogger; logScope: string; audioFormat?: MeetingRealtimeAudioFormat; spawn?: MeetingRealtimeAudioSpawn; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createLocalMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"6761bc03311f27e9ff57a2aade15b73d9b3dfe49ed89a3bf485d894878874e1e","declaration":"export function createLocalMeetingRealtimeAudioTransport(params: { inputCommand: string[]; outputCommand: string[]; bargeInInputCommand?: string[]; bargeInRmsThreshold: number; bargeInPeakThreshold: number; bargeInCooldownMs: number; logger: RuntimeLogger; logScope: string; audioFormat?: MeetingRealtimeAudioFormat; spawn?: MeetingRealtimeAudioSpawn; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createLocalMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"9cb63eefd2e5a70aea960bf17fcf570ef02a2d7b5afd5ad9451802964c653fcf","declaration":"export function createMeetingBrowserNodeCaller(params: { runtime: PluginRuntime; adapter: NodeAdapter; nodeId: string; }): MeetingBrowserRequestCaller;","entrypoint":"meeting-runtime","exportName":"createMeetingBrowserNodeCaller","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"f244ca8c47f556c9f388953cf9a7581c7d4d413af93e0dfa1462779e6a687a7c","declaration":"export function createMeetingBrowserNodeInvokePolicy(options: MeetingBrowserNodePolicyOptions): OpenClawPluginNodeInvokePolicy;","entrypoint":"meeting-runtime","exportName":"createMeetingBrowserNodeInvokePolicy","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"1aa4e3ff39a2cbe162421c62b1fa4a362973381d4d9ff15c671249b1ebd566bc","declaration":"export function createMeetingNodeHost(options: MeetingNodeHostOptions): { handleCommand(paramsJSON?: string | null): Promise<string>; };","entrypoint":"meeting-runtime","exportName":"createMeetingNodeHost","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"70a5384f9a5e217338410ae45f1b93bb431ccb0c2b9c1a40bc5e50bc68a2449b","declaration":"export function createMeetingRealtimeEngineBindings(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { agentId?: string; toolPolicy: RealtimeVoiceAgentConsultToolPolicy; }; }; fullConfig: OpenClawConfig; runtime: PluginRuntime; logger: RuntimeLogger; }): { platform: MeetingRuntimePlatform; consultAgent: (consult: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (call: MeetingRealtimeToolCallParams) => Promise<void>; };","entrypoint":"meeting-runtime","exportName":"createMeetingRealtimeEngineBindings","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"02c913751c269860cefec36c89fcbe993aaeec38dbd4e01964897fb128ecf9c9","declaration":"export function createMeetingRealtimeEngineBindings(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { agentId?: string; toolPolicy: RealtimeVoiceAgentConsultToolPolicy; }; }; fullConfig: OpenClawConfig; runtime: PluginRuntime; logger: RuntimeLogger; }): { platform: MeetingRuntimePlatform; consultAgent: (consult: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (call: MeetingRealtimeToolCallParams) => Promise<void>; };","entrypoint":"meeting-runtime","exportName":"createMeetingRealtimeEngineBindings","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"9124db05923060c467edb149bd8abdc8be1d00d00f867cf84600972923a58489","declaration":"export function createMeetingSession<TTransport extends string, TMode extends string, TToolPolicy extends string>(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { provider?: string; voiceProvider?: string; transcriptionProvider?: string; model?: string; toolPolicy: TToolPolicy; }; }; resolved: MeetingResolvedJoin<TTransport, TMode>; createdAt: string; }): MeetingSessionRecord<TTransport, TMode, { enabled: boolean; strategy: string; provider: string | undefined; model: string | undefined; transcriptionProvider: string | undefined; toolPolicy: TToolPolicy; }>;","entrypoint":"meeting-runtime","exportName":"createMeetingSession","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"fb276e07e395ab19177b3d7967fd0a8fcda79130883f15092a4a306b826481b9","declaration":"export function createMeetingSetupStatus(checks: MeetingSetupCheck[]): MeetingSetupStatus;","entrypoint":"meeting-runtime","exportName":"createMeetingSetupStatus","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"93c4bb70429a2739151e6f900e67bed81f3cd8f597811220e790341edb0d6008","declaration":"export function createMeetingVoiceCallGateway(params: { config: MeetingVoiceCallConfig; runtime: PluginRuntime; surface: MeetingVoiceCallSurface; connectClient: (params: { config: MeetingVoiceCallConfig; surface: MeetingVoiceCallSurface; }) => Promise<MeetingVoiceCallGatewayClient>; }): MeetingVoiceCallGateway;","entrypoint":"meeting-runtime","exportName":"createMeetingVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"bc1315a8130ef57fe62d31d54f35b7a9d3085ae5bae65a5dc23a83a66c36436a","declaration":"export function createNodeMeetingRealtimeAudioTransport(params: { runtime: PluginRuntime; nodeId: string; bridgeId: string; logger: RuntimeLogger; commandName: string; logScope: string; logPrefix: string; audioFormat?: MeetingRealtimeAudioFormat; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createNodeMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"6063c9f201a633e26d373759c3849c61dd7414ecf289f3acde555a987a856535","declaration":"export function createNodeMeetingRealtimeAudioTransport(params: { runtime: PluginRuntime; nodeId: string; bridgeId: string; logger: RuntimeLogger; commandName: string; logScope: string; logPrefix: string; audioFormat?: MeetingRealtimeAudioFormat; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createNodeMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"c0987b15a391a92d53d3e4e12b61eb122f9547fcf4f45478cdd3c5a8973a3e52","declaration":"export function endMeetingVoiceCallGatewayCall(params: { gateway: MeetingVoiceCallGateway; callId: string; }): Promise<void>;","entrypoint":"meeting-runtime","exportName":"endMeetingVoiceCallGatewayCall","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"b3ebc10fe7ab372e1317eaa8f78621cf277bba1e33fda57acf2254f6759d1e9d","declaration":"export function getMeetingVoiceCallGatewayCall(params: { gateway: MeetingVoiceCallGateway; callId: string; }): Promise<MeetingVoiceCallStatusResult>;","entrypoint":"meeting-runtime","exportName":"getMeetingVoiceCallGatewayCall","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"aac088df8e80d9ce89c45feccdb04ace3ae4417b3fcdd0f058410ddf25c96b46","declaration":"export function isMeetingVoiceCallMissingError(error: unknown): boolean;","entrypoint":"meeting-runtime","exportName":"isMeetingVoiceCallMissingError","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"6f5d639ae00dcc0d07499c27d21bf1f865139a81ddaa315468991d7ba2f35c7f","declaration":"export function joinMeetingViaVoiceCallGateway(params: { config: MeetingVoiceCallConfig; gateway: MeetingVoiceCallGateway; surface: MeetingVoiceCallSurface; dialInNumber: string; dtmfSequence?: string; logger?: RuntimeLogger; message?: string; requesterSessionKey?: string; agentId?: string; sessionKey?: string; }): Promise<MeetingVoiceCallJoinResult>;","entrypoint":"meeting-runtime","exportName":"joinMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"32a35206f0ead8c0fd4ca9609ebd3fc35d99937b3af832ffd2bbb00cdb3bf631","declaration":"export function joinMeetingViaVoiceCallGateway(params: { config: MeetingVoiceCallConfig; gateway: MeetingVoiceCallGateway; surface: MeetingVoiceCallSurface; dialInNumber: string; dtmfSequence?: string; logger?: RuntimeLogger; message?: string; requesterSessionKey?: string; agentId?: string; sessionKey?: string; }): Promise<MeetingVoiceCallJoinResult>;","entrypoint":"meeting-runtime","exportName":"joinMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"bac9621cd025754fbcd4c21409b06eb0503405db81f35cac993aaf0b881ab3e1","declaration":"export function leaveMeetingWithBrowser<Session, Mode extends string, Health extends MeetingBrowserHealth, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter<Session, Mode, Health, Transcript>; callBrowser: MeetingBrowserRequestCaller; launch: boolean; meetingSessionId?: string; meetingUrl: string; tab: MeetingBrowserTab; timeoutMs: number; }): Promise<{ left: boolean; note: string; }>;","entrypoint":"meeting-runtime","exportName":"leaveMeetingWithBrowser","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"c7b1aeac134b2070d053adf33026d876998b852ccd65cd2c9667119b1a1abee2","declaration":"export function openMeetingWithBrowser<Session extends MeetingBrowserJoinSession<Mode>, Mode extends string, Health extends MeetingBrowserHealth & { browserTitle?: string; browserUrl?: string; notes?: string[]; }, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter<Session, Mode, Health, Transcript>; callBrowser: MeetingBrowserRequestCaller; config: MeetingBrowserControllerConfig; session: Session; }): Promise<{ launched: boolean; browser?: Health; tab?: MeetingBrowserTab; }>;","entrypoint":"meeting-runtime","exportName":"openMeetingWithBrowser","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"34858979bd248799c2215d9c7bdd5d4a6b27660c9173bfca2e50dd4373ac27db","declaration":"export function readMeetingBrowserTab(result: unknown): MeetingBrowserCandidateTab | undefined;","entrypoint":"meeting-runtime","exportName":"readMeetingBrowserTab","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
@@ -3212,8 +3212,8 @@
{"closureHash":"56874c5b2ced5af647b1dff296c931f0d8b85b1318e98c741aa88b58a9c5c606","declaration":"export function resolveMeetingBrowserNodeInfo(params: { runtime: PluginRuntime; adapter: NodeAdapter; requestedNode?: string; }): Promise<MeetingBrowserNodeInfo>;","entrypoint":"meeting-runtime","exportName":"resolveMeetingBrowserNodeInfo","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"f21196c9ea8ab90248ffdcdef3e942f1247ee2b9064ddb9757fffe352362c779","declaration":"export function resolveMeetingRealtimeAudioFormat(audioFormat: MeetingRealtimeAudioFormat): RealtimeVoiceAudioFormat;","entrypoint":"meeting-runtime","exportName":"resolveMeetingRealtimeAudioFormat","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"521dc5c262524a709cdd27cdc88e95004073da8223537bcbc73d5d423172dc2b","declaration":"export function speakMeetingViaVoiceCallGateway(params: { gateway: MeetingVoiceCallGateway; callId: string; message: string; }): Promise<void>;","entrypoint":"meeting-runtime","exportName":"speakMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"0e660fb72f507995ee218f6cb63a392ddac22e9fcb98b4a5b9b577d6afbd2377","declaration":"export function startMeetingAgentRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeTranscriptionProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; }): Promise<MeetingRealtimeAudioEngineHandle>;","entrypoint":"meeting-runtime","exportName":"startMeetingAgentRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"690d10ea23a39c93ded9782825b020ebffa20d7a979ea5274773ef414d867529","declaration":"export function startMeetingRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; talkSessionId?: string; talkContext?: { nodeId: string; bridgeId: string; }; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeVoiceProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (params: MeetingRealtimeToolCallParams) => Promise<void>; }): Promise<MeetingRealtimeAudioEngineHandle>;","entrypoint":"meeting-runtime","exportName":"startMeetingRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"2b72efb32e2ccca2910d40516c81e5a59ec23f1790d8086f196613e7fb53a594","declaration":"export function startMeetingAgentRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeTranscriptionProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; }): Promise<MeetingRealtimeAudioEngineHandle>;","entrypoint":"meeting-runtime","exportName":"startMeetingAgentRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"02443f8df6e1f5344619d53b0cb136463a35725af962676660f57d0a8c82948d","declaration":"export function startMeetingRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; talkSessionId?: string; talkContext?: { nodeId: string; bridgeId: string; }; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeVoiceProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (params: MeetingRealtimeToolCallParams) => Promise<void>; }): Promise<MeetingRealtimeAudioEngineHandle>;","entrypoint":"meeting-runtime","exportName":"startMeetingRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"663b9c77231b21af697efd5042e22d1c33e9ee972adeb312ad86d81a2b2e36f7","declaration":"export type MeetingAgentConsultParams = MeetingAgentConsultParams;","entrypoint":"meeting-runtime","exportName":"MeetingAgentConsultParams","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"adf0745c611bf737fc28978d766fc2a25b7104e71b6b2fed4e9b95e519c7b115","declaration":"export type MeetingBrowserCandidateTab = MeetingBrowserCandidateTab;","entrypoint":"meeting-runtime","exportName":"MeetingBrowserCandidateTab","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"59b1526172b7491b85f14f13c1ee26d8d6726f33093770748580c767b1689a17","declaration":"export type MeetingBrowserControllerConfig = MeetingBrowserControllerConfig;","entrypoint":"meeting-runtime","exportName":"MeetingBrowserControllerConfig","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
@@ -3245,7 +3245,7 @@
{"closureHash":"8244b408803f57cf2004157e96f2ce11bb036cdf864123b15ba2fac7f03b5d43","declaration":"export type MeetingSessionRuntimeHandles<THealth extends MeetingBrowserHealth> = MeetingSessionRuntimeHandles<THealth>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeHandles","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"aa8054fc2c442891197e89bdad61e79999ea15d813c4b127fb6150607b9e67e7","declaration":"export type MeetingSessionRuntimeJoinContext<TSession extends MeetingSessionRecord<TTransport, TMode>, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab> = MeetingSessionRuntimeJoinContext<TSession, TTransport, TMode, THealth, TTab>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeJoinContext","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"325241724edd38da439c5df03d225e2a7cb8e56d817e98ed7c37850656d3744b","declaration":"export type MeetingSessionRuntimeMessages<TSpeechBlockedReason extends string> = MeetingSessionRuntimeMessages<TSpeechBlockedReason>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeMessages","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"d5cfb8ce0591d2c56f9c7d2962a3c103affaa560f066b3a9ec19e96104883458","declaration":"export type MeetingSessionRuntimeOptions<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeOptions","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"6d59e75073bf72818c78595899cbd5be345048a5ac0c852d6a35c2fe9348756b","declaration":"export type MeetingSessionRuntimeOptions<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeOptions","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"d3c730cba02d30512c4cdae6e2367c662982e2c25671833ecd8701283924c642","declaration":"export type MeetingSessionState = MeetingSessionState;","entrypoint":"meeting-runtime","exportName":"MeetingSessionState","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"2496931157bf64fdb16013172ab9c023e60c5f85b98f46a3ee9532ce58d636d6","declaration":"export type MeetingSetupCheck = MeetingSetupCheck;","entrypoint":"meeting-runtime","exportName":"MeetingSetupCheck","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"f0aed6f7955a43fc387d8bbc072cb6276b0f2d18e7f829441a2ad45974bb3831","declaration":"export type MeetingSetupStatus = MeetingSetupStatus;","entrypoint":"meeting-runtime","exportName":"MeetingSetupStatus","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
@@ -3259,9 +3259,9 @@
{"closureHash":"20c70a94483aeddda4d1500cfcc5e71fab844b474ba4061c55d6c8693d40821b","declaration":"export type MeetingVoiceCallJoinResult = MeetingVoiceCallJoinResult;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallJoinResult","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"4fb2c5928aa6f809dd4c604d71c5c99cdfe3e98b6e9c1662601c9b4ad9738162","declaration":"export type MeetingVoiceCallStatusResult = MeetingVoiceCallStatusResult;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallStatusResult","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"5ef812397b6409a4ca228943dd1080661365959477b42e705a9aaa219757d14f","declaration":"export type MeetingVoiceCallSurface = MeetingVoiceCallSurface;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallSurface","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"9b3952af9ca74d74b0ec98824cdc759023b134dacef29117fb907f7c5d5c7d1a","declaration":"export interface MeetingPlatformAdapter<Session, Mode extends string, Health extends MeetingBrowserHealth, Transcript extends MeetingTranscriptSnapshot, CreateParams = never, CreateResult = never, DialInParams = never, DialInPlan = never> extends MeetingPlatformAdapterContract<Session, Mode, Health, Transcript, CreateParams, CreateResult, DialInParams, DialInPlan> {\n}","entrypoint":"meeting-runtime","exportName":"MeetingPlatformAdapter","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"}
{"closureHash":"94279205b3342f00dd887290dff6f63279b7edc0ec1ed6e58fff959e41672689","declaration":"export interface MeetingPlatformAdapter<Session, Mode extends string, Health extends MeetingBrowserHealth, Transcript extends MeetingTranscriptSnapshot, CreateParams = never, CreateResult = never, DialInParams = never, DialInPlan = never> extends MeetingPlatformAdapterContract<Session, Mode, Health, Transcript, CreateParams, CreateResult, DialInParams, DialInPlan> {\n}","entrypoint":"meeting-runtime","exportName":"MeetingPlatformAdapter","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"}
{"closureHash":"79884bd1df1ea0d07d5ceb4c62d93c1ee744b75d31c94c988845869c0f3cbd2d","declaration":"export interface MeetingRealtimeAudioTransport {\n onFatal(handler: () => void): void;\n startInput(onAudio: (audio: Buffer) => void): void;\n beginOutput?(): void;\n stop(): Promise<void>;\n writeOutput(audio: Buffer): Promise<void>;\n clearOutput(): Promise<void>;\n dispose(): Promise<void>;\n getHealth?(): MeetingRealtimeAudioTransportHealth;\n startBargeInMonitor?(onBargeIn: (audio: Buffer) => boolean): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"}
{"closureHash":"1e14a81a141e1f34fc747e106e98538c67f53fe5e221883ce512f6863a6b53e3","declaration":"export class MeetingSessionRuntime<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map<string, TSession>;\n readonly #sessionLeaves: Map<string, Promise<MeetingSessionLeaveResult<TSession>>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map<string, () => Promise<void>>;\n readonly #sessionSpeakers: Map<string, (instructions?: string) => void>;\n readonly #sessionHealth: Map<string, () => Partial<THealth>>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts<TSession>;\n readonly #transcriptStore: MeetingSessionTranscriptStore<TSession>;\n constructor(private readonly options: MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>);\n list(): TSession[];\n getSession(sessionId: string): TSession | undefined;\n async status(sessionId?: string): Promise<{\n found: boolean;\n session?: TSession;\n sessions?: TSession[];\n }>;\n async transcript(sessionId: string, options: {\n sinceIndex?: number;\n }): Promise<{ found: boolean; sessionId?: string; startIndex?: number; nextIndex?: number; droppedLines?: number; evicted?: boolean; lines?: MeetingTranscriptLine[]; }>;\n async startTranscriptSource(request: TranscriptStartRequest): Promise<TranscriptsStartResult>;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise<TranscriptsStopResult>;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin<TTransport, TMode>): boolean;\n async join(request: TRequest): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async leave(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n async speak(sessionId: string, instructions?: string): Promise<{\n found: boolean;\n spoken: boolean;\n session?: TSession;\n }>;\n async speakWhenReady(session: TSession, instructions: string): Promise<boolean>;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise<void>;\n async refreshCaptionHealth(session: TSession): Promise<void>;\n refreshSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n markSessionEnded(session: TSession, reason: string): void;\n async #joinUnlocked(request: TRequest, resolved: MeetingResolvedJoin<TTransport, TMode>): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n #meetingKey(transport: TTransport, url: string): string;\n #inheritBrowserTabOwnership(params: {\n session: TSession;\n transport: TTransport;\n nodeId?: string;\n meetingUrl: string;\n tab?: TTab;\n }): TTab | undefined;\n async #settleRetainedBrowserTabs(retained: Array<{\n session: TSession;\n tab: TTab;\n }>, adopted?: {\n transport: TTransport;\n nodeId?: string;\n tab: TTab;\n }): Promise<boolean>;\n async #rollbackFailedJoinSession(session: TSession): Promise<void>;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise<void>;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles<THealth>): void;\n #dropRuntimeHandles(sessionId: string): void;\n #isManagedBrowserSession(session: TSession): boolean;\n #evaluateSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n #noteSession(session: TSession, note: string): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"class","recordType":"export"}
{"closureHash":"ecdf22dc3e79e8d465a80e2ff8e453653b30475ceac34ae9a7eba9c4ecfe9038","declaration":"export class MeetingSessionRuntime<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map<string, TSession>;\n readonly #sessionLeaves: Map<string, Promise<MeetingSessionLeaveResult<TSession>>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map<string, () => Promise<void>>;\n readonly #sessionSpeakers: Map<string, (instructions?: string) => void>;\n readonly #sessionHealth: Map<string, () => Partial<THealth>>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts<TSession>;\n readonly #transcriptStore: MeetingSessionTranscriptStore<TSession>;\n constructor(private readonly options: MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>);\n list(): TSession[];\n getSession(sessionId: string): TSession | undefined;\n async status(sessionId?: string): Promise<{\n found: boolean;\n session?: TSession;\n sessions?: TSession[];\n }>;\n async transcript(sessionId: string, options: {\n sinceIndex?: number;\n }): Promise<{ found: boolean; sessionId?: string; startIndex?: number; nextIndex?: number; droppedLines?: number; evicted?: boolean; lines?: MeetingTranscriptLine[]; }>;\n async startTranscriptSource(request: TranscriptStartRequest): Promise<TranscriptsStartResult>;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise<TranscriptsStopResult>;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin<TTransport, TMode>): boolean;\n async join(request: TRequest): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async leave(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n async speak(sessionId: string, instructions?: string): Promise<{\n found: boolean;\n spoken: boolean;\n session?: TSession;\n }>;\n async speakWhenReady(session: TSession, instructions: string): Promise<boolean>;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise<void>;\n async refreshCaptionHealth(session: TSession): Promise<void>;\n refreshSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n markSessionEnded(session: TSession, reason: string): void;\n async #joinUnlocked(request: TRequest, resolved: MeetingResolvedJoin<TTransport, TMode>): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n #meetingKey(transport: TTransport, url: string): string;\n #inheritBrowserTabOwnership(params: {\n session: TSession;\n transport: TTransport;\n nodeId?: string;\n meetingUrl: string;\n tab?: TTab;\n }): TTab | undefined;\n async #settleRetainedBrowserTabs(retained: Array<{\n session: TSession;\n tab: TTab;\n }>, adopted?: {\n transport: TTransport;\n nodeId?: string;\n tab: TTab;\n }): Promise<boolean>;\n async #rollbackFailedJoinSession(session: TSession): Promise<void>;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise<void>;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles<THealth>): void;\n #dropRuntimeHandles(sessionId: string): void;\n #isManagedBrowserSession(session: TSession): boolean;\n #evaluateSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n #noteSession(session: TSession, note: string): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"class","recordType":"export"}
{"category":null,"entrypoint":"memory-core-host-engine-foundation","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","recordType":"module"}
{"closureHash":"e2df8d235401aebf58eb71652fbeaedb42ca00aaa5042a76909ff2c48ba6fd54","declaration":"export function createSubsystemLogger(subsystem: string): SubsystemLogger;","entrypoint":"memory-core-host-engine-foundation","exportName":"createSubsystemLogger","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","kind":"function","recordType":"export"}
{"closureHash":null,"declaration":"export function isPathInside(root: string, target: string): boolean;","entrypoint":"memory-core-host-engine-foundation","exportName":"isPathInside","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","kind":"function","recordType":"export"}
@@ -3655,10 +3655,10 @@
{"closureHash":"e66adcb55dec79be73a52bc76594b7e9210fd93c69a85b56098fbb58cecbb354","declaration":"export type WriteOAuthCredentialsOptions = WriteOAuthCredentialsOptions;","entrypoint":"provider-auth","exportName":"WriteOAuthCredentialsOptions","importSpecifier":"openclaw/plugin-sdk/provider-auth","kind":"type","recordType":"export"}
{"category":null,"entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","recordType":"module"}
{"closureHash":"b5665596a3f011454a5614681513516acd50b6c749536c43842d811980cf7ad3","declaration":"export function augmentModelCatalogWithProviderPlugins(params: { config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; metadataSnapshot?: PluginMetadataSnapshot; context: ProviderAugmentModelCatalogContext; }): Promise<ModelCatalogEntry[]>;","entrypoint":"provider-catalog-runtime","exportName":"augmentModelCatalogWithProviderPlugins","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"21ecb9d9b55785897a3abb0fd07613d0e06cb323c2d9e5066d66e64821e2acd5","declaration":"export function isPluginProvidersLoadInFlight(params: Parameters<typeof resolvePluginProviders>[0]): boolean;","entrypoint":"provider-catalog-runtime","exportName":"isPluginProvidersLoadInFlight","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"a81a0603b2a5f136ec58ccf6b0963f15dcecec57506ca2f58a3aff2cc4ec903c","declaration":"export function resolveCatalogHookProviderPluginIds(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; metadataSnapshot?: ProviderManifestLoadParams[\"metadataSnapshot\"]; }): string[];","entrypoint":"provider-catalog-runtime","exportName":"resolveCatalogHookProviderPluginIds","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"61d926974cc9b13df361c3fefa7b7ec570ecda663126f0847c77211d1b710058","declaration":"export function resolveOwningPluginIdsForProvider(params: { provider: string; config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; manifestRegistry?: PluginManifestRegistry; metadataSnapshot?: Pick<PluginMetadataSnapshot, \"owners\" | \"manifestRegistry\" | \"byPluginId\">; }): string[] | undefined;","entrypoint":"provider-catalog-runtime","exportName":"resolveOwningPluginIdsForProvider","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"beaaa1633f55d6f9d1dc5c49da4a297d73e5c35756162302ed4dc72cfc291d90","declaration":"export function resolvePluginProviders(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; bundledProviderVitestCompat?: boolean; onlyPluginIds?: string[]; providerRefs?: readonly string[]; modelRefs?: readonly string[]; activate?: boolean; cache?: boolean; applyAutoEnable?: boolean; pluginSdkResolution?: PluginLoadOptions[\"pluginSdkResolution\"]; mode?: \"runtime\" | \"setup\"; includeUntrustedWorkspacePlugins?: boolean; pluginMetadataSnapshot?: PluginMetadataRegistryView; skipIfLoadInFlight?: boolean; }): ProviderPlugin[];","entrypoint":"provider-catalog-runtime","exportName":"resolvePluginProviders","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"160733277c5b1af7f64a67bcab55f106fe08efff5545889e1255f0e3cf9423f3","declaration":"export function isPluginProvidersLoadInFlight(params: Parameters<typeof resolvePluginProviders>[0]): boolean;","entrypoint":"provider-catalog-runtime","exportName":"isPluginProvidersLoadInFlight","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"6862b1adaf0803f095949e4a2db2f0a9544734428c778bae466e7aac32082baf","declaration":"export function resolveCatalogHookProviderPluginIds(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; metadataSnapshot?: ProviderManifestLoadParams[\"metadataSnapshot\"]; }): string[];","entrypoint":"provider-catalog-runtime","exportName":"resolveCatalogHookProviderPluginIds","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"cf07690a8d0732cfc1bf296474f5e2e4f9417f86cb35f9ca18f60c77f8e6e367","declaration":"export function resolveOwningPluginIdsForProvider(params: { provider: string; config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; manifestRegistry?: PluginManifestRegistry; metadataSnapshot?: Pick<PluginMetadataSnapshot, \"owners\" | \"manifestRegistry\" | \"byPluginId\">; }): string[] | undefined;","entrypoint":"provider-catalog-runtime","exportName":"resolveOwningPluginIdsForProvider","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"closureHash":"a6031956d807d7e7cf44c1249fb8f5f4f4c643f45a1a56caed6f0926aca2a2e3","declaration":"export function resolvePluginProviders(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; bundledProviderVitestCompat?: boolean; onlyPluginIds?: string[]; providerRefs?: readonly string[]; modelRefs?: readonly string[]; activate?: boolean; cache?: boolean; applyAutoEnable?: boolean; pluginSdkResolution?: PluginLoadOptions[\"pluginSdkResolution\"]; mode?: \"runtime\" | \"setup\"; includeUntrustedWorkspacePlugins?: boolean; pluginMetadataSnapshot?: PluginMetadataRegistryView; skipIfLoadInFlight?: boolean; }): ProviderPlugin[];","entrypoint":"provider-catalog-runtime","exportName":"resolvePluginProviders","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"}
{"category":null,"entrypoint":"proxy-capture","importSpecifier":"openclaw/plugin-sdk/proxy-capture","recordType":"module"}
{"closureHash":"d0e4e026d736bc00b1758d1089c44c3c5f3a09c7254291584de8cb297365804c","declaration":"export function acquireDebugProxyCaptureStore(dbPath: string, blobDir: string): { store: LegacyDebugProxyCaptureStore; release: () => void; };\nexport function acquireDebugProxyCaptureStore(options?: DebugProxyCaptureStoreOptions): { store: SharedDebugProxyCaptureStore; release: () => void; };","entrypoint":"proxy-capture","exportName":"acquireDebugProxyCaptureStore","importSpecifier":"openclaw/plugin-sdk/proxy-capture","kind":"function","recordType":"export"}
{"closureHash":"d41cbb9de51b7aee1c6cf8c713b0bc44f27ac926fbb6422c05726f196d11964e","declaration":"export function captureHttpExchange(params: { url: string; method: string; requestHeaders?: Headers | Record<string, string> | undefined; requestBody?: BodyInit | Buffer | string | null; response: Response; transport?: \"http\" | \"sse\"; flowId?: string; meta?: Record<string, unknown>; }, resolved?: DebugProxySettings, deps?: DebugProxyCaptureRuntimeDeps): void;","entrypoint":"proxy-capture","exportName":"captureHttpExchange","importSpecifier":"openclaw/plugin-sdk/proxy-capture","kind":"function","recordType":"export"}
@@ -0,0 +1,31 @@
import { Value } from "typebox/value";
import { describe, expect, it } from "vitest";
import { TaskSummarySchema } from "./tasks.js";
describe("TaskSummarySchema", () => {
it("accepts bounded live subagent progress and keeps diff stats closed", () => {
const summary = {
id: "task-1",
status: "running",
lastActivity: "Updating the gateway task ledger",
diffStat: { files: 3, added: 12, removed: 4 },
};
expect(Value.Check(TaskSummarySchema, summary)).toBe(true);
expect(Value.Check(TaskSummarySchema, { ...summary, lastActivity: "x".repeat(201) })).toBe(
false,
);
expect(
Value.Check(TaskSummarySchema, {
...summary,
diffStat: { ...summary.diffStat, removed: -1 },
}),
).toBe(false);
expect(
Value.Check(TaskSummarySchema, {
...summary,
diffStat: { ...summary.diffStat, unchanged: 8 },
}),
).toBe(false);
});
});
@@ -3,6 +3,7 @@ import type { Static } from "typebox";
import { Type } from "typebox";
import { closedObject } from "./closed-object.js";
import { NonEmptyString } from "./primitives.js";
import { withSince } from "./since.js";
/**
* Task ledger protocol schemas.
@@ -31,6 +32,14 @@ const TaskDeliveryStatusSchema = Type.Union([
Type.Literal("not_applicable"),
]);
const TaskTerminalOutcomeSchema = Type.Union([Type.Literal("succeeded"), Type.Literal("blocked")]);
const TaskDiffStatSchema = withSince(
"2026.8",
closedObject({
files: Type.Integer({ minimum: 0 }),
added: Type.Integer({ minimum: 0 }),
removed: Type.Integer({ minimum: 0 }),
}),
);
/** Public task summary returned by task list/get/cancel responses. */
export const TaskSummarySchema = closedObject({
@@ -54,6 +63,8 @@ export const TaskSummarySchema = closedObject({
endedAt: Type.Optional(TimestampSchema),
toolUseCount: Type.Optional(Type.Integer({ minimum: 0 })),
lastToolName: Type.Optional(Type.String()),
lastActivity: Type.Optional(withSince("2026.8", Type.String({ maxLength: 200 }))),
diffStat: Type.Optional(TaskDiffStatSchema),
progressSummary: Type.Optional(Type.String()),
terminalSummary: Type.Optional(Type.String()),
error: Type.Optional(Type.String()),
+13
View File
@@ -6,6 +6,7 @@ import type {
GatewayRequestOptions,
OpenClawEvent,
OpenClawTransport,
TaskSummary,
} from "./types.js";
type RequestCall = {
@@ -561,6 +562,8 @@ describe("OpenClaw SDK", () => {
title: "Investigate issue",
runId: "run_123",
sessionKey: "agent:main:main",
lastActivity: "Editing the registry",
diffStat: { files: 2, added: 12, removed: 3 },
},
],
},
@@ -569,6 +572,8 @@ describe("OpenClaw SDK", () => {
id: "task_123",
status: "running",
title: "Investigate issue",
lastActivity: "Running focused tests",
diffStat: { files: 2, added: 12, removed: 3 },
},
},
"tasks.cancel": {
@@ -592,13 +597,21 @@ describe("OpenClaw SDK", () => {
title: "Investigate issue",
runId: "run_123",
sessionKey: "agent:main:main",
lastActivity: "Editing the registry",
diffStat: { files: 2, added: 12, removed: 3 },
},
]);
const listedTask: TaskSummary | undefined = taskList.tasks[0];
expect(listedTask).toBeDefined();
expect(listedTask?.lastActivity).toBe("Editing the registry");
expect(listedTask?.diffStat).toEqual({ files: 2, added: 12, removed: 3 });
const taskGet = await oc.tasks.get("task_123");
expect(taskGet.task).toEqual({
id: "task_123",
status: "running",
title: "Investigate issue",
lastActivity: "Running focused tests",
diffStat: { files: 2, added: 12, removed: 3 },
});
const taskCancel = await oc.tasks.cancel("task_123", { reason: "user stopped task" });
expect(taskCancel.found).toBe(true);
+2
View File
@@ -195,6 +195,8 @@ export type TaskSummary = {
startedAt?: RunTimestamp;
endedAt?: RunTimestamp;
progressSummary?: string;
lastActivity?: string;
diffStat?: { files: number; added: number; removed: number };
terminalSummary?: string;
error?: string;
};
@@ -1,5 +1,5 @@
// CLI backends report a tool result without repeating the request, so the
// terminal progress event has to carry the args the tool started with.
// Correlated CLI tool results already carry their started args; display-only
// results must not duplicate that potentially large payload.
import { describe, expect, it, vi } from "vitest";
import { type AgentEventRuntimePayload, onAgentEvent } from "../../infra/agent-events.js";
import { createCliEventHandlers } from "./execute-events.js";
@@ -66,7 +66,7 @@ function collectToolEvents(runId: string): {
}
describe("cli tool result events", () => {
it("carries the started args into the terminal event", () => {
it("keeps correlated result args without adding them to display results", () => {
const runId = "run-tool-result-args";
const handlers = createCliEventHandlers({
context: buildContext(runId),
@@ -88,10 +88,32 @@ describe("cli tool result events", () => {
isError: true,
result: "bash: nope-not-a-command: command not found",
});
handlers.emitCliDisplayToolUseStart({
toolCallId: "call-2",
name: "write",
kind: "tool_use",
args: { path: "note.txt", content: "hello" },
});
handlers.emitCliDisplayToolResult({
toolCallId: "call-2",
name: "write",
isError: false,
result: "wrote note.txt",
});
// The display result also releases correlation state for this call id.
handlers.emitCliToolResult({
toolCallId: "call-2",
name: "write",
isError: false,
result: "duplicate terminal",
});
const result = events.find((event) => event.data.phase === "result");
expect(result?.data.args).toEqual({ command: "nope-not-a-command" });
expect(result?.data.isError).toBe(true);
const results = events.filter((event) => event.data.phase === "result");
expect(results[0]?.data.args).toEqual({ command: "nope-not-a-command" });
expect(results[0]?.data.isError).toBe(true);
expect(results[1]?.data.args).toBeUndefined();
expect(results[1]?.data.isError).toBe(false);
expect(results[2]?.data.args).toBeUndefined();
} finally {
dispose();
}
+2 -1
View File
@@ -128,7 +128,7 @@ export function createCliEventHandlers(params: {
toolCallId: event.toolCallId,
isError: event.isError,
result: sanitizeToolResult(event.result),
...(startedArgs ? { args: startedArgs } : {}),
...(startedArgs ? { args: sanitizeToolArgs(startedArgs) } : {}),
...(resultContentSource ? { resultContentSource } : {}),
},
});
@@ -165,6 +165,7 @@ export function createCliEventHandlers(params: {
observedCliActivity = true;
recordToolResult(event);
if (emitLiveEvents) {
toolArgsByCallId.delete(event.toolCallId);
emitAgentEvent({
runId: runParams.runId,
stream: "tool",
+3 -18
View File
@@ -1,12 +1,11 @@
import { parseStreamingJson } from "@openclaw/ai/internal/runtime";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { resolveFileMutationToolName, type FileMutationToolName } from "./tool-mutation-names.js";
const LIVE_EDIT_DIFF_MIN_INTERVAL_MS = 250;
const LIVE_EDIT_DIFF_MAX_PARTIAL_JSON_CHARS = 1024 * 1024;
const LIVE_EDIT_DIFF_MAX_TRACKED_CALLS = 64;
type LiveEditToolKind = "write" | "edit" | "patch";
type LiveEditDiffProgressState = {
added: number;
removed: number;
@@ -21,20 +20,6 @@ type LiveEditDiffProgress = {
diff: { added: number; removed: number };
};
/** Classify the canonical file tools shared by live progress and patch summaries. */
export function resolveLiveEditToolKind(toolName: string): LiveEditToolKind | undefined {
switch (normalizeLowercaseStringOrEmpty(toolName)) {
case "write":
return "write";
case "edit":
return "edit";
case "apply_patch":
return "patch";
default:
return undefined;
}
}
function countNewlines(value: unknown): number {
if (typeof value !== "string") {
return 0;
@@ -80,7 +65,7 @@ function countPatchLines(patch: unknown): { added: number; removed: number } {
}
function countLiveEditDiff(
kind: LiveEditToolKind,
kind: FileMutationToolName,
args: Record<string, unknown>,
): { added: number; removed: number } {
if (kind === "write") {
@@ -146,7 +131,7 @@ export function updateLiveEditDiffProgress(
const block = readToolCallBlock(event);
const toolCallId = typeof block?.id === "string" ? block.id : "";
const name = typeof block?.name === "string" ? block.name : "";
const kind = resolveLiveEditToolKind(name);
const kind = resolveFileMutationToolName(name);
const partialJson = typeof block?.partialJson === "string" ? block.partialJson : "";
if (!toolCallId || !kind || !partialJson) {
return undefined;
@@ -1610,7 +1610,7 @@ describe("handleToolExecutionEnd mutating failure recovery", () => {
});
it("snapshots hook-adjusted args before result middleware can mutate them", async () => {
const { ctx } = createTestContext();
const { ctx, onAgentEvent } = createTestContext();
const toolCallId = "tool-cron-mutable-adjusted-args";
const executedArgs = {
action: "add",
@@ -1636,6 +1636,10 @@ describe("handleToolExecutionEnd mutating failure recovery", () => {
hadPotentialSideEffects: true,
});
expect(ctx.state.successfulCronAdds).toBe(1);
const resultEvent = onAgentEvent.mock.calls.find(
([event]) => event.stream === "tool" && event.data.phase === "result",
)?.[0];
expect(resultEvent?.data).not.toHaveProperty("args");
});
it("uses hook-adjusted message arguments for delivery telemetry", async () => {
@@ -52,7 +52,6 @@ import type { ApplyPatchSummary } from "./apply-patch.js";
import type { ExecToolDetails } from "./bash-tools.exec-types.js";
import { sanitizeForConsole } from "./console-sanitize.js";
import { normalizeTextForComparison } from "./embedded-agent-helpers.js";
import { resolveLiveEditToolKind } from "./embedded-agent-live-edit-diff.js";
import {
isDeliveredMessageToolOnlySourceReplyResult,
isDeliveredMessagingToolResult,
@@ -100,6 +99,7 @@ import {
summarizeToolValidationError,
type ProcessTerminalDiagnostic,
} from "./tool-error-summary.js";
import { resolveFileMutationToolName } from "./tool-mutation-names.js";
import { buildToolMutationState } from "./tool-mutation.js";
import { normalizeToolName } from "./tool-policy.js";
import { readToolResultDetails } from "./tool-result-error.js";
@@ -1269,7 +1269,7 @@ export function handleToolExecutionStart(
toolCallId,
startedAt,
});
} else if (resolveLiveEditToolKind(toolName) === "patch") {
} else if (resolveFileMutationToolName(toolName) === "apply_patch") {
emitTrackedItemEvent(ctx, {
itemId: buildPatchItemId(toolCallId),
phase: "start",
@@ -1942,7 +1942,7 @@ export async function handleToolExecutionEnd(
}
}
if (resolveLiveEditToolKind(toolName) === "patch") {
if (resolveFileMutationToolName(toolName) === "apply_patch") {
const patchSummary = readApplyPatchSummary(sanitizedResult);
const patchItemId = buildPatchItemId(toolCallId);
const summaryText = patchSummary ? buildPatchSummaryText(patchSummary) : undefined;
+12 -3
View File
@@ -4,10 +4,12 @@ import {
LEGACY_AUTOMATIONS_TOOL_NAMES,
} from "./tools/automations-tool-name.js";
export type FileMutationToolName = "write" | "edit" | "apply_patch";
const FILE_MUTATION_TOOL_NAMES = new Set<FileMutationToolName>(["write", "edit", "apply_patch"]);
const MUTATING_TOOL_NAMES = new Set([
"write",
"edit",
"apply_patch",
...FILE_MUTATION_TOOL_NAMES,
"exec",
"bash",
"process",
@@ -30,6 +32,13 @@ const MUTATING_TOOL_NAMES = new Set([
"update_goal",
]);
export function resolveFileMutationToolName(toolName: string): FileMutationToolName | undefined {
const normalized = normalizeLowercaseStringOrEmpty(toolName);
return FILE_MUTATION_TOOL_NAMES.has(normalized as FileMutationToolName)
? (normalized as FileMutationToolName)
: undefined;
}
export function isLikelyMutatingToolName(toolName: string): boolean {
const normalized = normalizeLowercaseStringOrEmpty(toolName);
return Boolean(
@@ -1,6 +1,7 @@
// Public task summaries keep task-registry internals and unbounded status text
// out of gateway responses and events.
import type { TaskSummary } from "../../../packages/gateway-protocol/src/index.js";
import { getTaskActivitySnapshot } from "../../tasks/task-registry-activity.js";
import type { TaskRecord, TaskStatus } from "../../tasks/task-registry.types.js";
import {
TASK_STATUS_DETAIL_MAX_CHARS,
@@ -45,6 +46,8 @@ function sanitizeOptionalTaskText(
}
export function mapTaskSummary(task: TaskRecord, opts?: { includePrompt?: boolean }): TaskSummary {
const activity = getTaskActivitySnapshot(task.taskId);
const lastActivity = sanitizeOptionalTaskText(activity?.lastActivity);
const progressSummary = sanitizeOptionalTaskText(task.progressSummary);
const terminalSummary = sanitizeOptionalTaskText(task.terminalSummary, { errorContext: true });
const error = sanitizeOptionalTaskText(task.error, { errorContext: true });
@@ -80,6 +83,8 @@ export function mapTaskSummary(task: TaskRecord, opts?: { includePrompt?: boolea
...(task.endedAt !== undefined ? { endedAt: task.endedAt } : {}),
...(toolUseCount !== undefined ? { toolUseCount } : {}),
...(lastToolName ? { lastToolName } : {}),
...(lastActivity ? { lastActivity } : {}),
...(activity?.diffStat ? { diffStat: activity.diffStat } : {}),
...(progressSummary ? { progressSummary } : {}),
...(terminalSummary ? { terminalSummary } : {}),
...(error ? { error } : {}),
+148
View File
@@ -444,6 +444,11 @@ describe("tasks gateway handlers", () => {
progressSummary:
"Bundling output\nOpenClaw runtime context (internal): Keep internal details private.",
});
emitAgentEvent({
runId: "run-sanitized",
stream: "assistant",
data: { text: "OpenClaw runtime context (internal): Keep internal details private." },
});
markTaskTerminalById({
taskId: task.taskId,
status: "failed",
@@ -458,6 +463,7 @@ describe("tasks gateway handlers", () => {
expect(payload?.task?.title).toBe("Compile artifact");
expect(payload?.task?.terminalSummary).toBe("Failed after build");
expect(payload?.task?.error).toBe("Tool failed");
expect(payload?.task).not.toHaveProperty("lastActivity");
expect(payload?.task?.prompt).toBe("Compile artifact");
expect(JSON.stringify(calls[0]?.[1])).not.toContain("OpenClaw runtime context");
});
@@ -491,6 +497,148 @@ describe("tasks gateway handlers", () => {
expect(payload?.task?.lastToolName).toBe("exec");
});
it("projects isolated live subagent activity and best-effort diff stats", async () => {
const primary = createTaskRecord({
runtime: "subagent",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
childSessionKey: "agent:main:subagent:primary",
runId: "run-live-primary",
task: "Implement task activity",
status: "running",
deliveryStatus: "not_applicable",
progressSummary: "Milestone remains authoritative",
});
const secondary = createTaskRecord({
runtime: "subagent",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
childSessionKey: "agent:main:subagent:secondary",
runId: "run-live-secondary",
task: "Review task activity",
status: "running",
deliveryStatus: "not_applicable",
});
const longLastLine = `Updating files ${"x".repeat(220)}`;
emitAgentEvent({
runId: primary.runId!,
stream: "thinking",
data: { text: "Inspecting the fold\nThinking fallback" },
});
emitAgentEvent({
runId: secondary.runId!,
stream: "thinking",
data: { text: "Checking isolation\n Thinking-only progress " },
});
emitAgentEvent({
runId: primary.runId!,
stream: "assistant",
data: { text: `Earlier line\n\n${longLastLine}` },
});
emitAgentEvent({
runId: primary.runId!,
stream: "thinking",
data: { text: "Later thinking must not replace assistant activity" },
});
emitAgentEvent({
runId: primary.runId!,
stream: "tool",
data: {
phase: "start",
name: "edit",
toolCallId: "edit-1",
args: {
path: "src/a.ts",
edits: [{ oldText: "one\ntwo", newText: "one\nthree\nfour" }],
},
},
});
emitAgentEvent({
runId: primary.runId!,
stream: "tool",
data: { phase: "result", name: "edit", toolCallId: "edit-1", isError: false },
});
emitAgentEvent({
runId: primary.runId!,
stream: "tool",
data: {
phase: "start",
name: "write",
toolCallId: "write-1",
args: { file_path: "src/b.ts", content: "alpha\nbeta" },
},
});
emitAgentEvent({
runId: primary.runId!,
stream: "tool",
data: { phase: "result", name: "write", toolCallId: "write-1", isError: false },
});
emitAgentEvent({
runId: primary.runId!,
stream: "tool",
data: {
phase: "start",
name: "apply_patch",
toolCallId: "patch-1",
args: {
input: [
"*** Begin Patch",
"*** Update File: src/a.ts",
"@@",
"-old",
"+new",
"+newer",
"*** Delete File: src/c.ts",
"*** End Patch",
].join("\n"),
},
},
});
emitAgentEvent({
runId: primary.runId!,
stream: "tool",
data: { phase: "result", name: "apply_patch", toolCallId: "patch-1", isError: false },
});
emitAgentEvent({
runId: primary.runId!,
stream: "tool",
data: {
phase: "start",
name: "write",
toolCallId: "write-failed",
args: { path: "src/ignored.ts", content: "not\ncounted" },
},
});
emitAgentEvent({
runId: primary.runId!,
stream: "tool",
data: { phase: "result", name: "write", toolCallId: "write-failed", isError: true },
});
const primaryGet = await getTaskPayload(primary.taskId);
const secondaryGet = await getTaskPayload(secondary.taskId);
const listed = await runTaskHandler("tasks.list", {});
const listedPrimary = listed.payload?.tasks?.find((task) => task.id === primary.taskId);
expect(primaryGet.payload?.task?.lastActivity).toMatch(/^Updating files x+…$/);
expect(String(primaryGet.payload?.task?.lastActivity).length).toBeLessThanOrEqual(200);
expect(primaryGet.payload?.task?.diffStat).toEqual({ files: 3, added: 7, removed: 3 });
expect(primaryGet.payload?.task?.progressSummary).toBe("Milestone remains authoritative");
expect(secondaryGet.payload?.task?.lastActivity).toBe("Thinking-only progress");
expect(secondaryGet.payload?.task).not.toHaveProperty("diffStat");
expect(listedPrimary?.lastActivity).toBe(primaryGet.payload?.task?.lastActivity);
expect(listedPrimary?.diffStat).toEqual(primaryGet.payload?.task?.diffStat);
markTaskTerminalById({ taskId: primary.taskId, status: "succeeded", endedAt: Date.now() });
const terminal = await getTaskPayload(primary.taskId);
expect(terminal.payload?.task).not.toHaveProperty("lastActivity");
expect(terminal.payload?.task).not.toHaveProperty("diffStat");
expect(terminal.payload?.task?.progressSummary).toBe("Milestone remains authoritative");
});
it("cancels running task records and returns the updated task", async () => {
const task = createTaskRecord({
runtime: "cli",
@@ -178,6 +178,7 @@ describe("startGatewayEventSubscriptions", () => {
});
afterEach(async () => {
vi.useRealTimers();
await unsubs?.agentUnsub();
unsubs?.heartbeatUnsub();
unsubs?.transcriptUnsub();
@@ -412,6 +413,120 @@ describe("startGatewayEventSubscriptions", () => {
expect(broadcast).not.toHaveBeenCalled();
});
it("throttles live subagent progress per task and flushes before terminal status", async () => {
const broadcast = vi.fn<SubscriptionParams["broadcast"]>();
unsubs = startGatewayEventSubscriptions({ ...createParams(), broadcast });
await waitForFast(() => expect(getTaskRegistryObservers()).not.toBeNull());
vi.useFakeTimers();
vi.setSystemTime(10_000);
const primary = createTaskRecord({
runtime: "subagent",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
childSessionKey: "agent:main:subagent:primary",
runId: "run-throttle-primary",
task: "Implement live progress",
status: "running",
deliveryStatus: "not_applicable",
notifyPolicy: "silent",
});
const secondary = createTaskRecord({
runtime: "subagent",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
childSessionKey: "agent:main:subagent:secondary",
runId: "run-throttle-secondary",
task: "Review live progress",
status: "running",
deliveryStatus: "not_applicable",
notifyPolicy: "silent",
});
if (!primary || !secondary) {
throw new Error("expected task records");
}
broadcast.mockClear();
for (const text of ["first", "second", "third"]) {
emitAgentEvent({
runId: primary.runId!,
stream: "assistant",
data: { text },
});
}
emitAgentEvent({
runId: secondary.runId!,
stream: "thinking",
data: { text: "parallel" },
});
await vi.advanceTimersByTimeAsync(999);
expect(broadcast).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
const firstFlush = broadcast.mock.calls
.filter(([event]) => event === "task")
.map(([, payload]) => payload as TaskEventPayload)
.filter(
(payload): payload is Extract<TaskEventPayload, { action: "upserted" }> =>
payload.action === "upserted",
);
expect(firstFlush).toHaveLength(2);
expect(firstFlush.find((event) => event.task.id === primary.taskId)?.task.lastActivity).toBe(
"third",
);
expect(firstFlush.find((event) => event.task.id === secondary.taskId)?.task.lastActivity).toBe(
"parallel",
);
broadcast.mockClear();
emitAgentEvent({
runId: secondary.runId!,
stream: "assistant",
data: { text: "OpenClaw runtime context (internal): Keep internal details private." },
});
await vi.advanceTimersByTimeAsync(1_000);
const sanitizedActivity = broadcast.mock.calls.find(
([event, payload]) =>
event === "task" &&
(payload as TaskEventPayload).action === "upserted" &&
(payload as Extract<TaskEventPayload, { action: "upserted" }>).task.id === secondary.taskId,
)?.[1] as Extract<TaskEventPayload, { action: "upserted" }> | undefined;
expect(sanitizedActivity?.task).not.toHaveProperty("lastActivity");
expect(JSON.stringify(sanitizedActivity)).not.toContain("OpenClaw runtime context");
broadcast.mockClear();
emitAgentEvent({
runId: primary.runId!,
stream: "assistant",
data: { text: "third" },
});
await vi.advanceTimersByTimeAsync(1_000);
expect(broadcast).not.toHaveBeenCalled();
emitAgentEvent({
runId: primary.runId!,
stream: "assistant",
data: { text: "final activity" },
});
markTaskTerminalById({ taskId: primary.taskId, status: "succeeded", endedAt: Date.now() });
const terminalFlush = broadcast.mock.calls
.filter(([event]) => event === "task")
.map(([, payload]) => payload as TaskEventPayload)
.filter(
(payload): payload is Extract<TaskEventPayload, { action: "upserted" }> =>
payload.action === "upserted" && payload.task.id === primary.taskId,
);
expect(terminalFlush.map((event) => event.task.status)).toEqual(["running", "completed"]);
expect(terminalFlush[0]?.task.lastActivity).toBe("final activity");
expect(terminalFlush[1]?.task).not.toHaveProperty("lastActivity");
broadcast.mockClear();
await vi.advanceTimersByTimeAsync(1_000);
expect(broadcast).not.toHaveBeenCalled();
});
it.each(["succeeded", "failed", "cancelled", "timed_out", "lost"] as const)(
"closes task-run terminals exactly once for a %s transition",
async (status) => {
+4 -1
View File
@@ -590,7 +590,10 @@ export async function markDeliveryPlatformSendDispatched(
producerClaimId: undefined,
platformSendStartedAt: Date.now(),
...(route && "replyToId" in route ? { effectiveReplyToId: route.replyToId ?? null } : {}),
recoveryState: "send_attempt_started",
// A later batch send must not erase concrete evidence from an earlier result;
// recovery could otherwise replay the whole batch and duplicate that delivery.
recoveryState:
entry.recoveryState === "unknown_after_send" ? entry.recoveryState : "send_attempt_started",
}),
expectedPlatformSendAttemptId,
);
+309
View File
@@ -0,0 +1,309 @@
import path from "node:path";
import { asOptionalObjectRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { sliceUtf16Safe, truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { extractApplyPatchTargetPaths } from "../agents/apply-patch-paths.js";
import {
resolveFileMutationToolName,
type FileMutationToolName,
} from "../agents/tool-mutation-names.js";
import type { AgentEventPayload } from "../infra/agent-events.js";
import { isTerminalTaskStatus } from "./task-executor-policy.js";
import { cloneTaskRecord } from "./task-registry-records.js";
import {
emitTaskRegistryObserverEvent,
taskActivityByTaskId,
tasks,
} from "./task-registry-state.js";
import type { TaskActivityOverlayState } from "./task-registry.process-state.js";
import type { TaskRecord } from "./task-registry.types.js";
const MAX_ACTIVITY_CHARS = 200;
const STREAM_TEXT_BUFFER_CHARS = 4_000;
const ACTIVITY_FLUSH_MS = 1_000;
const MAX_PENDING_DIFFS = 64;
type TaskActivitySnapshot = {
lastActivity?: string;
diffStat?: { files: number; added: number; removed: number };
};
type DiffDelta = { files: string[]; added: number; removed: number };
type EditPair = { oldText: string; newText: string };
function activityFor(task: TaskRecord): TaskActivityOverlayState {
const runId = task.runId ?? "";
const existing = taskActivityByTaskId.get(task.taskId);
if (existing?.runId === runId) {
return existing;
}
if (existing?.flushTimer) {
clearTimeout(existing.flushTimer);
}
const created: TaskActivityOverlayState = {
runId,
assistantText: "",
thinkingText: "",
hasAssistantActivity: false,
files: new Set(),
added: 0,
removed: 0,
pendingDiffByToolCallId: new Map(),
dirty: false,
};
taskActivityByTaskId.set(task.taskId, created);
return created;
}
function lastLineSnippet(text: string): string | undefined {
const lines = text.split(/\r\n|\r|\n/);
for (let index = lines.length - 1; index >= 0; index -= 1) {
const line = lines[index]?.replace(/\s+/g, " ").trim();
if (line) {
return truncateUtf16Safe(line, MAX_ACTIVITY_CHARS);
}
}
return undefined;
}
function updateStreamText(
activity: TaskActivityOverlayState,
stream: "assistant" | "thinking",
data: Record<string, unknown>,
): string | undefined {
const key = stream === "assistant" ? "assistantText" : "thinkingText";
let cumulative: string;
if (typeof data.text === "string") {
cumulative = data.text;
} else if (typeof data.delta === "string") {
cumulative = activity[key] + data.delta;
} else {
return undefined;
}
// Retain only a suffix for delta-only producers; full snapshots remain authoritative.
activity[key] = sliceUtf16Safe(cumulative, -STREAM_TEXT_BUFFER_CHARS);
return lastLineSnippet(cumulative);
}
function readString(record: Record<string, unknown>, keys: string[]): string | undefined {
for (const key of keys) {
if (typeof record[key] === "string") {
return record[key];
}
}
return undefined;
}
function readTarget(record: Record<string, unknown>): string | undefined {
const target = normalizeOptionalString(record.path ?? record.file_path ?? record.filePath);
return target ? path.resolve(target) : undefined;
}
// Live progress is intentionally a best-effort count from submitted args, not a post-write diff.
function countLines(text: string): number {
return text.length === 0 ? 0 : text.split(/\r\n|\r|\n/).length;
}
function readEditPairs(args: Record<string, unknown>): EditPair[] {
const candidates = Array.isArray(args.edits) ? args.edits : [args];
const pairs: EditPair[] = [];
for (const candidate of candidates) {
const edit = asOptionalObjectRecord(candidate);
if (!edit) {
continue;
}
const oldText = readString(edit, ["oldText", "old_string"]);
const newText = readString(edit, ["newText", "new_string"]);
if (oldText !== undefined && newText !== undefined) {
pairs.push({ oldText, newText });
}
}
return pairs;
}
function readPatchDelta(args: Record<string, unknown>): DiffDelta | undefined {
if (typeof args.input !== "string") {
return undefined;
}
const files = extractApplyPatchTargetPaths(args);
if (files.length === 0) {
return undefined;
}
let added = 0;
let removed = 0;
let inBody = false;
for (const line of args.input.split(/\r\n|\r|\n/)) {
if (/^\s*\*\*\* (?:Add|Update|Delete) File: /.test(line)) {
inBody = true;
} else if (!/^\s*\*\* /.test(line) && inBody) {
added += Number(line.startsWith("+"));
removed += Number(line.startsWith("-"));
}
}
return { files, added, removed };
}
function readDiffDelta(
kind: FileMutationToolName,
args: Record<string, unknown>,
): DiffDelta | undefined {
if (kind === "apply_patch") {
return readPatchDelta(args);
}
const target = readTarget(args);
if (!target) {
return undefined;
}
if (kind === "write") {
return typeof args.content === "string"
? { files: [target], added: countLines(args.content), removed: 0 }
: undefined;
}
const pairs = readEditPairs(args);
return pairs.length > 0
? {
files: [target],
added: pairs.reduce((total, pair) => total + countLines(pair.newText), 0),
removed: pairs.reduce((total, pair) => total + countLines(pair.oldText), 0),
}
: undefined;
}
function scheduleFlush(taskId: string, activity: TaskActivityOverlayState): void {
if (activity.flushTimer) {
return;
}
const elapsed = activity.lastFlushedAt === undefined ? 0 : Date.now() - activity.lastFlushedAt;
const delay = Math.max(0, ACTIVITY_FLUSH_MS - elapsed);
activity.flushTimer = setTimeout(() => {
activity.flushTimer = undefined;
flushTaskActivity(taskId);
}, delay);
activity.flushTimer.unref?.();
}
function markChanged(taskId: string, activity: TaskActivityOverlayState): void {
activity.dirty = true;
scheduleFlush(taskId, activity);
}
/** Folds streaming-only fields and returns true when durable task mutation should be skipped. */
export function recordTaskActivityEvent(task: TaskRecord, event: AgentEventPayload): boolean {
const textStream =
event.stream === "assistant"
? "assistant"
: event.stream === "thinking"
? "thinking"
: undefined;
if (textStream) {
const activity = activityFor(task);
const snippet = updateStreamText(activity, textStream, event.data);
if (!snippet) {
return true;
}
if (textStream === "assistant") {
activity.hasAssistantActivity = true;
} else if (activity.hasAssistantActivity) {
return true;
}
if (activity.lastActivity !== snippet) {
activity.lastActivity = snippet;
markChanged(task.taskId, activity);
}
return true;
}
if (event.stream !== "tool") {
return false;
}
const toolName = typeof event.data.name === "string" ? event.data.name : "";
const kind = resolveFileMutationToolName(toolName);
if (!kind) {
return false;
}
const toolCallId = normalizeOptionalString(event.data.toolCallId);
if (event.data.phase === "start") {
const args = asOptionalObjectRecord(event.data.args);
const delta = args ? readDiffDelta(kind, args) : undefined;
if (!toolCallId || !delta) {
return false;
}
const activity = activityFor(task);
if (
!activity.pendingDiffByToolCallId.has(toolCallId) &&
activity.pendingDiffByToolCallId.size >= MAX_PENDING_DIFFS
) {
return false;
}
activity.pendingDiffByToolCallId.set(toolCallId, delta);
return false;
}
if (event.data.phase !== "result") {
return false;
}
const activity = taskActivityByTaskId.get(task.taskId);
const delta = toolCallId ? activity?.pendingDiffByToolCallId.get(toolCallId) : undefined;
if (toolCallId) {
activity?.pendingDiffByToolCallId.delete(toolCallId);
}
if (event.data.isError === true || !delta || !activity) {
return event.data.isError !== true;
}
let changed = delta.added > 0 || delta.removed > 0;
for (const file of delta.files) {
const size = activity.files.size;
activity.files.add(file);
changed ||= activity.files.size !== size;
}
if (changed) {
activity.added += delta.added;
activity.removed += delta.removed;
markChanged(task.taskId, activity);
}
return true;
}
export function getTaskActivitySnapshot(taskId: string): TaskActivitySnapshot | undefined {
const activity = taskActivityByTaskId.get(taskId);
return activity
? {
...(activity.lastActivity ? { lastActivity: activity.lastActivity } : {}),
...(activity.files.size > 0
? {
diffStat: {
files: activity.files.size,
added: activity.added,
removed: activity.removed,
},
}
: {}),
}
: undefined;
}
export function flushTaskActivity(taskId: string): void {
const activity = taskActivityByTaskId.get(taskId);
if (!activity?.dirty) {
return;
}
if (activity.flushTimer) {
clearTimeout(activity.flushTimer);
activity.flushTimer = undefined;
}
const task = tasks.get(taskId);
if (!task || isTerminalTaskStatus(task.status)) {
clearTaskActivity(taskId);
return;
}
activity.dirty = false;
activity.lastFlushedAt = Date.now();
emitTaskRegistryObserverEvent(() => ({ kind: "upserted", task: cloneTaskRecord(task) }));
}
export function clearTaskActivity(taskId: string): void {
const activity = taskActivityByTaskId.get(taskId);
if (activity?.flushTimer) {
clearTimeout(activity.flushTimer);
}
taskActivityByTaskId.delete(taskId);
}
+11
View File
@@ -1,6 +1,7 @@
import { buildAgentRunTerminalOutcomeFromLifecycleEvent } from "../agents/agent-run-terminal-outcome.js";
import { onAgentEvent } from "../infra/agent-events.js";
import { isTerminalTaskStatus } from "./task-executor-policy.js";
import { recordTaskActivityEvent } from "./task-registry-activity.js";
import {
appendTaskEvent,
mapAgentRunTerminalOutcomeToTaskStatus,
@@ -20,6 +21,9 @@ import {
} from "./task-registry-state.js";
import type { TaskRecord } from "./task-registry.types.js";
// Keep durable liveness well inside the 30-minute stale-task audit without writing every delta.
const ACTIVITY_LIVENESS_WRITE_MS = 60_000;
function ensureListener() {
if (!claimTaskRegistryListenerStart()) {
return;
@@ -38,6 +42,13 @@ function ensureListener() {
if (isTerminalTaskStatus(current.status)) {
continue;
}
if (recordTaskActivityEvent(current, evt)) {
const lastEventAt = current.lastEventAt ?? current.startedAt ?? current.createdAt;
if (now - lastEventAt >= ACTIVITY_LIVENESS_WRITE_MS) {
updateTask(current.taskId, { lastEventAt: now });
}
continue;
}
const patch: Partial<TaskRecord> = {
lastEventAt: now,
};
+19
View File
@@ -8,6 +8,7 @@ import {
syncFlowFromTaskResult,
updateFlowRecordByIdExpectedRevision,
} from "./task-flow-runtime-internal.js";
import { clearTaskActivity, flushTaskActivity } from "./task-registry-activity.js";
import { ensureLinkedTaskFlowRegistryReady, isTerminalFlowStatus } from "./task-registry-common.js";
import { findLatestTaskForFlowId, listTasksForFlowId } from "./task-registry-query.js";
import {
@@ -177,12 +178,20 @@ export function updateTask(taskId: string, patch: Partial<TaskRecord>): TaskReco
const parentFlowIndexChanged = current.parentFlowId?.trim() !== next.parentFlowId?.trim();
ensureLinkedTaskFlowRegistryReady(current);
ensureLinkedTaskFlowRegistryReady(next);
const becomesTerminal =
!isTerminalTaskStatus(current.status) && isTerminalTaskStatus(next.status);
if (becomesTerminal) {
flushTaskActivity(taskId);
}
// Persist before mutating memory. If the store rejects the write, keep the
// in-memory mirror at the durable value and report that no mutation applied.
if (!tryPersistTaskUpsert(next, "update")) {
return null;
}
tasks.set(taskId, next);
if (becomesTerminal) {
clearTaskActivity(taskId);
}
if (patch.runId && patch.runId !== current.runId) {
rebuildRunIdIndex();
}
@@ -218,12 +227,22 @@ export function updateTask(taskId: string, patch: Partial<TaskRecord>): TaskReco
export function publishTaskRecordAfterAtomicStore(record: TaskRecord): TaskRecord {
const next = normalizeTaskTimestamps(cloneTaskRecord(record));
const current = tasks.get(next.taskId);
const becomesTerminal =
current !== undefined &&
!isTerminalTaskStatus(current.status) &&
isTerminalTaskStatus(next.status);
if (becomesTerminal) {
flushTaskActivity(next.taskId);
}
if (current) {
deleteOwnerKeyIndex(next.taskId, current);
deleteParentFlowIdIndex(next.taskId, current);
deleteRelatedSessionKeyIndex(next.taskId, current);
}
tasks.set(next.taskId, next);
if (becomesTerminal) {
clearTaskActivity(next.taskId);
}
addOwnerKeyIndex(next.taskId, next);
addParentFlowIdIndex(next.taskId, next);
addRelatedSessionKeyIndex(next.taskId, next);
+2
View File
@@ -1,5 +1,6 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { parseAgentSessionKey } from "../routing/session-key.js";
import { clearTaskActivity } from "./task-registry-activity.js";
import { isActiveTaskStatus, ensureLinkedTaskFlowRegistryReady } from "./task-registry-common.js";
import type { TaskRegistryControlRuntime } from "./task-registry-control.types.js";
import { cloneTaskRecord, normalizeTaskTimestamps } from "./task-registry-records.js";
@@ -281,6 +282,7 @@ export function deleteTaskRecordById(taskId: string): boolean {
deleteOwnerKeyIndex(taskId, current);
deleteParentFlowIdIndex(taskId, current);
deleteRelatedSessionKeyIndex(taskId, current);
clearTaskActivity(taskId);
tasks.delete(taskId);
taskDeliveryStates.delete(taskId);
rebuildRunIdIndex();
+7
View File
@@ -29,6 +29,7 @@ export const taskIdsByOwnerKey = taskRegistryProcessState.taskIdsByOwnerKey;
export const taskIdsByParentFlowId = taskRegistryProcessState.taskIdsByParentFlowId;
export const taskIdsByRelatedSessionKey = taskRegistryProcessState.taskIdsByRelatedSessionKey;
export const tasksWithPendingDelivery = taskRegistryProcessState.tasksWithPendingDelivery;
export const taskActivityByTaskId = taskRegistryProcessState.taskActivityByTaskId;
type TaskRegistryRestoreState =
| { status: "uninitialized" }
| { status: "restoring" }
@@ -249,6 +250,12 @@ export function tryPersistTaskDeliveryStateUpsert(state: TaskDeliveryState): boo
export function clearTaskRegistryMemory(): void {
clearTaskFlowSyncRetries();
for (const activity of taskActivityByTaskId.values()) {
if (activity.flushTimer) {
clearTimeout(activity.flushTimer);
}
}
taskActivityByTaskId.clear();
tasks.clear();
taskDeliveryStates.clear();
taskIdsByRunId.clear();
+18
View File
@@ -1,6 +1,21 @@
// Tracks task process state transitions used to reconcile running work.
import type { TaskDeliveryState, TaskRecord } from "./task-registry.types.js";
export type TaskActivityOverlayState = {
runId: string;
assistantText: string;
thinkingText: string;
hasAssistantActivity: boolean;
lastActivity?: string;
files: Set<string>;
added: number;
removed: number;
pendingDiffByToolCallId: Map<string, { files: string[]; added: number; removed: number }>;
dirty: boolean;
lastFlushedAt?: number;
flushTimer?: ReturnType<typeof setTimeout>;
};
/** Process-local indexes backing task lookup, owner access, and pending delivery scans. */
type TaskRegistryProcessState = {
tasks: Map<string, TaskRecord>;
@@ -10,6 +25,8 @@ type TaskRegistryProcessState = {
taskIdsByParentFlowId: Map<string, Set<string>>;
taskIdsByRelatedSessionKey: Map<string, Set<string>>;
tasksWithPendingDelivery: Set<string>;
/** Ephemeral live activity is intentionally discarded on gateway restart. */
taskActivityByTaskId: Map<string, TaskActivityOverlayState>;
// Listener ownership must survive module reloads alongside the task indexes it updates.
listenerStop?: (() => void) | null;
};
@@ -29,6 +46,7 @@ export function getTaskRegistryProcessState(): TaskRegistryProcessState {
taskIdsByParentFlowId: new Map<string, Set<string>>(),
taskIdsByRelatedSessionKey: new Map<string, Set<string>>(),
tasksWithPendingDelivery: new Set<string>(),
taskActivityByTaskId: new Map<string, TaskActivityOverlayState>(),
};
return globalState[TASK_REGISTRY_PROCESS_STATE_KEY];
}
+68
View File
@@ -589,6 +589,74 @@ describe("task-registry", () => {
});
});
it("bounds durable liveness writes for live activity deltas", async () => {
await withTaskRegistryTempDir(async () => {
resetTaskRegistryMemoryForTest();
const store = createInMemoryTaskRegistryStore();
const upsert = vi.spyOn(store, "upsertTaskWithDeliveryState");
configureTaskRegistryRuntime({ store });
createTaskFixture("subagent", {
childSessionKey: "agent:main:subagent:ephemeral",
runId: "run-ephemeral-activity",
task: "Keep streaming state in memory",
});
const initialLastEventAt = requireTaskByRunId("run-ephemeral-activity").lastEventAt!;
upsert.mockClear();
emitAgentEvent({
runId: "run-ephemeral-activity",
stream: "thinking",
data: { text: "Planning" },
});
emitAgentEvent({
runId: "run-ephemeral-activity",
stream: "assistant",
data: { text: "Editing" },
});
expect(upsert).not.toHaveBeenCalled();
const dateNow = vi.spyOn(Date, "now").mockReturnValue(initialLastEventAt + 60_000);
try {
emitAgentEvent({
runId: "run-ephemeral-activity",
stream: "assistant",
data: { text: "Still editing" },
});
} finally {
dateNow.mockRestore();
}
expect(upsert).toHaveBeenCalledOnce();
expect(requireTaskByRunId("run-ephemeral-activity").lastEventAt).toBe(
initialLastEventAt + 60_000,
);
upsert.mockClear();
emitAgentEvent({
runId: "run-ephemeral-activity",
stream: "tool",
data: {
phase: "start",
name: "write",
toolCallId: "write-1",
args: { path: "src/example.ts", content: "one\ntwo" },
},
});
expect(upsert).toHaveBeenCalledOnce();
upsert.mockClear();
emitAgentEvent({
runId: "run-ephemeral-activity",
stream: "tool",
data: { phase: "result", name: "write", toolCallId: "write-1", isError: false },
});
expect(upsert).not.toHaveBeenCalled();
emitAgentEvent({
runId: "run-ephemeral-activity",
stream: "lifecycle",
data: { phase: "end", endedAt: 200 },
});
expect(upsert).toHaveBeenCalledOnce();
});
});
it.each([
{
name: "persists an ACP producer timestamp across lifecycle projection and SQLite reload",