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