diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index 25c9db5f9fa1..ba46316247b9 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -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 diff --git a/docs/.generated/plugin-sdk-api-baseline.jsonl b/docs/.generated/plugin-sdk-api-baseline.jsonl index 33b47283fbe5..a5a37464e8c4 100644 --- a/docs/.generated/plugin-sdk-api-baseline.jsonl +++ b/docs/.generated/plugin-sdk-api-baseline.jsonl @@ -53,7 +53,7 @@ {"closureHash":"ff3c4616cd6212a6d698831a8b287ad87e3968c8663f4090d095bb30ec40fc1c","declaration":"export function abortAndDrainAgentHarnessRun(params: { sessionId: string; sessionKey?: string; settleMs?: number; forceClear?: boolean; reason?: string; }): Promise;","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; };","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>; };","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;","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;\n request>(method: string, params?: unknown, opts?: GatewayClientRequestOptions): Promise;\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;\n request>(method: string, params?: unknown, opts?: GatewayClientRequestOptions): Promise;\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;","entrypoint":"inbound-reply-dispatch","exportName":"resolveInboundReplyDispatchCounts","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"9d503d05e408ddd1a1c373e3c4485b1f9acff4c240fdc241840a2cfa8b55c3a7","declaration":"export function runChannelInboundEvent(params: RunChannelTurnParams): Promise>;\nexport function runChannelInboundEvent(params: ChannelInboundEventRunnerParams): Promise>;","entrypoint":"inbound-reply-dispatch","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"2c68496a174872b31aaf1ac8fad2789deb79d490c5ffbcb157325f1c95d1b51f","declaration":"export function runPreparedInboundReply(params: PreparedChannelTurn): Promise>;","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;","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; };","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; };","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; };","entrypoint":"meeting-runtime","exportName":"createMeetingRealtimeEngineBindings","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"9124db05923060c467edb149bd8abdc8be1d00d00f867cf84600972923a58489","declaration":"export function createMeetingSession(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { provider?: string; voiceProvider?: string; transcriptionProvider?: string; model?: string; toolPolicy: TToolPolicy; }; }; resolved: MeetingResolvedJoin; createdAt: string; }): MeetingSessionRecord;","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; }): 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;","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;","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;","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;","entrypoint":"meeting-runtime","exportName":"joinMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"bac9621cd025754fbcd4c21409b06eb0503405db81f35cac993aaf0b881ab3e1","declaration":"export function leaveMeetingWithBrowser(params: { adapter: BrowserAdapter; 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, Mode extends string, Health extends MeetingBrowserHealth & { browserTitle?: string; browserUrl?: string; notes?: string[]; }, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter; 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;","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;","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;","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; }): Promise;","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;","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; }): Promise;","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 = MeetingSessionRuntimeHandles;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeHandles","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"aa8054fc2c442891197e89bdad61e79999ea15d813c4b127fb6150607b9e67e7","declaration":"export type MeetingSessionRuntimeJoinContext, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab> = MeetingSessionRuntimeJoinContext;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeJoinContext","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"325241724edd38da439c5df03d225e2a7cb8e56d817e98ed7c37850656d3744b","declaration":"export type MeetingSessionRuntimeMessages = MeetingSessionRuntimeMessages;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeMessages","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} -{"closureHash":"d5cfb8ce0591d2c56f9c7d2962a3c103affaa560f066b3a9ec19e96104883458","declaration":"export type MeetingSessionRuntimeOptions, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeOptions","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} +{"closureHash":"6d59e75073bf72818c78595899cbd5be345048a5ac0c852d6a35c2fe9348756b","declaration":"export type MeetingSessionRuntimeOptions, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions;","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 extends MeetingPlatformAdapterContract {\n}","entrypoint":"meeting-runtime","exportName":"MeetingPlatformAdapter","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"} +{"closureHash":"94279205b3342f00dd887290dff6f63279b7edc0ec1ed6e58fff959e41672689","declaration":"export interface MeetingPlatformAdapter extends MeetingPlatformAdapterContract {\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;\n writeOutput(audio: Buffer): Promise;\n clearOutput(): Promise;\n dispose(): Promise;\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, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map;\n readonly #sessionLeaves: Map>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map Promise>;\n readonly #sessionSpeakers: Map void>;\n readonly #sessionHealth: Map Partial>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts;\n readonly #transcriptStore: MeetingSessionTranscriptStore;\n constructor(private readonly options: MeetingSessionRuntimeOptions);\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;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin): 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>;\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;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise;\n async refreshCaptionHealth(session: TSession): Promise;\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): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\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;\n async #rollbackFailedJoinSession(session: TSession): Promise;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles): 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, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map;\n readonly #sessionLeaves: Map>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map Promise>;\n readonly #sessionSpeakers: Map void>;\n readonly #sessionHealth: Map Partial>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts;\n readonly #transcriptStore: MeetingSessionTranscriptStore;\n constructor(private readonly options: MeetingSessionRuntimeOptions);\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;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin): 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>;\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;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise;\n async refreshCaptionHealth(session: TSession): Promise;\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): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\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;\n async #rollbackFailedJoinSession(session: TSession): Promise;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles): 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;","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[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; }): 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[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; }): 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 | undefined; requestBody?: BodyInit | Buffer | string | null; response: Response; transport?: \"http\" | \"sse\"; flowId?: string; meta?: Record; }, resolved?: DebugProxySettings, deps?: DebugProxyCaptureRuntimeDeps): void;","entrypoint":"proxy-capture","exportName":"captureHttpExchange","importSpecifier":"openclaw/plugin-sdk/proxy-capture","kind":"function","recordType":"export"} diff --git a/packages/gateway-protocol/src/schema/tasks.test.ts b/packages/gateway-protocol/src/schema/tasks.test.ts new file mode 100644 index 000000000000..48b7e1a11bb2 --- /dev/null +++ b/packages/gateway-protocol/src/schema/tasks.test.ts @@ -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); + }); +}); diff --git a/packages/gateway-protocol/src/schema/tasks.ts b/packages/gateway-protocol/src/schema/tasks.ts index 8927fbf46cf4..970cc699edff 100644 --- a/packages/gateway-protocol/src/schema/tasks.ts +++ b/packages/gateway-protocol/src/schema/tasks.ts @@ -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()), diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts index 671d98392ac9..cb5034142bdd 100644 --- a/packages/sdk/src/index.test.ts +++ b/packages/sdk/src/index.test.ts @@ -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); diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index d655d9027ebd..95850555d827 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -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; }; diff --git a/src/agents/cli-runner/execute-events.tool-result-args.test.ts b/src/agents/cli-runner/execute-events.tool-result-args.test.ts index 4b4b04e907d4..c200f9f73886 100644 --- a/src/agents/cli-runner/execute-events.tool-result-args.test.ts +++ b/src/agents/cli-runner/execute-events.tool-result-args.test.ts @@ -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(); } diff --git a/src/agents/cli-runner/execute-events.ts b/src/agents/cli-runner/execute-events.ts index ae76b463bf51..1dbdbac840c0 100644 --- a/src/agents/cli-runner/execute-events.ts +++ b/src/agents/cli-runner/execute-events.ts @@ -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", diff --git a/src/agents/embedded-agent-live-edit-diff.ts b/src/agents/embedded-agent-live-edit-diff.ts index 215b5290d18f..7f22ac7ce583 100644 --- a/src/agents/embedded-agent-live-edit-diff.ts +++ b/src/agents/embedded-agent-live-edit-diff.ts @@ -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, ): { 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; diff --git a/src/agents/embedded-agent-subscribe.handlers.tools.test.ts b/src/agents/embedded-agent-subscribe.handlers.tools.test.ts index 6446fa62ea9b..b20b7bca2984 100644 --- a/src/agents/embedded-agent-subscribe.handlers.tools.test.ts +++ b/src/agents/embedded-agent-subscribe.handlers.tools.test.ts @@ -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 () => { diff --git a/src/agents/embedded-agent-subscribe.handlers.tools.ts b/src/agents/embedded-agent-subscribe.handlers.tools.ts index e1c8f482e7d9..3671a61e4145 100644 --- a/src/agents/embedded-agent-subscribe.handlers.tools.ts +++ b/src/agents/embedded-agent-subscribe.handlers.tools.ts @@ -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; diff --git a/src/agents/tool-mutation-names.ts b/src/agents/tool-mutation-names.ts index 7af58679290e..10819a87c375 100644 --- a/src/agents/tool-mutation-names.ts +++ b/src/agents/tool-mutation-names.ts @@ -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(["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( diff --git a/src/gateway/server-methods/task-summary.ts b/src/gateway/server-methods/task-summary.ts index ffa784eaf11c..c674744ccd44 100644 --- a/src/gateway/server-methods/task-summary.ts +++ b/src/gateway/server-methods/task-summary.ts @@ -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 } : {}), diff --git a/src/gateway/server-methods/tasks.test.ts b/src/gateway/server-methods/tasks.test.ts index 9945d8aad909..31b6d35b4105 100644 --- a/src/gateway/server-methods/tasks.test.ts +++ b/src/gateway/server-methods/tasks.test.ts @@ -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", diff --git a/src/gateway/server-runtime-subscriptions.test.ts b/src/gateway/server-runtime-subscriptions.test.ts index bcde13bec6b4..376518a08a6b 100644 --- a/src/gateway/server-runtime-subscriptions.test.ts +++ b/src/gateway/server-runtime-subscriptions.test.ts @@ -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(); + 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 => + 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).task.id === secondary.taskId, + )?.[1] as Extract | 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 => + 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) => { diff --git a/src/infra/outbound/delivery-queue-storage.ts b/src/infra/outbound/delivery-queue-storage.ts index 90cf860fde33..a4c289137e4d 100644 --- a/src/infra/outbound/delivery-queue-storage.ts +++ b/src/infra/outbound/delivery-queue-storage.ts @@ -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, ); diff --git a/src/tasks/task-registry-activity.ts b/src/tasks/task-registry-activity.ts new file mode 100644 index 000000000000..8c32fb181ed1 --- /dev/null +++ b/src/tasks/task-registry-activity.ts @@ -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 | 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, keys: string[]): string | undefined { + for (const key of keys) { + if (typeof record[key] === "string") { + return record[key]; + } + } + return undefined; +} + +function readTarget(record: Record): 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): 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): 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, +): 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); +} diff --git a/src/tasks/task-registry-lifecycle.ts b/src/tasks/task-registry-lifecycle.ts index 31a2fec5a329..b9db25223a08 100644 --- a/src/tasks/task-registry-lifecycle.ts +++ b/src/tasks/task-registry-lifecycle.ts @@ -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 = { lastEventAt: now, }; diff --git a/src/tasks/task-registry-mutation.ts b/src/tasks/task-registry-mutation.ts index b17fc3073892..334c1f6e779d 100644 --- a/src/tasks/task-registry-mutation.ts +++ b/src/tasks/task-registry-mutation.ts @@ -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): 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): 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); diff --git a/src/tasks/task-registry-query.ts b/src/tasks/task-registry-query.ts index 9d4f275812ad..b0030fa8becb 100644 --- a/src/tasks/task-registry-query.ts +++ b/src/tasks/task-registry-query.ts @@ -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(); diff --git a/src/tasks/task-registry-state.ts b/src/tasks/task-registry-state.ts index 4cb423ac4041..dd5ae00f9d09 100644 --- a/src/tasks/task-registry-state.ts +++ b/src/tasks/task-registry-state.ts @@ -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(); diff --git a/src/tasks/task-registry.process-state.ts b/src/tasks/task-registry.process-state.ts index 73428d7dd96f..d2d3a1365978 100644 --- a/src/tasks/task-registry.process-state.ts +++ b/src/tasks/task-registry.process-state.ts @@ -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; + added: number; + removed: number; + pendingDiffByToolCallId: Map; + dirty: boolean; + lastFlushedAt?: number; + flushTimer?: ReturnType; +}; + /** Process-local indexes backing task lookup, owner access, and pending delivery scans. */ type TaskRegistryProcessState = { tasks: Map; @@ -10,6 +25,8 @@ type TaskRegistryProcessState = { taskIdsByParentFlowId: Map>; taskIdsByRelatedSessionKey: Map>; tasksWithPendingDelivery: Set; + /** Ephemeral live activity is intentionally discarded on gateway restart. */ + taskActivityByTaskId: Map; // 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>(), taskIdsByRelatedSessionKey: new Map>(), tasksWithPendingDelivery: new Set(), + taskActivityByTaskId: new Map(), }; return globalState[TASK_REGISTRY_PROCESS_STATE_KEY]; } diff --git a/src/tasks/task-registry.test.ts b/src/tasks/task-registry.test.ts index cb85da236fa6..08951b592298 100644 --- a/src/tasks/task-registry.test.ts +++ b/src/tasks/task-registry.test.ts @@ -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",