From 46179a4ac8eb736b35a1a183cdc6e72b611b5db6 Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Mon, 17 Aug 2026 15:06:41 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(checkpoint):=20=E5=9B=9E=E9=80=80?= =?UTF-8?q?=E7=82=B9=E8=BF=81=E7=A7=BB=E5=88=B0=E7=94=A8=E6=88=B7=E6=B0=94?= =?UTF-8?q?=E6=B3=A1=E8=A1=8C=E5=86=85=E6=8C=89=E9=92=AE=EF=BC=88=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=20Claude=20Code=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 右上角 History 下拉菜单移除,每条用户消息下新增行内 Undo 按钮作为回退点; WebUI 同步获得回退能力(此前 P2 缺口)。 - turnId 改为用户消息 ID:发送管线经 checkpoint_begin_turn 写入 kind:"turn" 边界记录,零文件轮也是合法回退点;行内按钮按 messageRef.messageId 命中该轮 - 共享真源 agent-ui/lib/chat/checkpointRewind.tsx:Provider(确认弹窗/预览/ TOCTOU expected 回传/结果提示)+ useCheckpointRewindAction;回调走 latest-ref 防流式期间全量用户行重渲染,列表加载带 epoch 防慢响应乱序,切会话即清空 - 按钮渲染内聚共享 TranscriptUserMessageActions,调用方只传 rewindTurnId; 回退完成通知文案单一真源 formatCheckpointRewoundNotification(零计数不展示、 NBSP 钉住数字量词、问题项收进括号尾注) - WebUI 通道:proto CheckpointRequest/Response 直通臂(GatewayEnvelope 98 / AgentEnvelope 103),guard vetCheckpoint 按 action 分档校验,桌面端 gateway_bridge 复用同一批 checkpoint 命令 - 桌面传输层 CheckpointRewindMenu.tsx 重命名为 DesktopCheckpointRewindProvider.tsx 测试:Rust checkpoint 42 通过(新增 turn 边界 3 例)、Go guard 通过、 web 587 / GUI 1909 通过、两端 tsc/biome 干净。 Closes #523 --- .../internal/proto/v2/gateway.pb.go | 531 +++++++++++++----- .../internal/protocol/pbws/guard.go | 41 ++ .../internal/protocol/pbws/guard_test.go | 54 ++ crates/agent-gateway/proto/v2/gateway.proto | 20 + .../web/src/app/GatewayAppView.tsx | 128 +++-- .../web/src/components/GatewayTranscript.tsx | 1 + .../web/src/lib/gatewaySocket.ts | 11 + .../web/src/lib/gatewaySocketRpc.ts | 35 ++ .../web/src/lib/gatewaySocketShared.ts | 9 + .../web/src/lib/gatewaySocketV2/adapters.ts | 27 + .../src/lib/proto/gen/proto/v2/gateway_pb.ts | 97 +++- .../web/test/gateway-v2-adapters.test.mjs | 70 +++ .../src/commands/workspace/checkpoint.rs | 283 ++++++++-- crates/agent-gui/src-tauri/src/lib.rs | 1 + .../src/services/gateway/envelope_handler.rs | 13 + .../src-tauri/src/services/gateway_bridge.rs | 65 ++- crates/agent-gui/src/pages/ChatPage.tsx | 108 ++-- .../chat/components/CheckpointRewindMenu.tsx | 332 ----------- .../DesktopCheckpointRewindProvider.tsx | 80 +++ .../src/pages/chat/runtime/useSendChatTurn.ts | 11 + .../src/pages/chat/transcript/RowActions.tsx | 4 +- .../pages/chat/transcript/UserMessageRow.tsx | 1 + .../chat/turns/runAgentConversationTurn.ts | 9 +- .../chat/TranscriptMessageActions.tsx | 24 + .../src/i18n/translations/enUSCommon.ts | 2 + .../src/i18n/translations/zhCNCommon.ts | 5 +- .../src/lib/chat/checkpointRewind.tsx | 403 +++++++++++++ 27 files changed, 1738 insertions(+), 627 deletions(-) delete mode 100644 crates/agent-gui/src/pages/chat/components/CheckpointRewindMenu.tsx create mode 100644 crates/agent-gui/src/pages/chat/components/DesktopCheckpointRewindProvider.tsx create mode 100644 crates/agent-ui/src/lib/chat/checkpointRewind.tsx diff --git a/crates/agent-gateway/internal/proto/v2/gateway.pb.go b/crates/agent-gateway/internal/proto/v2/gateway.pb.go index 22e19ac62..0405e9d4c 100644 --- a/crates/agent-gateway/internal/proto/v2/gateway.pb.go +++ b/crates/agent-gateway/internal/proto/v2/gateway.pb.go @@ -393,6 +393,7 @@ type GatewayEnvelope struct { // *GatewayEnvelope_HistorySetCwd // *GatewayEnvelope_WorkspaceRootGrants // *GatewayEnvelope_ImportDirectory + // *GatewayEnvelope_Checkpoint Payload isGatewayEnvelope_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -917,6 +918,15 @@ func (x *GatewayEnvelope) GetImportDirectory() *ImportDirectoryRequest { return nil } +func (x *GatewayEnvelope) GetCheckpoint() *CheckpointRequest { + if x != nil { + if x, ok := x.Payload.(*GatewayEnvelope_Checkpoint); ok { + return x.Checkpoint + } + } + return nil +} + type isGatewayEnvelope_Payload interface { isGatewayEnvelope_Payload() } @@ -1129,6 +1139,10 @@ type GatewayEnvelope_ImportDirectory struct { ImportDirectory *ImportDirectoryRequest `protobuf:"bytes,97,opt,name=import_directory,json=importDirectory,proto3,oneof"` } +type GatewayEnvelope_Checkpoint struct { + Checkpoint *CheckpointRequest `protobuf:"bytes,98,opt,name=checkpoint,proto3,oneof"` +} + func (*GatewayEnvelope_ChatCommand) isGatewayEnvelope_Payload() {} func (*GatewayEnvelope_CronManage) isGatewayEnvelope_Payload() {} @@ -1233,6 +1247,8 @@ func (*GatewayEnvelope_WorkspaceRootGrants) isGatewayEnvelope_Payload() {} func (*GatewayEnvelope_ImportDirectory) isGatewayEnvelope_Payload() {} +func (*GatewayEnvelope_Checkpoint) isGatewayEnvelope_Payload() {} + type AgentEnvelope struct { state protoimpl.MessageState `protogen:"open.v1"` RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` @@ -1304,6 +1320,7 @@ type AgentEnvelope struct { // *AgentEnvelope_HistorySetCwdResp // *AgentEnvelope_WorkspaceRootGrantsResp // *AgentEnvelope_ImportDirectoryResp + // *AgentEnvelope_CheckpointResp Payload isAgentEnvelope_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1945,6 +1962,15 @@ func (x *AgentEnvelope) GetImportDirectoryResp() *ImportDirectoryResponse { return nil } +func (x *AgentEnvelope) GetCheckpointResp() *CheckpointResponse { + if x != nil { + if x, ok := x.Payload.(*AgentEnvelope_CheckpointResp); ok { + return x.CheckpointResp + } + } + return nil +} + type isAgentEnvelope_Payload interface { isAgentEnvelope_Payload() } @@ -2209,6 +2235,10 @@ type AgentEnvelope_ImportDirectoryResp struct { ImportDirectoryResp *ImportDirectoryResponse `protobuf:"bytes,102,opt,name=import_directory_resp,json=importDirectoryResp,proto3,oneof"` } +type AgentEnvelope_CheckpointResp struct { + CheckpointResp *CheckpointResponse `protobuf:"bytes,103,opt,name=checkpoint_resp,json=checkpointResp,proto3,oneof"` +} + func (*AgentEnvelope_ChatEvent) isAgentEnvelope_Payload() {} func (*AgentEnvelope_CronManageResp) isAgentEnvelope_Payload() {} @@ -2339,6 +2369,8 @@ func (*AgentEnvelope_WorkspaceRootGrantsResp) isAgentEnvelope_Payload() {} func (*AgentEnvelope_ImportDirectoryResp) isAgentEnvelope_Payload() {} +func (*AgentEnvelope_CheckpointResp) isAgentEnvelope_Payload() {} + type ChatSelectedModel struct { state protoimpl.MessageState `protogen:"open.v1"` CustomProviderId string `protobuf:"bytes,1,opt,name=custom_provider_id,json=customProviderId,proto3" json:"custom_provider_id,omitempty"` @@ -13460,11 +13492,191 @@ func (x *WorkspaceRootGrantsResponse) GetGrants() []*WorkspaceRootGrant { return nil } +type CheckpointExpectedEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + CurrentHash string `protobuf:"bytes,2,opt,name=current_hash,json=currentHash,proto3" json:"current_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckpointExpectedEntry) Reset() { + *x = CheckpointExpectedEntry{} + mi := &file_proto_v2_gateway_proto_msgTypes[160] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckpointExpectedEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckpointExpectedEntry) ProtoMessage() {} + +func (x *CheckpointExpectedEntry) ProtoReflect() protoreflect.Message { + mi := &file_proto_v2_gateway_proto_msgTypes[160] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckpointExpectedEntry.ProtoReflect.Descriptor instead. +func (*CheckpointExpectedEntry) Descriptor() ([]byte, []int) { + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{160} +} + +func (x *CheckpointExpectedEntry) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *CheckpointExpectedEntry) GetCurrentHash() string { + if x != nil { + return x.CurrentHash + } + return "" +} + +type CheckpointRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` // "list" | "diff" | "rewind" + ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + TurnSeq uint64 `protobuf:"varint,3,opt,name=turn_seq,json=turnSeq,proto3" json:"turn_seq,omitempty"` + AuthorizedRoots []string `protobuf:"bytes,4,rep,name=authorized_roots,json=authorizedRoots,proto3" json:"authorized_roots,omitempty"` + Expected []*CheckpointExpectedEntry `protobuf:"bytes,5,rep,name=expected,proto3" json:"expected,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckpointRequest) Reset() { + *x = CheckpointRequest{} + mi := &file_proto_v2_gateway_proto_msgTypes[161] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckpointRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckpointRequest) ProtoMessage() {} + +func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_v2_gateway_proto_msgTypes[161] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckpointRequest.ProtoReflect.Descriptor instead. +func (*CheckpointRequest) Descriptor() ([]byte, []int) { + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{161} +} + +func (x *CheckpointRequest) GetAction() string { + if x != nil { + return x.Action + } + return "" +} + +func (x *CheckpointRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *CheckpointRequest) GetTurnSeq() uint64 { + if x != nil { + return x.TurnSeq + } + return 0 +} + +func (x *CheckpointRequest) GetAuthorizedRoots() []string { + if x != nil { + return x.AuthorizedRoots + } + return nil +} + +func (x *CheckpointRequest) GetExpected() []*CheckpointExpectedEntry { + if x != nil { + return x.Expected + } + return nil +} + +type CheckpointResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` + ResultJson string `protobuf:"bytes,2,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckpointResponse) Reset() { + *x = CheckpointResponse{} + mi := &file_proto_v2_gateway_proto_msgTypes[162] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckpointResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckpointResponse) ProtoMessage() {} + +func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_v2_gateway_proto_msgTypes[162] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckpointResponse.ProtoReflect.Descriptor instead. +func (*CheckpointResponse) Descriptor() ([]byte, []int) { + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{162} +} + +func (x *CheckpointResponse) GetAction() string { + if x != nil { + return x.Action + } + return "" +} + +func (x *CheckpointResponse) GetResultJson() string { + if x != nil { + return x.ResultJson + } + return "" +} + var File_proto_v2_gateway_proto protoreflect.FileDescriptor const file_proto_v2_gateway_proto_rawDesc = "" + "\n" + - "\x16proto/v2/gateway.proto\x12\x14liveagent.gateway.v2\"\xcb#\n" + + "\x16proto/v2/gateway.proto\x12\x14liveagent.gateway.v2\"\x96$\n" + "\x0fGatewayEnvelope\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" + @@ -13527,8 +13739,11 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\x0echat_file_open\x18^ \x01(\v2).liveagent.gateway.v2.ChatFileOpenRequestH\x00R\fchatFileOpen\x12T\n" + "\x0fhistory_set_cwd\x18_ \x01(\v2*.liveagent.gateway.v2.HistorySetCwdRequestH\x00R\rhistorySetCwd\x12f\n" + "\x15workspace_root_grants\x18` \x01(\v20.liveagent.gateway.v2.WorkspaceRootGrantsRequestH\x00R\x13workspaceRootGrants\x12Y\n" + - "\x10import_directory\x18a \x01(\v2,.liveagent.gateway.v2.ImportDirectoryRequestH\x00R\x0fimportDirectoryB\t\n" + - "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bJ\x10K\"\xc5/\n" + + "\x10import_directory\x18a \x01(\v2,.liveagent.gateway.v2.ImportDirectoryRequestH\x00R\x0fimportDirectory\x12I\n" + + "\n" + + "checkpoint\x18b \x01(\v2'.liveagent.gateway.v2.CheckpointRequestH\x00R\n" + + "checkpointB\t\n" + + "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bJ\x10K\"\x9a0\n" + "\rAgentEnvelope\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" + @@ -13601,7 +13816,8 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\x05error\x18c \x01(\v2#.liveagent.gateway.v2.ErrorResponseH\x00R\x05error\x12^\n" + "\x14history_set_cwd_resp\x18d \x01(\v2+.liveagent.gateway.v2.HistorySetCwdResponseH\x00R\x11historySetCwdResp\x12p\n" + "\x1aworkspace_root_grants_resp\x18e \x01(\v21.liveagent.gateway.v2.WorkspaceRootGrantsResponseH\x00R\x17workspaceRootGrantsResp\x12c\n" + - "\x15import_directory_resp\x18f \x01(\v2-.liveagent.gateway.v2.ImportDirectoryResponseH\x00R\x13importDirectoryRespB\t\n" + + "\x15import_directory_resp\x18f \x01(\v2-.liveagent.gateway.v2.ImportDirectoryResponseH\x00R\x13importDirectoryResp\x12S\n" + + "\x0fcheckpoint_resp\x18g \x01(\v2(.liveagent.gateway.v2.CheckpointResponseH\x00R\x0echeckpointRespB\t\n" + "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bN\x10O\"|\n" + "\x11ChatSelectedModel\x12,\n" + "\x12custom_provider_id\x18\x01 \x01(\tR\x10customProviderId\x12\x14\n" + @@ -14595,7 +14811,20 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\fproject_path\x18\x03 \x01(\tR\vprojectPath\x12E\n" + "\x06grants\x18\x04 \x03(\v2-.liveagent.gateway.v2.WorkspaceRootGrantDraftR\x06grants\"_\n" + "\x1bWorkspaceRootGrantsResponse\x12@\n" + - "\x06grants\x18\x01 \x03(\v2(.liveagent.gateway.v2.WorkspaceRootGrantR\x06grants*\xe5\x01\n" + + "\x06grants\x18\x01 \x03(\v2(.liveagent.gateway.v2.WorkspaceRootGrantR\x06grants\"N\n" + + "\x17CheckpointExpectedEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12!\n" + + "\fcurrent_hash\x18\x02 \x01(\tR\vcurrentHash\"\xe5\x01\n" + + "\x11CheckpointRequest\x12\x16\n" + + "\x06action\x18\x01 \x01(\tR\x06action\x12'\n" + + "\x0fconversation_id\x18\x02 \x01(\tR\x0econversationId\x12\x19\n" + + "\bturn_seq\x18\x03 \x01(\x04R\aturnSeq\x12)\n" + + "\x10authorized_roots\x18\x04 \x03(\tR\x0fauthorizedRoots\x12I\n" + + "\bexpected\x18\x05 \x03(\v2-.liveagent.gateway.v2.CheckpointExpectedEntryR\bexpected\"M\n" + + "\x12CheckpointResponse\x12\x16\n" + + "\x06action\x18\x01 \x01(\tR\x06action\x12\x1f\n" + + "\vresult_json\x18\x02 \x01(\tR\n" + + "resultJson*\xe5\x01\n" + "\x18ImportDirectoryOperation\x12*\n" + "&IMPORT_DIRECTORY_OPERATION_UNSPECIFIED\x10\x00\x12$\n" + " IMPORT_DIRECTORY_OPERATION_START\x10\x01\x12*\n" + @@ -14638,7 +14867,7 @@ func file_proto_v2_gateway_proto_rawDescGZIP() []byte { } var file_proto_v2_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_proto_v2_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 160) +var file_proto_v2_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 163) var file_proto_v2_gateway_proto_goTypes = []any{ (ImportDirectoryOperation)(0), // 0: liveagent.gateway.v2.ImportDirectoryOperation (TunnelFrameKind)(0), // 1: liveagent.gateway.v2.TunnelFrameKind @@ -14805,6 +15034,9 @@ var file_proto_v2_gateway_proto_goTypes = []any{ (*WorkspaceRootGrant)(nil), // 162: liveagent.gateway.v2.WorkspaceRootGrant (*WorkspaceRootGrantsRequest)(nil), // 163: liveagent.gateway.v2.WorkspaceRootGrantsRequest (*WorkspaceRootGrantsResponse)(nil), // 164: liveagent.gateway.v2.WorkspaceRootGrantsResponse + (*CheckpointExpectedEntry)(nil), // 165: liveagent.gateway.v2.CheckpointExpectedEntry + (*CheckpointRequest)(nil), // 166: liveagent.gateway.v2.CheckpointRequest + (*CheckpointResponse)(nil), // 167: liveagent.gateway.v2.CheckpointResponse } var file_proto_v2_gateway_proto_depIdxs = []int32{ 60, // 0: liveagent.gateway.v2.GatewayEnvelope.chat_command:type_name -> liveagent.gateway.v2.ChatCommandRequest @@ -14859,146 +15091,149 @@ var file_proto_v2_gateway_proto_depIdxs = []int32{ 159, // 49: liveagent.gateway.v2.GatewayEnvelope.history_set_cwd:type_name -> liveagent.gateway.v2.HistorySetCwdRequest 163, // 50: liveagent.gateway.v2.GatewayEnvelope.workspace_root_grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrantsRequest 14, // 51: liveagent.gateway.v2.GatewayEnvelope.import_directory:type_name -> liveagent.gateway.v2.ImportDirectoryRequest - 64, // 52: liveagent.gateway.v2.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v2.ChatEvent - 70, // 53: liveagent.gateway.v2.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v2.CronManageResponse - 72, // 54: liveagent.gateway.v2.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v2.HistoryListResponse - 75, // 55: liveagent.gateway.v2.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v2.HistoryGetResponse - 79, // 56: liveagent.gateway.v2.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v2.HistoryRenameResponse - 95, // 57: liveagent.gateway.v2.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v2.HistoryDeleteResponse - 96, // 58: liveagent.gateway.v2.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v2.HistorySyncEvent - 77, // 59: liveagent.gateway.v2.AgentEnvelope.history_prefix_resp:type_name -> liveagent.gateway.v2.HistoryPrefixResponse - 83, // 60: liveagent.gateway.v2.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v2.HistoryPinResponse - 86, // 61: liveagent.gateway.v2.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v2.HistoryShareGetResponse - 88, // 62: liveagent.gateway.v2.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v2.HistoryShareSetResponse - 90, // 63: liveagent.gateway.v2.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v2.HistoryShareResolveResponse - 93, // 64: liveagent.gateway.v2.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v2.HistoryWorkdirsResponse - 98, // 65: liveagent.gateway.v2.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v2.ProviderListResponse - 100, // 66: liveagent.gateway.v2.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v2.SettingsGetResponse - 102, // 67: liveagent.gateway.v2.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v2.SettingsUpdateResponse - 105, // 68: liveagent.gateway.v2.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v2.SettingsSyncEvent - 107, // 69: liveagent.gateway.v2.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v2.SkillFilesListResponse - 109, // 70: liveagent.gateway.v2.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v2.SkillMetadataReadResponse - 111, // 71: liveagent.gateway.v2.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v2.SkillTextReadResponse - 116, // 72: liveagent.gateway.v2.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v2.FileMentionListResponse - 12, // 73: liveagent.gateway.v2.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v2.UploadReadableFilesResponse - 119, // 74: liveagent.gateway.v2.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v2.FsRootsResponse - 143, // 75: liveagent.gateway.v2.AgentEnvelope.pong:type_name -> liveagent.gateway.v2.PongResponse - 122, // 76: liveagent.gateway.v2.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v2.FsListDirsResponse - 17, // 77: liveagent.gateway.v2.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v2.UploadedImagePreviewResponse - 36, // 78: liveagent.gateway.v2.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v2.MemoryManageResponse - 113, // 79: liveagent.gateway.v2.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v2.SkillManageResponse - 124, // 80: liveagent.gateway.v2.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v2.FsCreateProjectFolderResponse - 52, // 81: liveagent.gateway.v2.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v2.TerminalResponse - 53, // 82: liveagent.gateway.v2.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v2.TerminalEvent - 127, // 83: liveagent.gateway.v2.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v2.FsListResponse - 135, // 84: liveagent.gateway.v2.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v2.FsWriteTextResponse - 137, // 85: liveagent.gateway.v2.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v2.FsCreateDirResponse - 139, // 86: liveagent.gateway.v2.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v2.FsRenameResponse - 141, // 87: liveagent.gateway.v2.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v2.FsDeleteResponse - 56, // 88: liveagent.gateway.v2.AgentEnvelope.git_response:type_name -> liveagent.gateway.v2.GitResponse - 129, // 89: liveagent.gateway.v2.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v2.FsReadEditableTextResponse - 131, // 90: liveagent.gateway.v2.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageResponse - 43, // 91: liveagent.gateway.v2.AgentEnvelope.sftp_response:type_name -> liveagent.gateway.v2.SftpResponse - 44, // 92: liveagent.gateway.v2.AgentEnvelope.sftp_event:type_name -> liveagent.gateway.v2.SftpEvent - 62, // 93: liveagent.gateway.v2.AgentEnvelope.chat_queue_resp:type_name -> liveagent.gateway.v2.ChatQueueResponse - 63, // 94: liveagent.gateway.v2.AgentEnvelope.chat_queue_event:type_name -> liveagent.gateway.v2.ChatQueueEvent - 65, // 95: liveagent.gateway.v2.AgentEnvelope.chat_control:type_name -> liveagent.gateway.v2.ChatControlEvent - 67, // 96: liveagent.gateway.v2.AgentEnvelope.runtime_status:type_name -> liveagent.gateway.v2.RuntimeStatusEvent - 104, // 97: liveagent.gateway.v2.AgentEnvelope.settings_reset_ssh_known_host_resp:type_name -> liveagent.gateway.v2.SettingsResetSshKnownHostResponse - 66, // 98: liveagent.gateway.v2.AgentEnvelope.chat_runtime_snapshot:type_name -> liveagent.gateway.v2.ChatRuntimeSnapshot - 146, // 99: liveagent.gateway.v2.AgentEnvelope.provider_models_resp:type_name -> liveagent.gateway.v2.ProviderModelsResponse - 19, // 100: liveagent.gateway.v2.AgentEnvelope.tunnel_desired:type_name -> liveagent.gateway.v2.TunnelDesiredState - 24, // 101: liveagent.gateway.v2.AgentEnvelope.tunnel_mutation_result:type_name -> liveagent.gateway.v2.TunnelMutationResult - 28, // 102: liveagent.gateway.v2.AgentEnvelope.tunnel_frame:type_name -> liveagent.gateway.v2.TunnelFrame - 26, // 103: liveagent.gateway.v2.AgentEnvelope.tunnel_probe_report:type_name -> liveagent.gateway.v2.TunnelProbeReport - 30, // 104: liveagent.gateway.v2.AgentEnvelope.workspace_activity:type_name -> liveagent.gateway.v2.WorkspaceActivityEvent - 34, // 105: liveagent.gateway.v2.AgentEnvelope.managed_process_response:type_name -> liveagent.gateway.v2.ManagedProcessResponse - 32, // 106: liveagent.gateway.v2.AgentEnvelope.managed_process_snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot - 81, // 107: liveagent.gateway.v2.AgentEnvelope.history_branch_resp:type_name -> liveagent.gateway.v2.HistoryBranchResponse - 148, // 108: liveagent.gateway.v2.AgentEnvelope.provider_usage_resp:type_name -> liveagent.gateway.v2.ProviderUsageResponse - 149, // 109: liveagent.gateway.v2.AgentEnvelope.chat_ingress_batch:type_name -> liveagent.gateway.v2.ChatIngressBatch - 155, // 110: liveagent.gateway.v2.AgentEnvelope.chat_ingress_resume:type_name -> liveagent.gateway.v2.ChatIngressResume - 157, // 111: liveagent.gateway.v2.AgentEnvelope.chat_ingress_fragment:type_name -> liveagent.gateway.v2.ChatIngressFragment - 133, // 112: liveagent.gateway.v2.AgentEnvelope.chat_file_open_resp:type_name -> liveagent.gateway.v2.ChatFileOpenResponse - 144, // 113: liveagent.gateway.v2.AgentEnvelope.error:type_name -> liveagent.gateway.v2.ErrorResponse - 160, // 114: liveagent.gateway.v2.AgentEnvelope.history_set_cwd_resp:type_name -> liveagent.gateway.v2.HistorySetCwdResponse - 164, // 115: liveagent.gateway.v2.AgentEnvelope.workspace_root_grants_resp:type_name -> liveagent.gateway.v2.WorkspaceRootGrantsResponse - 15, // 116: liveagent.gateway.v2.AgentEnvelope.import_directory_resp:type_name -> liveagent.gateway.v2.ImportDirectoryResponse - 10, // 117: liveagent.gateway.v2.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v2.UploadReadableFile - 9, // 118: liveagent.gateway.v2.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v2.ChatUploadedFile - 13, // 119: liveagent.gateway.v2.ImportDirectoryRequest.files:type_name -> liveagent.gateway.v2.ImportDirectoryFile - 0, // 120: liveagent.gateway.v2.ImportDirectoryRequest.operation:type_name -> liveagent.gateway.v2.ImportDirectoryOperation - 18, // 121: liveagent.gateway.v2.TunnelDesiredState.tunnels:type_name -> liveagent.gateway.v2.TunnelSpec - 20, // 122: liveagent.gateway.v2.TunnelStatus.local:type_name -> liveagent.gateway.v2.TunnelHealth - 21, // 123: liveagent.gateway.v2.TunnelStateSnapshot.tunnels:type_name -> liveagent.gateway.v2.TunnelStatus - 20, // 124: liveagent.gateway.v2.TunnelStateSnapshot.relay:type_name -> liveagent.gateway.v2.TunnelHealth - 20, // 125: liveagent.gateway.v2.TunnelProbeResult.local:type_name -> liveagent.gateway.v2.TunnelHealth - 25, // 126: liveagent.gateway.v2.TunnelProbeReport.results:type_name -> liveagent.gateway.v2.TunnelProbeResult - 1, // 127: liveagent.gateway.v2.TunnelFrame.kind:type_name -> liveagent.gateway.v2.TunnelFrameKind - 27, // 128: liveagent.gateway.v2.TunnelFrame.headers:type_name -> liveagent.gateway.v2.TunnelHeader - 2, // 129: liveagent.gateway.v2.TunnelFrame.ws_message_type:type_name -> liveagent.gateway.v2.TunnelWsMessageType - 31, // 130: liveagent.gateway.v2.ManagedProcessSnapshot.processes:type_name -> liveagent.gateway.v2.ManagedProcessRecord - 32, // 131: liveagent.gateway.v2.ManagedProcessResponse.snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot - 39, // 132: liveagent.gateway.v2.TerminalSession.ssh:type_name -> liveagent.gateway.v2.TerminalSshMetadata - 41, // 133: liveagent.gateway.v2.SftpResponse.entries:type_name -> liveagent.gateway.v2.SftpEntry - 41, // 134: liveagent.gateway.v2.SftpResponse.entry:type_name -> liveagent.gateway.v2.SftpEntry - 42, // 135: liveagent.gateway.v2.SftpResponse.transfer:type_name -> liveagent.gateway.v2.SftpTransfer - 42, // 136: liveagent.gateway.v2.SftpEvent.transfer:type_name -> liveagent.gateway.v2.SftpTransfer - 47, // 137: liveagent.gateway.v2.TerminalSshTabsSnapshot.tabs:type_name -> liveagent.gateway.v2.TerminalSshTab - 49, // 138: liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot.forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForward - 49, // 139: liveagent.gateway.v2.TerminalSshLocalForwardAction.forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForward - 38, // 140: liveagent.gateway.v2.TerminalResponse.sessions:type_name -> liveagent.gateway.v2.TerminalSession - 38, // 141: liveagent.gateway.v2.TerminalResponse.session:type_name -> liveagent.gateway.v2.TerminalSession - 46, // 142: liveagent.gateway.v2.TerminalResponse.shell_options:type_name -> liveagent.gateway.v2.TerminalShellOption - 45, // 143: liveagent.gateway.v2.TerminalResponse.ssh_prompt:type_name -> liveagent.gateway.v2.TerminalSshPrompt - 48, // 144: liveagent.gateway.v2.TerminalResponse.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot - 50, // 145: liveagent.gateway.v2.TerminalResponse.ssh_local_forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot - 51, // 146: liveagent.gateway.v2.TerminalResponse.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction - 38, // 147: liveagent.gateway.v2.TerminalEvent.session:type_name -> liveagent.gateway.v2.TerminalSession - 48, // 148: liveagent.gateway.v2.TerminalEvent.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot - 51, // 149: liveagent.gateway.v2.TerminalEvent.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction - 38, // 150: liveagent.gateway.v2.TerminalStreamFrame.session:type_name -> liveagent.gateway.v2.TerminalSession - 7, // 151: liveagent.gateway.v2.ChatRequest.selected_model:type_name -> liveagent.gateway.v2.ChatSelectedModel - 9, // 152: liveagent.gateway.v2.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v2.ChatUploadedFile - 8, // 153: liveagent.gateway.v2.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v2.ChatRuntimeControls - 57, // 154: liveagent.gateway.v2.ChatCommandRequest.request:type_name -> liveagent.gateway.v2.ChatRequest - 58, // 155: liveagent.gateway.v2.ChatCommandRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 59, // 156: liveagent.gateway.v2.ChatCommandRequest.cancel:type_name -> liveagent.gateway.v2.CancelChatRequest - 3, // 157: liveagent.gateway.v2.ChatEvent.type:type_name -> liveagent.gateway.v2.ChatEvent.ChatEventType - 68, // 158: liveagent.gateway.v2.RuntimeStatusEvent.active_runs:type_name -> liveagent.gateway.v2.ChatRunReport - 68, // 159: liveagent.gateway.v2.RuntimeStatusEvent.finished_runs:type_name -> liveagent.gateway.v2.ChatRunReport - 73, // 160: liveagent.gateway.v2.HistoryListResponse.conversations:type_name -> liveagent.gateway.v2.ConversationSummary - 73, // 161: liveagent.gateway.v2.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 58, // 162: liveagent.gateway.v2.HistoryPrefixRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 73, // 163: liveagent.gateway.v2.HistoryPrefixResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 73, // 164: liveagent.gateway.v2.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 58, // 165: liveagent.gateway.v2.HistoryBranchRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 73, // 166: liveagent.gateway.v2.HistoryBranchResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 73, // 167: liveagent.gateway.v2.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 84, // 168: liveagent.gateway.v2.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus - 84, // 169: liveagent.gateway.v2.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus - 73, // 170: liveagent.gateway.v2.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 92, // 171: liveagent.gateway.v2.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v2.HistoryWorkdirSummary - 73, // 172: liveagent.gateway.v2.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 115, // 173: liveagent.gateway.v2.FileMentionListResponse.entries:type_name -> liveagent.gateway.v2.FileMentionEntry - 117, // 174: liveagent.gateway.v2.FsRootsResponse.roots:type_name -> liveagent.gateway.v2.FsRoot - 121, // 175: liveagent.gateway.v2.FsListDirsResponse.entries:type_name -> liveagent.gateway.v2.FsDirEntry - 126, // 176: liveagent.gateway.v2.FsListResponse.entries:type_name -> liveagent.gateway.v2.FsListEntry - 150, // 177: liveagent.gateway.v2.ChatIngressBatch.records:type_name -> liveagent.gateway.v2.ChatIngressRecord - 151, // 178: liveagent.gateway.v2.ChatIngressRecord.delta:type_name -> liveagent.gateway.v2.ChatIngressDelta - 153, // 179: liveagent.gateway.v2.ChatIngressRecord.checkpoint:type_name -> liveagent.gateway.v2.ChatIngressCheckpoint - 154, // 180: liveagent.gateway.v2.ChatIngressRecord.terminal:type_name -> liveagent.gateway.v2.ChatIngressTerminal - 152, // 181: liveagent.gateway.v2.ChatIngressRecord.heartbeat:type_name -> liveagent.gateway.v2.ChatIngressHeartbeat - 156, // 182: liveagent.gateway.v2.ChatIngressResume.runs:type_name -> liveagent.gateway.v2.ChatIngressRunResume - 4, // 183: liveagent.gateway.v2.ChatIngressAck.action:type_name -> liveagent.gateway.v2.ChatIngressAck.Action - 73, // 184: liveagent.gateway.v2.HistorySetCwdResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 161, // 185: liveagent.gateway.v2.WorkspaceRootGrantsRequest.grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrantDraft - 162, // 186: liveagent.gateway.v2.WorkspaceRootGrantsResponse.grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrant - 187, // [187:187] is the sub-list for method output_type - 187, // [187:187] is the sub-list for method input_type - 187, // [187:187] is the sub-list for extension type_name - 187, // [187:187] is the sub-list for extension extendee - 0, // [0:187] is the sub-list for field type_name + 166, // 52: liveagent.gateway.v2.GatewayEnvelope.checkpoint:type_name -> liveagent.gateway.v2.CheckpointRequest + 64, // 53: liveagent.gateway.v2.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v2.ChatEvent + 70, // 54: liveagent.gateway.v2.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v2.CronManageResponse + 72, // 55: liveagent.gateway.v2.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v2.HistoryListResponse + 75, // 56: liveagent.gateway.v2.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v2.HistoryGetResponse + 79, // 57: liveagent.gateway.v2.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v2.HistoryRenameResponse + 95, // 58: liveagent.gateway.v2.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v2.HistoryDeleteResponse + 96, // 59: liveagent.gateway.v2.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v2.HistorySyncEvent + 77, // 60: liveagent.gateway.v2.AgentEnvelope.history_prefix_resp:type_name -> liveagent.gateway.v2.HistoryPrefixResponse + 83, // 61: liveagent.gateway.v2.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v2.HistoryPinResponse + 86, // 62: liveagent.gateway.v2.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v2.HistoryShareGetResponse + 88, // 63: liveagent.gateway.v2.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v2.HistoryShareSetResponse + 90, // 64: liveagent.gateway.v2.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v2.HistoryShareResolveResponse + 93, // 65: liveagent.gateway.v2.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v2.HistoryWorkdirsResponse + 98, // 66: liveagent.gateway.v2.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v2.ProviderListResponse + 100, // 67: liveagent.gateway.v2.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v2.SettingsGetResponse + 102, // 68: liveagent.gateway.v2.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v2.SettingsUpdateResponse + 105, // 69: liveagent.gateway.v2.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v2.SettingsSyncEvent + 107, // 70: liveagent.gateway.v2.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v2.SkillFilesListResponse + 109, // 71: liveagent.gateway.v2.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v2.SkillMetadataReadResponse + 111, // 72: liveagent.gateway.v2.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v2.SkillTextReadResponse + 116, // 73: liveagent.gateway.v2.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v2.FileMentionListResponse + 12, // 74: liveagent.gateway.v2.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v2.UploadReadableFilesResponse + 119, // 75: liveagent.gateway.v2.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v2.FsRootsResponse + 143, // 76: liveagent.gateway.v2.AgentEnvelope.pong:type_name -> liveagent.gateway.v2.PongResponse + 122, // 77: liveagent.gateway.v2.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v2.FsListDirsResponse + 17, // 78: liveagent.gateway.v2.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v2.UploadedImagePreviewResponse + 36, // 79: liveagent.gateway.v2.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v2.MemoryManageResponse + 113, // 80: liveagent.gateway.v2.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v2.SkillManageResponse + 124, // 81: liveagent.gateway.v2.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v2.FsCreateProjectFolderResponse + 52, // 82: liveagent.gateway.v2.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v2.TerminalResponse + 53, // 83: liveagent.gateway.v2.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v2.TerminalEvent + 127, // 84: liveagent.gateway.v2.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v2.FsListResponse + 135, // 85: liveagent.gateway.v2.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v2.FsWriteTextResponse + 137, // 86: liveagent.gateway.v2.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v2.FsCreateDirResponse + 139, // 87: liveagent.gateway.v2.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v2.FsRenameResponse + 141, // 88: liveagent.gateway.v2.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v2.FsDeleteResponse + 56, // 89: liveagent.gateway.v2.AgentEnvelope.git_response:type_name -> liveagent.gateway.v2.GitResponse + 129, // 90: liveagent.gateway.v2.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v2.FsReadEditableTextResponse + 131, // 91: liveagent.gateway.v2.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageResponse + 43, // 92: liveagent.gateway.v2.AgentEnvelope.sftp_response:type_name -> liveagent.gateway.v2.SftpResponse + 44, // 93: liveagent.gateway.v2.AgentEnvelope.sftp_event:type_name -> liveagent.gateway.v2.SftpEvent + 62, // 94: liveagent.gateway.v2.AgentEnvelope.chat_queue_resp:type_name -> liveagent.gateway.v2.ChatQueueResponse + 63, // 95: liveagent.gateway.v2.AgentEnvelope.chat_queue_event:type_name -> liveagent.gateway.v2.ChatQueueEvent + 65, // 96: liveagent.gateway.v2.AgentEnvelope.chat_control:type_name -> liveagent.gateway.v2.ChatControlEvent + 67, // 97: liveagent.gateway.v2.AgentEnvelope.runtime_status:type_name -> liveagent.gateway.v2.RuntimeStatusEvent + 104, // 98: liveagent.gateway.v2.AgentEnvelope.settings_reset_ssh_known_host_resp:type_name -> liveagent.gateway.v2.SettingsResetSshKnownHostResponse + 66, // 99: liveagent.gateway.v2.AgentEnvelope.chat_runtime_snapshot:type_name -> liveagent.gateway.v2.ChatRuntimeSnapshot + 146, // 100: liveagent.gateway.v2.AgentEnvelope.provider_models_resp:type_name -> liveagent.gateway.v2.ProviderModelsResponse + 19, // 101: liveagent.gateway.v2.AgentEnvelope.tunnel_desired:type_name -> liveagent.gateway.v2.TunnelDesiredState + 24, // 102: liveagent.gateway.v2.AgentEnvelope.tunnel_mutation_result:type_name -> liveagent.gateway.v2.TunnelMutationResult + 28, // 103: liveagent.gateway.v2.AgentEnvelope.tunnel_frame:type_name -> liveagent.gateway.v2.TunnelFrame + 26, // 104: liveagent.gateway.v2.AgentEnvelope.tunnel_probe_report:type_name -> liveagent.gateway.v2.TunnelProbeReport + 30, // 105: liveagent.gateway.v2.AgentEnvelope.workspace_activity:type_name -> liveagent.gateway.v2.WorkspaceActivityEvent + 34, // 106: liveagent.gateway.v2.AgentEnvelope.managed_process_response:type_name -> liveagent.gateway.v2.ManagedProcessResponse + 32, // 107: liveagent.gateway.v2.AgentEnvelope.managed_process_snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot + 81, // 108: liveagent.gateway.v2.AgentEnvelope.history_branch_resp:type_name -> liveagent.gateway.v2.HistoryBranchResponse + 148, // 109: liveagent.gateway.v2.AgentEnvelope.provider_usage_resp:type_name -> liveagent.gateway.v2.ProviderUsageResponse + 149, // 110: liveagent.gateway.v2.AgentEnvelope.chat_ingress_batch:type_name -> liveagent.gateway.v2.ChatIngressBatch + 155, // 111: liveagent.gateway.v2.AgentEnvelope.chat_ingress_resume:type_name -> liveagent.gateway.v2.ChatIngressResume + 157, // 112: liveagent.gateway.v2.AgentEnvelope.chat_ingress_fragment:type_name -> liveagent.gateway.v2.ChatIngressFragment + 133, // 113: liveagent.gateway.v2.AgentEnvelope.chat_file_open_resp:type_name -> liveagent.gateway.v2.ChatFileOpenResponse + 144, // 114: liveagent.gateway.v2.AgentEnvelope.error:type_name -> liveagent.gateway.v2.ErrorResponse + 160, // 115: liveagent.gateway.v2.AgentEnvelope.history_set_cwd_resp:type_name -> liveagent.gateway.v2.HistorySetCwdResponse + 164, // 116: liveagent.gateway.v2.AgentEnvelope.workspace_root_grants_resp:type_name -> liveagent.gateway.v2.WorkspaceRootGrantsResponse + 15, // 117: liveagent.gateway.v2.AgentEnvelope.import_directory_resp:type_name -> liveagent.gateway.v2.ImportDirectoryResponse + 167, // 118: liveagent.gateway.v2.AgentEnvelope.checkpoint_resp:type_name -> liveagent.gateway.v2.CheckpointResponse + 10, // 119: liveagent.gateway.v2.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v2.UploadReadableFile + 9, // 120: liveagent.gateway.v2.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v2.ChatUploadedFile + 13, // 121: liveagent.gateway.v2.ImportDirectoryRequest.files:type_name -> liveagent.gateway.v2.ImportDirectoryFile + 0, // 122: liveagent.gateway.v2.ImportDirectoryRequest.operation:type_name -> liveagent.gateway.v2.ImportDirectoryOperation + 18, // 123: liveagent.gateway.v2.TunnelDesiredState.tunnels:type_name -> liveagent.gateway.v2.TunnelSpec + 20, // 124: liveagent.gateway.v2.TunnelStatus.local:type_name -> liveagent.gateway.v2.TunnelHealth + 21, // 125: liveagent.gateway.v2.TunnelStateSnapshot.tunnels:type_name -> liveagent.gateway.v2.TunnelStatus + 20, // 126: liveagent.gateway.v2.TunnelStateSnapshot.relay:type_name -> liveagent.gateway.v2.TunnelHealth + 20, // 127: liveagent.gateway.v2.TunnelProbeResult.local:type_name -> liveagent.gateway.v2.TunnelHealth + 25, // 128: liveagent.gateway.v2.TunnelProbeReport.results:type_name -> liveagent.gateway.v2.TunnelProbeResult + 1, // 129: liveagent.gateway.v2.TunnelFrame.kind:type_name -> liveagent.gateway.v2.TunnelFrameKind + 27, // 130: liveagent.gateway.v2.TunnelFrame.headers:type_name -> liveagent.gateway.v2.TunnelHeader + 2, // 131: liveagent.gateway.v2.TunnelFrame.ws_message_type:type_name -> liveagent.gateway.v2.TunnelWsMessageType + 31, // 132: liveagent.gateway.v2.ManagedProcessSnapshot.processes:type_name -> liveagent.gateway.v2.ManagedProcessRecord + 32, // 133: liveagent.gateway.v2.ManagedProcessResponse.snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot + 39, // 134: liveagent.gateway.v2.TerminalSession.ssh:type_name -> liveagent.gateway.v2.TerminalSshMetadata + 41, // 135: liveagent.gateway.v2.SftpResponse.entries:type_name -> liveagent.gateway.v2.SftpEntry + 41, // 136: liveagent.gateway.v2.SftpResponse.entry:type_name -> liveagent.gateway.v2.SftpEntry + 42, // 137: liveagent.gateway.v2.SftpResponse.transfer:type_name -> liveagent.gateway.v2.SftpTransfer + 42, // 138: liveagent.gateway.v2.SftpEvent.transfer:type_name -> liveagent.gateway.v2.SftpTransfer + 47, // 139: liveagent.gateway.v2.TerminalSshTabsSnapshot.tabs:type_name -> liveagent.gateway.v2.TerminalSshTab + 49, // 140: liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot.forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForward + 49, // 141: liveagent.gateway.v2.TerminalSshLocalForwardAction.forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForward + 38, // 142: liveagent.gateway.v2.TerminalResponse.sessions:type_name -> liveagent.gateway.v2.TerminalSession + 38, // 143: liveagent.gateway.v2.TerminalResponse.session:type_name -> liveagent.gateway.v2.TerminalSession + 46, // 144: liveagent.gateway.v2.TerminalResponse.shell_options:type_name -> liveagent.gateway.v2.TerminalShellOption + 45, // 145: liveagent.gateway.v2.TerminalResponse.ssh_prompt:type_name -> liveagent.gateway.v2.TerminalSshPrompt + 48, // 146: liveagent.gateway.v2.TerminalResponse.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot + 50, // 147: liveagent.gateway.v2.TerminalResponse.ssh_local_forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot + 51, // 148: liveagent.gateway.v2.TerminalResponse.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction + 38, // 149: liveagent.gateway.v2.TerminalEvent.session:type_name -> liveagent.gateway.v2.TerminalSession + 48, // 150: liveagent.gateway.v2.TerminalEvent.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot + 51, // 151: liveagent.gateway.v2.TerminalEvent.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction + 38, // 152: liveagent.gateway.v2.TerminalStreamFrame.session:type_name -> liveagent.gateway.v2.TerminalSession + 7, // 153: liveagent.gateway.v2.ChatRequest.selected_model:type_name -> liveagent.gateway.v2.ChatSelectedModel + 9, // 154: liveagent.gateway.v2.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v2.ChatUploadedFile + 8, // 155: liveagent.gateway.v2.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v2.ChatRuntimeControls + 57, // 156: liveagent.gateway.v2.ChatCommandRequest.request:type_name -> liveagent.gateway.v2.ChatRequest + 58, // 157: liveagent.gateway.v2.ChatCommandRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef + 59, // 158: liveagent.gateway.v2.ChatCommandRequest.cancel:type_name -> liveagent.gateway.v2.CancelChatRequest + 3, // 159: liveagent.gateway.v2.ChatEvent.type:type_name -> liveagent.gateway.v2.ChatEvent.ChatEventType + 68, // 160: liveagent.gateway.v2.RuntimeStatusEvent.active_runs:type_name -> liveagent.gateway.v2.ChatRunReport + 68, // 161: liveagent.gateway.v2.RuntimeStatusEvent.finished_runs:type_name -> liveagent.gateway.v2.ChatRunReport + 73, // 162: liveagent.gateway.v2.HistoryListResponse.conversations:type_name -> liveagent.gateway.v2.ConversationSummary + 73, // 163: liveagent.gateway.v2.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 58, // 164: liveagent.gateway.v2.HistoryPrefixRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef + 73, // 165: liveagent.gateway.v2.HistoryPrefixResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 73, // 166: liveagent.gateway.v2.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 58, // 167: liveagent.gateway.v2.HistoryBranchRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef + 73, // 168: liveagent.gateway.v2.HistoryBranchResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 73, // 169: liveagent.gateway.v2.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 84, // 170: liveagent.gateway.v2.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus + 84, // 171: liveagent.gateway.v2.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus + 73, // 172: liveagent.gateway.v2.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 92, // 173: liveagent.gateway.v2.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v2.HistoryWorkdirSummary + 73, // 174: liveagent.gateway.v2.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 115, // 175: liveagent.gateway.v2.FileMentionListResponse.entries:type_name -> liveagent.gateway.v2.FileMentionEntry + 117, // 176: liveagent.gateway.v2.FsRootsResponse.roots:type_name -> liveagent.gateway.v2.FsRoot + 121, // 177: liveagent.gateway.v2.FsListDirsResponse.entries:type_name -> liveagent.gateway.v2.FsDirEntry + 126, // 178: liveagent.gateway.v2.FsListResponse.entries:type_name -> liveagent.gateway.v2.FsListEntry + 150, // 179: liveagent.gateway.v2.ChatIngressBatch.records:type_name -> liveagent.gateway.v2.ChatIngressRecord + 151, // 180: liveagent.gateway.v2.ChatIngressRecord.delta:type_name -> liveagent.gateway.v2.ChatIngressDelta + 153, // 181: liveagent.gateway.v2.ChatIngressRecord.checkpoint:type_name -> liveagent.gateway.v2.ChatIngressCheckpoint + 154, // 182: liveagent.gateway.v2.ChatIngressRecord.terminal:type_name -> liveagent.gateway.v2.ChatIngressTerminal + 152, // 183: liveagent.gateway.v2.ChatIngressRecord.heartbeat:type_name -> liveagent.gateway.v2.ChatIngressHeartbeat + 156, // 184: liveagent.gateway.v2.ChatIngressResume.runs:type_name -> liveagent.gateway.v2.ChatIngressRunResume + 4, // 185: liveagent.gateway.v2.ChatIngressAck.action:type_name -> liveagent.gateway.v2.ChatIngressAck.Action + 73, // 186: liveagent.gateway.v2.HistorySetCwdResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 161, // 187: liveagent.gateway.v2.WorkspaceRootGrantsRequest.grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrantDraft + 162, // 188: liveagent.gateway.v2.WorkspaceRootGrantsResponse.grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrant + 165, // 189: liveagent.gateway.v2.CheckpointRequest.expected:type_name -> liveagent.gateway.v2.CheckpointExpectedEntry + 190, // [190:190] is the sub-list for method output_type + 190, // [190:190] is the sub-list for method input_type + 190, // [190:190] is the sub-list for extension type_name + 190, // [190:190] is the sub-list for extension extendee + 0, // [0:190] is the sub-list for field type_name } func init() { file_proto_v2_gateway_proto_init() } @@ -15059,6 +15294,7 @@ func file_proto_v2_gateway_proto_init() { (*GatewayEnvelope_HistorySetCwd)(nil), (*GatewayEnvelope_WorkspaceRootGrants)(nil), (*GatewayEnvelope_ImportDirectory)(nil), + (*GatewayEnvelope_Checkpoint)(nil), } file_proto_v2_gateway_proto_msgTypes[1].OneofWrappers = []any{ (*AgentEnvelope_ChatEvent)(nil), @@ -15126,6 +15362,7 @@ func file_proto_v2_gateway_proto_init() { (*AgentEnvelope_HistorySetCwdResp)(nil), (*AgentEnvelope_WorkspaceRootGrantsResp)(nil), (*AgentEnvelope_ImportDirectoryResp)(nil), + (*AgentEnvelope_CheckpointResp)(nil), } file_proto_v2_gateway_proto_msgTypes[18].OneofWrappers = []any{} file_proto_v2_gateway_proto_msgTypes[26].OneofWrappers = []any{} @@ -15148,7 +15385,7 @@ func file_proto_v2_gateway_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_v2_gateway_proto_rawDesc), len(file_proto_v2_gateway_proto_rawDesc)), NumEnums: 5, - NumMessages: 160, + NumMessages: 163, NumExtensions: 0, NumServices: 0, }, diff --git a/crates/agent-gateway/internal/protocol/pbws/guard.go b/crates/agent-gateway/internal/protocol/pbws/guard.go index 15bbfb24c..0abc1a231 100644 --- a/crates/agent-gateway/internal/protocol/pbws/guard.go +++ b/crates/agent-gateway/internal/protocol/pbws/guard.go @@ -70,6 +70,8 @@ func vetAgentRequest(sm session.AgentView, env *gatewayv2.GatewayEnvelope) error return vetChatFileOpen(payload.ChatFileOpen) case *gatewayv2.GatewayEnvelope_WorkspaceRootGrants: return vetWorkspaceRootGrants(payload.WorkspaceRootGrants) + case *gatewayv2.GatewayEnvelope_Checkpoint: + return vetCheckpoint(payload.Checkpoint) // ---- 带功能门控 / 限额的直通臂 ---- case *gatewayv2.GatewayEnvelope_GitRequest: @@ -111,6 +113,45 @@ func vetAgentRequest(sm session.AgentView, env *gatewayv2.GatewayEnvelope) error } } +func vetCheckpoint(req *gatewayv2.CheckpointRequest) error { + if req == nil || strings.TrimSpace(req.GetConversationId()) == "" || len(req.GetConversationId()) > 256 { + return errors.New("checkpoint conversation_id is invalid") + } + switch strings.TrimSpace(req.GetAction()) { + case "list": + if req.GetTurnSeq() != 0 || len(req.GetAuthorizedRoots()) != 0 || len(req.GetExpected()) != 0 { + return errors.New("checkpoint list payload is invalid") + } + case "diff": + if req.GetTurnSeq() == 0 || len(req.GetExpected()) != 0 { + return errors.New("checkpoint diff payload is invalid") + } + case "rewind": + if req.GetTurnSeq() == 0 { + return errors.New("checkpoint rewind turn_seq is required") + } + default: + return errors.New("checkpoint action is invalid") + } + if len(req.GetAuthorizedRoots()) > 64 { + return errors.New("too many checkpoint authorized roots") + } + for _, root := range req.GetAuthorizedRoots() { + if strings.TrimSpace(root) == "" || len(root) > 32768 { + return errors.New("checkpoint authorized root is invalid") + } + } + if len(req.GetExpected()) > 10_000 { + return errors.New("too many checkpoint expected entries") + } + for _, entry := range req.GetExpected() { + if entry == nil || strings.TrimSpace(entry.GetKey()) == "" || len(entry.GetKey()) > 65536 || len(entry.GetCurrentHash()) > 256 { + return errors.New("checkpoint expected entry is invalid") + } + } + return nil +} + func vetWorkspaceRootGrants(req *gatewayv2.WorkspaceRootGrantsRequest) error { if req == nil { return errors.New("workspace root grants request is required") diff --git a/crates/agent-gateway/internal/protocol/pbws/guard_test.go b/crates/agent-gateway/internal/protocol/pbws/guard_test.go index da9cadb91..9b992febc 100644 --- a/crates/agent-gateway/internal/protocol/pbws/guard_test.go +++ b/crates/agent-gateway/internal/protocol/pbws/guard_test.go @@ -135,3 +135,57 @@ func TestVetAgentRequestRejectsMalformedWorkspaceRootGrants(t *testing.T) { } } } + +func TestVetAgentRequestAllowsCheckpointActions(t *testing.T) { + requests := []*gatewayv2.CheckpointRequest{ + {Action: "list", ConversationId: "conversation-1"}, + {Action: "diff", ConversationId: "conversation-1", TurnSeq: 2, AuthorizedRoots: []string{"/work"}}, + { + Action: "rewind", + ConversationId: "conversation-1", + TurnSeq: 2, + AuthorizedRoots: []string{"/work"}, + Expected: []*gatewayv2.CheckpointExpectedEntry{{Key: "/work\x01a.txt", CurrentHash: "abc"}}, + }, + } + + for _, request := range requests { + env := &gatewayv2.GatewayEnvelope{ + Payload: &gatewayv2.GatewayEnvelope_Checkpoint{Checkpoint: request}, + } + if err := vetAgentRequest(session.AgentView{}, env); err != nil { + t.Fatalf("vetAgentRequest(%+v) error = %v", request, err) + } + } +} + +func TestVetAgentRequestRejectsMalformedCheckpoint(t *testing.T) { + tooManyRoots := make([]string, 65) + for index := range tooManyRoots { + tooManyRoots[index] = "/work" + } + requests := []*gatewayv2.CheckpointRequest{ + nil, + {Action: "list", ConversationId: "conversation-1", TurnSeq: 1}, + {Action: "diff", ConversationId: "conversation-1"}, + {Action: "rewind", ConversationId: "conversation-1"}, + {Action: "unknown", ConversationId: "conversation-1"}, + {Action: "diff", ConversationId: "conversation-1", TurnSeq: 1, AuthorizedRoots: []string{" "}}, + {Action: "diff", ConversationId: "conversation-1", TurnSeq: 1, AuthorizedRoots: tooManyRoots}, + { + Action: "rewind", + ConversationId: "conversation-1", + TurnSeq: 1, + Expected: []*gatewayv2.CheckpointExpectedEntry{{Key: " ", CurrentHash: "abc"}}, + }, + } + + for _, request := range requests { + env := &gatewayv2.GatewayEnvelope{ + Payload: &gatewayv2.GatewayEnvelope_Checkpoint{Checkpoint: request}, + } + if err := vetAgentRequest(session.AgentView{}, env); err == nil { + t.Fatalf("vetAgentRequest(%+v) unexpectedly succeeded", request) + } + } +} diff --git a/crates/agent-gateway/proto/v2/gateway.proto b/crates/agent-gateway/proto/v2/gateway.proto index 061dc9a3e..96834593f 100644 --- a/crates/agent-gateway/proto/v2/gateway.proto +++ b/crates/agent-gateway/proto/v2/gateway.proto @@ -64,6 +64,7 @@ message GatewayEnvelope { HistorySetCwdRequest history_set_cwd = 95; WorkspaceRootGrantsRequest workspace_root_grants = 96; ImportDirectoryRequest import_directory = 97; + CheckpointRequest checkpoint = 98; } // Legacy tunnel control/frame payloads (pre-rewrite protocol) and the @@ -141,6 +142,7 @@ message AgentEnvelope { HistorySetCwdResponse history_set_cwd_resp = 100; WorkspaceRootGrantsResponse workspace_root_grants_resp = 101; ImportDirectoryResponse import_directory_resp = 102; + CheckpointResponse checkpoint_resp = 103; } // Legacy tunnel control/frame payloads (pre-rewrite protocol) and the @@ -1385,3 +1387,21 @@ message WorkspaceRootGrantsRequest { message WorkspaceRootGrantsResponse { repeated WorkspaceRootGrant grants = 1; } + +message CheckpointExpectedEntry { + string key = 1; + string current_hash = 2; +} + +message CheckpointRequest { + string action = 1; // "list" | "diff" | "rewind" + string conversation_id = 2; + uint64 turn_seq = 3; + repeated string authorized_roots = 4; + repeated CheckpointExpectedEntry expected = 5; +} + +message CheckpointResponse { + string action = 1; + string result_json = 2; +} diff --git a/crates/agent-gateway/web/src/app/GatewayAppView.tsx b/crates/agent-gateway/web/src/app/GatewayAppView.tsx index 7828827c1..2a99f34a9 100644 --- a/crates/agent-gateway/web/src/app/GatewayAppView.tsx +++ b/crates/agent-gateway/web/src/app/GatewayAppView.tsx @@ -16,6 +16,12 @@ import { RightDockPanel } from "@liveagent/ui/components/project-tools/RightDock import { ScrollArea } from "@liveagent/ui/components/ui/scroll-area"; import { WorkspaceOverlayHost } from "@liveagent/ui/components/workspace-editor/WorkspaceOverlayHost"; import { LocaleContext, t as translate } from "@liveagent/ui/i18n/index"; +import { + type CheckpointRewindClient, + CheckpointRewindProvider, + type CheckpointRewoundInfo, + formatCheckpointRewoundNotification, +} from "@liveagent/ui/lib/chat/checkpointRewind"; import type { PendingUploadedFile } from "@liveagent/ui/lib/chat/uploadedFiles"; import { mergePendingUploadedFiles } from "@liveagent/ui/lib/chat/uploadedFiles"; import { ChatComposerBar } from "@liveagent/ui/pages/chat/ChatComposerBar"; @@ -25,7 +31,7 @@ import { TranscriptWidthControls, } from "@liveagent/ui/pages/chat/transcript/TranscriptWidthControls"; import { SettingsPage } from "@liveagent/ui/pages/settings/SettingsPage"; -import { type CSSProperties, useCallback } from "react"; +import { type CSSProperties, useCallback, useMemo } from "react"; import { GatewayTranscript } from "@/components/GatewayTranscript"; import { getNextTheme, @@ -305,6 +311,48 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel } setSettings((prev) => updateExecutionModeFromChatSelection(prev, mode)), [setSettings], ); + const resolveCheckpointAuthorizedRoots = useCallback(async () => { + const roots: string[] = []; + const push = (value?: string | null) => { + const normalized = value?.trim(); + if (normalized && !roots.includes(normalized)) roots.push(normalized); + }; + push(displayedConversationWorkdir); + if ( + activeWorkspaceProject && + activeWorkspaceProjectPath && + activeWorkspaceProjectPath === displayedConversationWorkdir + ) { + try { + const grants = await api.listWorkspaceRootGrants( + activeWorkspaceProject.id, + activeWorkspaceProject.path, + ); + for (const grant of grants) { + if (grant.state === "active" && grant.access === "write") push(grant.canonicalPath); + } + } catch { + // Keep the primary root when additional grant lookup fails. + } + } + return roots; + }, [activeWorkspaceProject, activeWorkspaceProjectPath, api, displayedConversationWorkdir]); + const checkpointClient = useMemo( + () => ({ + list: (conversationId) => api.listCheckpointTurns(conversationId), + preview: (params) => api.previewCheckpointRewind(params), + rewind: (params) => api.rewindCheckpoint(params), + }), + [api], + ); + + const handleCheckpointRewound = useCallback( + (info: CheckpointRewoundInfo) => { + const notice = formatCheckpointRewoundNotification(info, settings.locale === "zh-CN"); + addNotify(notice.level, notice.message); + }, + [addNotify, settings.locale], + ); return ( @@ -532,42 +580,50 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel } className="gateway-transcript-scroll" > - 0} - onOpenSettings={openSettings} - hasMoreHistory={selectedHistoryHasMore} - isLoadingMoreHistory={loadingOlderHistory} - onLoadEarlierHistory={ - selectedHistoryHasMore ? handleLoadEarlierHistory : undefined - } - showUsage={isAgentDevExecutionMode} - usageContextWindow={currentModelContextWindow} - workspaceRoot={displayedConversationWorkdir} - onOpenFileLink={handleOpenChatFileLink} - gitClient={gitClient} - onLoadUploadedImagePreview={handleLoadUploadedImagePreview} - onResendFromEdit={handleResendFromEdit} - onBranchConversation={handleBranchConversation} - branchPendingMessageId={branchPendingMessageId} - onSuggestionSelect={handleEmptyStateSuggestion} - suggestionsDisabled={isSuggestionTyping} - /> + disabled={!displayedConversationId || transcriptBusy} + resolveAuthorizedRoots={resolveCheckpointAuthorizedRoots} + onRewound={handleCheckpointRewound} + > + 0} + onOpenSettings={openSettings} + hasMoreHistory={selectedHistoryHasMore} + isLoadingMoreHistory={loadingOlderHistory} + onLoadEarlierHistory={ + selectedHistoryHasMore ? handleLoadEarlierHistory : undefined + } + showUsage={isAgentDevExecutionMode} + usageContextWindow={currentModelContextWindow} + workspaceRoot={displayedConversationWorkdir} + onOpenFileLink={handleOpenChatFileLink} + gitClient={gitClient} + onLoadUploadedImagePreview={handleLoadUploadedImagePreview} + onResendFromEdit={handleResendFromEdit} + onBranchConversation={handleBranchConversation} + branchPendingMessageId={branchPendingMessageId} + onSuggestionSelect={handleEmptyStateSuggestion} + suggestionsDisabled={isSuggestionTyping} + /> + { if (effectiveMessageRef) setEditingMessageId(row.key); }} + rewindTurnId={effectiveMessageRef?.messageId} readOnly={readOnly} alwaysShowActions /> diff --git a/crates/agent-gateway/web/src/lib/gatewaySocket.ts b/crates/agent-gateway/web/src/lib/gatewaySocket.ts index 378947794..aa4a4721c 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocket.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocket.ts @@ -41,6 +41,10 @@ import type { ChatCommandUpdateListener, ChatFileOpenResponse, ChatQueueListener, + CheckpointDiffStats, + CheckpointRewindClient, + CheckpointRewindResult, + CheckpointTurnSummary, FsCreateDirResponse, FsDeleteResponse, FsListDirsResponse, @@ -341,6 +345,13 @@ export type GatewayWebSocketClientLike = { projectId: string, projectPath: string, ): Promise; + listCheckpointTurns(conversationId: string): Promise; + previewCheckpointRewind( + params: Parameters[0], + ): Promise; + rewindCheckpoint( + params: Parameters[0], + ): Promise; applyWorkspaceRootGrants( projectId: string, projectPath: string, diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketRpc.ts b/crates/agent-gateway/web/src/lib/gatewaySocketRpc.ts index 0690d7855..fc0acff2a 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketRpc.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketRpc.ts @@ -60,6 +60,9 @@ import { type ChatCommandUpdateListener, type ChatFileOpenResponse, type ChatQueueListener, + type CheckpointDiffStats, + type CheckpointRewindResult, + type CheckpointTurnSummary, type FsCreateDirResponse, type FsDeleteResponse, type FsListDirsResponse, @@ -1112,6 +1115,38 @@ export class GatewayWebSocketRpcClient extends GatewayWebSocketTransport { }); } + async listCheckpointTurns(conversationId: string): Promise { + return this.requestWithRecovery("checkpoint.list", { + conversation_id: conversationId, + }); + } + + async previewCheckpointRewind(params: { + conversationId: string; + turnSeq: number; + authorizedRoots: string[]; + }): Promise { + return this.request("checkpoint.diff", { + conversation_id: params.conversationId, + turn_seq: params.turnSeq, + authorized_roots: params.authorizedRoots, + }); + } + + async rewindCheckpoint(params: { + conversationId: string; + turnSeq: number; + authorizedRoots: string[]; + expected: { key: string; currentHash: string }[]; + }): Promise { + return this.request("checkpoint.rewind", { + conversation_id: params.conversationId, + turn_seq: params.turnSeq, + authorized_roots: params.authorizedRoots, + expected: params.expected, + }); + } + async listDirs(path: string, maxResults?: number): Promise { return this.requestWithRecovery("fs.list_dirs", { path, diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketShared.ts b/crates/agent-gateway/web/src/lib/gatewaySocketShared.ts index bc63d9038..0197dd268 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketShared.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketShared.ts @@ -131,6 +131,15 @@ export type GatewayWorkspaceRootGrantsResponse = { grants: GatewayWorkspaceRootGrant[]; }; +// 检查点回退的载荷类型:UI 层 checkpointRewind 是唯一真源,这里只做转发, +// 保证 socket 层调用方(gatewaySocket/gatewaySocketRpc)不直接跨层 import。 +export type { + CheckpointDiffStats, + CheckpointRewindClient, + CheckpointRewindResult, + CheckpointTurnSummary, +} from "@liveagent/ui/lib/chat/checkpointRewind"; + export type FsListDirsResponse = { path: string; entries: Array<{ path: string; name: string }>; diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts index 8e53b5283..f16304fa6 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts @@ -39,6 +39,8 @@ import { ChatRuntimeControlsSchema, ChatSelectedModelSchema, ChatUploadedFileSchema, + CheckpointExpectedEntrySchema, + CheckpointRequestSchema, CronManageRequestSchema, FileMentionListRequestSchema, FsCreateDirRequestSchema, @@ -814,6 +816,29 @@ function agentRequestPayload(type: string, body: J): GatewayEnvelope["payload"] openInFileManager: bool(body.open_in_file_manager), }), }; + case "checkpoint.list": + case "checkpoint.diff": + case "checkpoint.rewind": { + const expected = Array.isArray(body.expected) ? body.expected : []; + return { + case: "checkpoint", + value: create(CheckpointRequestSchema, { + action: type.slice("checkpoint.".length), + conversationId: trimStr(body.conversation_id), + turnSeq: toI64(body.turn_seq), + authorizedRoots: Array.isArray(body.authorized_roots) + ? body.authorized_roots.filter((root): root is string => typeof root === "string") + : [], + expected: expected.map((entry) => { + const value = rec(entry); + return create(CheckpointExpectedEntrySchema, { + key: trimStr(value.key), + currentHash: trimStr(value.current_hash), + }); + }), + }), + }; + } default: throw new Error(`unsupported gateway request type: ${type}`); } @@ -1190,6 +1215,8 @@ function decodeAgentResponse(envelope: AgentEnvelope, options: { agentOnline: bo updatedAt: num(grant.updatedAt), })), }; + case "checkpointResp": + return unmarshalJsonPayload(payload.value.resultJson); case "fsListDirsResp": return { path: payload.value.path.trim(), diff --git a/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts b/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts index 528d87a31..224c7631e 100644 --- a/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts +++ b/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file proto/v2/gateway.proto. */ export const file_proto_v2_gateway: GenFile = /*@__PURE__*/ - fileDesc("ChZwcm90by92Mi9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MiKaHQoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXF1ZXN0SAASQAoQY2hhdF9pbmdyZXNzX2FjaxhLIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrSAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlcXVlc3RIABJECg5wcm92aWRlcl91c2FnZRhdIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXF1ZXN0SAASQwoOY2hhdF9maWxlX29wZW4YXiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXF1ZXN0SAASRQoPaGlzdG9yeV9zZXRfY3dkGF8gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNldEN3ZFJlcXVlc3RIABJRChV3b3Jrc3BhY2Vfcm9vdF9ncmFudHMYYCABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VSb290R3JhbnRzUmVxdWVzdEgAEkgKEGltcG9ydF9kaXJlY3RvcnkYYSABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlSZXF1ZXN0SABCCQoHcGF5bG9hZEoECEMQREoECEQQRUoECEUQRkoECEoQSyKQJgoNQWdlbnRFbnZlbG9wZRISCgpyZXF1ZXN0X2lkGAEgASgJEhEKCXRpbWVzdGFtcBgCIAEoAxI1CgpjaGF0X2V2ZW50GAogASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEV2ZW50SAASRAoQY3Jvbl9tYW5hZ2VfcmVzcBgUIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNyb25NYW5hZ2VSZXNwb25zZUgAEkYKEWhpc3RvcnlfbGlzdF9yZXNwGB4gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUxpc3RSZXNwb25zZUgAEkQKEGhpc3RvcnlfZ2V0X3Jlc3AYHyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5R2V0UmVzcG9uc2VIABJKChNoaXN0b3J5X3JlbmFtZV9yZXNwGCAgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVJlbmFtZVJlc3BvbnNlSAASSgoTaGlzdG9yeV9kZWxldGVfcmVzcBghIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlEZWxldGVSZXNwb25zZUgAEj4KDGhpc3Rvcnlfc3luYxgiIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTeW5jRXZlbnRIABJKChNoaXN0b3J5X3ByZWZpeF9yZXNwGCMgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVByZWZpeFJlc3BvbnNlSAASRAoQaGlzdG9yeV9waW5fcmVzcBgkIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQaW5SZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfZ2V0X3Jlc3AYJSABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVHZXRSZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfc2V0X3Jlc3AYJiABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTZXRSZXNwb25zZUgAElcKGmhpc3Rvcnlfc2hhcmVfcmVzb2x2ZV9yZXNwGCcgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlSAASTgoVaGlzdG9yeV93b3JrZGlyc19yZXNwGDggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2VIABJIChJwcm92aWRlcl9saXN0X3Jlc3AYKCABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlckxpc3RSZXNwb25zZUgAEkYKEXNldHRpbmdzX2dldF9yZXNwGCkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NHZXRSZXNwb25zZUgAEkwKFHNldHRpbmdzX3VwZGF0ZV9yZXNwGCogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NVcGRhdGVSZXNwb25zZUgAEkAKDXNldHRpbmdzX3N5bmMYKyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1N5bmNFdmVudEgAEk0KFXNraWxsX2ZpbGVzX2xpc3RfcmVzcBgsIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsRmlsZXNMaXN0UmVzcG9uc2VIABJTChhza2lsbF9tZXRhZGF0YV9yZWFkX3Jlc3AYLSABKAsyLy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbE1ldGFkYXRhUmVhZFJlc3BvbnNlSAASSwoUc2tpbGxfdGV4dF9yZWFkX3Jlc3AYLiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbFRleHRSZWFkUmVzcG9uc2VIABJPChZmaWxlX21lbnRpb25fbGlzdF9yZXNwGC8gASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuRmlsZU1lbnRpb25MaXN0UmVzcG9uc2VIABJXChp1cGxvYWRfcmVhZGFibGVfZmlsZXNfcmVzcBgwIAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZUgAEj4KDWZzX3Jvb3RzX3Jlc3AYMSABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1Jvb3RzUmVzcG9uc2VIABIyCgRwb25nGDIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuUG9uZ1Jlc3BvbnNlSAASRQoRZnNfbGlzdF9kaXJzX3Jlc3AYMyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVzcG9uc2VIABJZCht1cGxvYWRlZF9pbWFnZV9wcmV2aWV3X3Jlc3AYNCABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1Jlc3BvbnNlSAASSAoSbWVtb3J5X21hbmFnZV9yZXNwGDUgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVzcG9uc2VIABJGChFza2lsbF9tYW5hZ2VfcmVzcBg2IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVzcG9uc2VIABJcCh1mc19jcmVhdGVfcHJvamVjdF9mb2xkZXJfcmVzcBg3IAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlSAASQwoRdGVybWluYWxfcmVzcG9uc2UYOSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFJlc3BvbnNlSAASPQoOdGVybWluYWxfZXZlbnQYOiABKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbEV2ZW50SAASPAoMZnNfbGlzdF9yZXNwGDsgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNMaXN0UmVzcG9uc2VIABJHChJmc193cml0ZV90ZXh0X3Jlc3AYPCABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1dyaXRlVGV4dFJlc3BvbnNlSAASRwoSZnNfY3JlYXRlX2Rpcl9yZXNwGD0gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNDcmVhdGVEaXJSZXNwb25zZUgAEkAKDmZzX3JlbmFtZV9yZXNwGD4gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSZW5hbWVSZXNwb25zZUgAEkAKDmZzX2RlbGV0ZV9yZXNwGD8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNEZWxldGVSZXNwb25zZUgAEjkKDGdpdF9yZXNwb25zZRhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLkdpdFJlc3BvbnNlSAASVgoaZnNfcmVhZF9lZGl0YWJsZV90ZXh0X3Jlc3AYQSABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRFZGl0YWJsZVRleHRSZXNwb25zZUgAEloKHGZzX3JlYWRfd29ya3NwYWNlX2ltYWdlX3Jlc3AYQiABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlSAASOwoNc2Z0cF9yZXNwb25zZRhJIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXNwb25zZUgAEjUKCnNmdHBfZXZlbnQYSiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRXZlbnRIABJCCg9jaGF0X3F1ZXVlX3Jlc3AYSyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXNwb25zZUgAEkAKEGNoYXRfcXVldWVfZXZlbnQYTCABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVFdmVudEgAEj4KDGNoYXRfY29udHJvbBhGIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb250cm9sRXZlbnRIABJCCg5ydW50aW1lX3N0YXR1cxhHIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlJ1bnRpbWVTdGF0dXNFdmVudEgAEmUKInNldHRpbmdzX3Jlc2V0X3NzaF9rbm93bl9ob3N0X3Jlc3AYSCABKAsyNy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVzcG9uc2VIABJKChVjaGF0X3J1bnRpbWVfc25hcHNob3QYTSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVudGltZVNuYXBzaG90SAASTAoUcHJvdmlkZXJfbW9kZWxzX3Jlc3AYTyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlck1vZGVsc1Jlc3BvbnNlSAASQgoOdHVubmVsX2Rlc2lyZWQYUCABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxEZXNpcmVkU3RhdGVIABJMChZ0dW5uZWxfbXV0YXRpb25fcmVzdWx0GFEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsTXV0YXRpb25SZXN1bHRIABI5Cgx0dW5uZWxfZnJhbWUYUiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUgAEkYKE3R1bm5lbF9wcm9iZV9yZXBvcnQYUyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlcG9ydEgAEkoKEndvcmtzcGFjZV9hY3Rpdml0eRhaIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZUFjdGl2aXR5RXZlbnRIABJQChhtYW5hZ2VkX3Byb2Nlc3NfcmVzcG9uc2UYWyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlSAASUAoYbWFuYWdlZF9wcm9jZXNzX3NuYXBzaG90GFwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdEgAEkoKE2hpc3RvcnlfYnJhbmNoX3Jlc3AYXSABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5QnJhbmNoUmVzcG9uc2VIABJKChNwcm92aWRlcl91c2FnZV9yZXNwGF4gASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJVc2FnZVJlc3BvbnNlSAASRAoSY2hhdF9pbmdyZXNzX2JhdGNoGF8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NCYXRjaEgAEkYKE2NoYXRfaW5ncmVzc19yZXN1bWUYYCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Jlc3VtZUgAEkoKFWNoYXRfaW5ncmVzc19mcmFnbWVudBhhIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRnJhZ21lbnRIABJJChNjaGF0X2ZpbGVfb3Blbl9yZXNwGGIgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEZpbGVPcGVuUmVzcG9uc2VIABI0CgVlcnJvchhjIAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkVycm9yUmVzcG9uc2VIABJLChRoaXN0b3J5X3NldF9jd2RfcmVzcBhkIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTZXRDd2RSZXNwb25zZUgAElcKGndvcmtzcGFjZV9yb290X2dyYW50c19yZXNwGGUgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlUm9vdEdyYW50c1Jlc3BvbnNlSAASTgoVaW1wb3J0X2RpcmVjdG9yeV9yZXNwGGYgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSW1wb3J0RGlyZWN0b3J5UmVzcG9uc2VIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIThBPIlUKEUNoYXRTZWxlY3RlZE1vZGVsEhoKEmN1c3RvbV9wcm92aWRlcl9pZBgBIAEoCRINCgVtb2RlbBgCIAEoCRIVCg1wcm92aWRlcl90eXBlGAMgASgJImUKE0NoYXRSdW50aW1lQ29udHJvbHMSGAoQdGhpbmtpbmdfZW5hYmxlZBgBIAEoCBIhChluYXRpdmVfd2ViX3NlYXJjaF9lbmFibGVkGAIgASgIEhEKCXJlYXNvbmluZxgDIAEoCSJ1ChBDaGF0VXBsb2FkZWRGaWxlEhUKDXJlbGF0aXZlX3BhdGgYASABKAkSFQoNYWJzb2x1dGVfcGF0aBgCIAEoCRIRCglmaWxlX25hbWUYAyABKAkSDAoEa2luZBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgDIksKElVwbG9hZFJlYWRhYmxlRmlsZRIRCglmaWxlX25hbWUYASABKAkSEQoJbWltZV90eXBlGAIgASgJEg8KB2NvbnRlbnQYAyABKAwiZgoaVXBsb2FkUmVhZGFibGVGaWxlc1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRI3CgVmaWxlcxgCIAMoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZSJlChtVcGxvYWRSZWFkYWJsZUZpbGVzUmVzcG9uc2USNQoFZmlsZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0VXBsb2FkZWRGaWxlEg8KB3NraXBwZWQYAiADKAkiPQoTSW1wb3J0RGlyZWN0b3J5RmlsZRIVCg1yZWxhdGl2ZV9wYXRoGAEgASgJEg8KB2NvbnRlbnQYAiABKAwiwwIKFkltcG9ydERpcmVjdG9yeVJlcXVlc3QSDAoEbmFtZRgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSPAoFZmlsZXMYAyADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlGaWxlQgIYARITCgt0cmFuc2Zlcl9pZBgEIAEoCRJBCglvcGVyYXRpb24YBSABKA4yLi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlPcGVyYXRpb24SEwoLdG90YWxfZmlsZXMYBiABKA0SEwoLdG90YWxfYnl0ZXMYByABKAQSFQoNcmVsYXRpdmVfcGF0aBgIIAEoCRIOCgZvZmZzZXQYCSABKAQSDQoFY2h1bmsYCiABKAwSFQoNZmlsZV9jb21wbGV0ZRgLIAEoCCJ+ChdJbXBvcnREaXJlY3RvcnlSZXNwb25zZRIRCglyb290X3BhdGgYASABKAkSEgoKZmlsZV9jb3VudBgCIAEoBRIPCgdza2lwcGVkGAMgAygJEhMKC3RyYW5zZmVyX2lkGAQgASgJEhYKDnJlY2VpdmVkX2J5dGVzGAUgASgEIkUKG1VwbG9hZGVkSW1hZ2VQcmV2aWV3UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEhUKDWFic29sdXRlX3BhdGgYAiABKAkiPwocVXBsb2FkZWRJbWFnZVByZXZpZXdSZXNwb25zZRIRCgltaW1lX3R5cGUYASABKAkSDAoEZGF0YRgCIAEoCSJ7CgpUdW5uZWxTcGVjEgoKAmlkGAEgASgJEhEKCXNsdWdfaGludBgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAxIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJIlkKElR1bm5lbERlc2lyZWRTdGF0ZRIxCgd0dW5uZWxzGAEgAygLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3BlYxIQCghyZXZpc2lvbhgCIAEoBCJmCgxUdW5uZWxIZWFsdGgSDgoGc3RhdHVzGAEgASgJEhMKC2h0dHBfc3RhdHVzGAIgASgNEg0KBWVycm9yGAMgASgJEhIKCmNoZWNrZWRfYXQYBCABKAMSDgoGcnR0X21zGAUgASgNIvABCgxUdW5uZWxTdGF0dXMSCgoCaWQYASABKAkSDAoEc2x1ZxgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEwoLcHVibGljX3BhdGgYBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoAxISCgpleHBpcmVzX2F0GAcgASgDEhoKEmFjdGl2ZV9jb25uZWN0aW9ucxgIIAEoDRIYChBwcm9qZWN0X3BhdGhfa2V5GAkgASgJEjEKBWxvY2FsGAogASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIqUBChNUdW5uZWxTdGF0ZVNuYXBzaG90EjMKB3R1bm5lbHMYASADKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxTdGF0dXMSEAoIcmV2aXNpb24YAiABKAQSFAoMYWdlbnRfb25saW5lGAMgASgIEjEKBXJlbGF5GAQgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIpkBCg5UdW5uZWxNdXRhdGlvbhIOCgZhY3Rpb24YASABKAkSEQoJdHVubmVsX2lkGAIgASgJEhIKCnRhcmdldF91cmwYAyABKAkSDAoEbmFtZRgEIAEoCRIYCgt0dGxfc2Vjb25kcxgFIAEoDUgAiAEBEhgKEHByb2plY3RfcGF0aF9rZXkYBiABKAlCDgoMX3R0bF9zZWNvbmRzIlQKFFR1bm5lbE11dGF0aW9uUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRISCgplcnJvcl9jb2RlGAIgASgJEhUKDWVycm9yX21lc3NhZ2UYAyABKAkiWQoRVHVubmVsUHJvYmVSZXN1bHQSEQoJdHVubmVsX2lkGAEgASgJEjEKBWxvY2FsGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIk0KEVR1bm5lbFByb2JlUmVwb3J0EjgKB3Jlc3VsdHMYASADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlc3VsdCIrCgxUdW5uZWxIZWFkZXISDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSL1AgoLVHVubmVsRnJhbWUSEQoJc3RyZWFtX2lkGAEgASgJEjMKBGtpbmQYAiABKA4yJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUtpbmQSEgoKdGFyZ2V0X3VybBgDIAEoCRIOCgZtZXRob2QYBCABKAkSDAoEcGF0aBgFIAEoCRIzCgdoZWFkZXJzGAYgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhZGVyEg4KBnN0YXR1cxgHIAEoDRIMCgRib2R5GAggASgMEg0KBWVycm9yGAkgASgJEkIKD3dzX21lc3NhZ2VfdHlwZRgKIAEoDjIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFdzTWVzc2FnZVR5cGUSFgoOd3Nfc3VicHJvdG9jb2wYCyABKAkSFQoNd3NfY2xvc2VfY29kZRgMIAEoDRIXCg93c19jbG9zZV9yZWFzb24YDSABKAkiKQoVV29ya3NwYWNlV2F0Y2hSZXF1ZXN0EhAKCHdvcmtkaXJzGAEgAygJIn4KFldvcmtzcGFjZUFjdGl2aXR5RXZlbnQSDwoHd29ya2RpchgBIAEoCRIQCghyZXZpc2lvbhgCIAEoBBIKCgJmcxgDIAEoCBILCgNnaXQYBCABKAgSFQoNY2hhbmdlZF9wYXRocxgFIAMoCRIRCgl0cnVuY2F0ZWQYBiABKAgilgIKFE1hbmFnZWRQcm9jZXNzUmVjb3JkEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEgsKA3BpZBgGIAEoDRIQCghsb2dfcGF0aBgHIAEoCRISCgpzdGFydGVkX2F0GAggASgDEhgKC2ZpbmlzaGVkX2F0GAkgASgDSACIAQESFgoJZXhpdF9jb2RlGAogASgFSAGIAQESDwoHcnVubmluZxgLIAEoCBIQCghpc29sYXRlZBgMIAEoCBIQCghyZXN0b3JlZBgNIAEoCEIOCgxfZmluaXNoZWRfYXRCDAoKX2V4aXRfY29kZSJpChZNYW5hZ2VkUHJvY2Vzc1NuYXBzaG90Ej0KCXByb2Nlc3NlcxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVjb3JkEhAKCHJldmlzaW9uGAIgASgEIk4KFU1hbmFnZWRQcm9jZXNzUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKcHJvY2Vzc19pZBgCIAEoCRIRCgltYXhfYnl0ZXMYAyABKA0itwEKFk1hbmFnZWRQcm9jZXNzUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEj4KCHNuYXBzaG90GAIgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBITCgtsb2dfY29udGVudBgDIAEoCRIQCghsb2dfcGF0aBgEIAEoCRIVCg1sb2dfdHJ1bmNhdGVkGAUgASgIEg8KB3N0b3BwZWQYBiABKAgiOQoTTWVtb3J5TWFuYWdlUmVxdWVzdBIPCgdjb21tYW5kGAEgASgJEhEKCWFyZ3NfanNvbhgCIAEoCSIrChRNZW1vcnlNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSKYAwoPVGVybWluYWxSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEg0KBXRpdGxlGAYgASgJEgwKBGRhdGEYByABKAkSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRITCgtzc2hfaG9zdF9pZBgLIAEoCRIRCglwcm9tcHRfaWQYDCABKAkSFQoNcHJvbXB0X2Fuc3dlchgNIAEoCRIWCg50cnVzdF9ob3N0X2tleRgOIAEoCBIUCgxzZnRwX2VuYWJsZWQYDyABKAgSDgoGdGFiX2lkGBAgASgJEhAKCHRhYl9raW5kGBEgASgJEhMKC3JlbW90ZV9ob3N0GBIgASgJEhMKC3JlbW90ZV9wb3J0GBMgASgNEhIKCmxvY2FsX3BvcnQYFCABKA0SEgoKZm9yd2FyZF9pZBgVIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCKRAwoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgSDwoHY29udGVudBgNIAEoCRIOCgZvZmZzZXQYDiABKAQSEQoJbWF4X2J5dGVzGA8gASgEEhMKC3N0cmljdF91dGY4GBAgASgIEhYKDmV4cGVjdGVkX210aW1lGBEgASgEEhsKE2V4cGVjdGVkX3NpemVfYnl0ZXMYEiABKAQSGgoSY3JlYXRlX3BhcmVudF9kaXJzGBMgASgIIlgKCVNmdHBFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkSDAoEa2luZBgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEg0KBW10aW1lGAUgASgEIvIBCgxTZnRwVHJhbnNmZXISCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIRCglkaXJlY3Rpb24YAyABKAkSDgoGc3RhdHVzGAQgASgJEhMKC3NvdXJjZV9wYXRoGAUgASgJEhMKC3RhcmdldF9wYXRoGAYgASgJEhQKDGN1cnJlbnRfcGF0aBgHIAEoCRISCgpieXRlc19kb25lGAggASgEEhMKC2J5dGVzX3RvdGFsGAkgASgEEhIKCmZpbGVzX2RvbmUYCiABKA0SEwoLZmlsZXNfdG90YWwYCyABKA0SDQoFZXJyb3IYDCABKAkisAIKDFNmdHBSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSDAoEcGF0aBgCIAEoCRIwCgdlbnRyaWVzGAMgAygLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEVudHJ5Ei4KBWVudHJ5GAQgASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEVudHJ5Eg4KBmV4aXN0cxgFIAEoCBI0Cgh0cmFuc2ZlchgGIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBUcmFuc2ZlchIPCgdjb250ZW50GAcgASgJEg4KBm9mZnNldBgIIAEoBBISCgpieXRlc19yZWFkGAkgASgEEhIKCnNpemVfYnl0ZXMYCiABKAQSEQoJdHJ1bmNhdGVkGAsgASgIIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCL9AQoXVGVybWluYWxTc2hMb2NhbEZvcndhcmQSCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEhIKCmxvY2FsX2hvc3QYBCABKAkSEgoKbG9jYWxfcG9ydBgFIAEoDRIPCgdhZGRyZXNzGAYgASgJEhMKC3JlbW90ZV9ob3N0GAcgASgJEhMKC3JlbW90ZV9wb3J0GAggASgNEg4KBnN0YXR1cxgJIAEoCRISCgpjcmVhdGVkX2F0GAogASgEEhIKCnVwZGF0ZWRfYXQYCyABKAQSDQoFZXJyb3IYDCABKAkidQogVGVybWluYWxTc2hMb2NhbEZvcndhcmRzU25hcHNob3QSPwoIZm9yd2FyZHMYASADKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIQCghyZXZpc2lvbhgCIAEoBCJ/Ch1UZXJtaW5hbFNzaExvY2FsRm9yd2FyZEFjdGlvbhI+Cgdmb3J3YXJkGAEgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmQSEAoIcmV2aXNpb24YAiABKAQSDAoEa2luZBgDIAEoCSKnBQoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90ElIKEnNzaF9sb2NhbF9mb3J3YXJkcxgNIAEoCzI2LmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkc1NuYXBzaG90Ek4KEXNzaF9sb2NhbF9mb3J3YXJkGA4gASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRBY3Rpb24SKAogc3NoX2xvY2FsX2ZvcndhcmRfcG9ydF9hdmFpbGFibGUYDyABKAgi2gIKDVRlcm1pbmFsRXZlbnQSDAoEa2luZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSNgoHc2Vzc2lvbhgEIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGAUgASgMEhsKE291dHB1dF9zdGFydF9vZmZzZXQYBiABKAQSGQoRb3V0cHV0X2VuZF9vZmZzZXQYByABKAQSPwoIc3NoX3RhYnMYCCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBJOChFzc2hfbG9jYWxfZm9yd2FyZBgJIAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkQWN0aW9uIrICChNUZXJtaW5hbFN0cmVhbUZyYW1lEgwKBGtpbmQYASABKAkSEQoJc3RyZWFtX2lkGAIgASgJEhIKCnNlc3Npb25faWQYAyABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgEIAEoCRILCgNzZXEYBSABKAQSFAoMc3RhcnRfb2Zmc2V0GAYgASgEEhIKCmVuZF9vZmZzZXQYByABKAQSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRIRCgl0cnVuY2F0ZWQYCyABKAgSDQoFZXJyb3IYDCABKAkSNgoHc2Vzc2lvbhgNIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGA4gASgMIkAKCkdpdFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3dvcmtkaXIYAiABKAkSEQoJYXJnc19qc29uGAMgASgJIjIKC0dpdFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSL0AgoLQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSPwoOc2VsZWN0ZWRfbW9kZWwYAyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0U2VsZWN0ZWRNb2RlbBIWCg5leGVjdXRpb25fbW9kZRgEIAEoCRIPCgd3b3JrZGlyGAUgASgJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAlKBAgGEAdSFXNlbGVjdGVkX3N5c3RlbV90b29scyKKAQoOQ2hhdE1lc3NhZ2VSZWYSFQoNc2VnbWVudF9pbmRleBgBIAEoBRIVCg1tZXNzYWdlX2luZGV4GAIgASgFEhIKCnNlZ21lbnRfaWQYAyABKAkSEgoKbWVzc2FnZV9pZBgEIAEoCRIMCgRyb2xlGAUgASgJEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSI8ChFDYW5jZWxDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDgoGcnVuX2lkGAIgASgJIs8BChJDaGF0Q29tbWFuZFJlcXVlc3QSDAoEdHlwZRgBIAEoCRIyCgdyZXF1ZXN0GAIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJlcXVlc3QSPgoQYmFzZV9tZXNzYWdlX3JlZhgDIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmEjcKBmNhbmNlbBgEIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNhbmNlbENoYXRSZXF1ZXN0IrgBChBDaGF0UXVldWVSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDwoHaXRlbV9pZBgDIAEoCRIRCglkaXJlY3Rpb24YBCABKAkSEAoIcmV2aXNpb24YBSABKAQSEgoKZHJhZnRfanNvbhgGIAEoCRIbChN1cGxvYWRlZF9maWxlc19qc29uGAcgASgJEhQKDHJlcXVlc3RfanNvbhgIIAEoCSKGAQoRQ2hhdFF1ZXVlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCRIVCg1zbmFwc2hvdF9qc29uGAMgASgJEhEKCWl0ZW1fanNvbhgEIAEoCRISCgplcnJvcl9jb2RlGAUgASgJEhAKCHJldmlzaW9uGAYgASgEIlIKDkNoYXRRdWV1ZUV2ZW50EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1zbmFwc2hvdF9qc29uGAIgASgJEhAKCHJldmlzaW9uGAMgASgEIoUCCglDaGF0RXZlbnQSOwoEdHlwZRgBIAEoDjItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRFdmVudC5DaGF0RXZlbnRUeXBlEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIMCgRkYXRhGAMgASgJIpMBCg1DaGF0RXZlbnRUeXBlEgkKBVRPS0VOEAASDAoIVEhJTktJTkcQARINCglUT09MX0NBTEwQAhIPCgtUT09MX1JFU1VMVBADEggKBERPTkUQBBIJCgVFUlJPUhAFEg8KC1RPT0xfU1RBVFVTEAYSEQoNSE9TVEVEX1NFQVJDSBAHEhAKDFVTRVJfTUVTU0FHRRAIIrwBChBDaGF0Q29udHJvbEV2ZW50EhIKCnJlcXVlc3RfaWQYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJEhEKCXJ1bl9lcG9jaBgEIAEoAxIMCgR0eXBlGAUgASgJEg0KBXN0YXRlGAYgASgJEhIKCmVycm9yX2NvZGUYByABKAkSDwoHbWVzc2FnZRgIIAEoCRILCgNzZXEYCSABKAMi/AEKE0NoYXRSdW50aW1lU25hcHNob3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCRIRCgl3b3JrZXJfaWQYBCABKAkSDQoFc3RhdGUYBSABKAkSCwoDY3dkGAYgASgJEhIKCnVwZGF0ZWRfYXQYByABKAMSEAoIcmV2aXNpb24YCCABKAMSFAoMZW50cmllc19qc29uGAkgASgJEhMKC3Rvb2xfc3RhdHVzGAogASgJEiEKGXRvb2xfc3RhdHVzX2lzX2NvbXBhY3Rpb24YCyABKAgi6gEKElJ1bnRpbWVTdGF0dXNFdmVudBIRCgl3b3JrZXJfaWQYASABKAkSDQoFc3RhdGUYAiABKAkSDwoHdmlzaWJsZRgDIAEoCBIYChBhY3RpdmVfcnVuX2NvdW50GAQgASgNEhEKCXRpbWVzdGFtcBgFIAEoAxI4CgthY3RpdmVfcnVucxgGIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQSOgoNZmluaXNoZWRfcnVucxgHIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQigAEKDUNoYXRSdW5SZXBvcnQSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRINCgVzdGF0ZRgDIAEoCRISCgplcnJvcl9jb2RlGAQgASgJEg8KB21lc3NhZ2UYBSABKAkSEgoKdXBkYXRlZF9hdBgGIAEoAyJHChFDcm9uTWFuYWdlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHdGFza19pZBgCIAEoCRIRCgl0YXNrX2pzb24YAyABKAkiOQoSQ3Jvbk1hbmFnZVJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSJVChJIaXN0b3J5TGlzdFJlcXVlc3QSDAoEcGFnZRgBIAEoBRIRCglwYWdlX3NpemUYAiABKAUSCwoDY3dkGAMgASgJEhEKCWN3ZF9lbXB0eRgEIAEoCCJsChNIaXN0b3J5TGlzdFJlc3BvbnNlEkAKDWNvbnZlcnNhdGlvbnMYASADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5EhMKC3RvdGFsX2NvdW50GAIgASgFIooCChNDb252ZXJzYXRpb25TdW1tYXJ5EgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhIKCmNyZWF0ZWRfYXQYAyABKAMSEgoKdXBkYXRlZF9hdBgEIAEoAxIVCg1tZXNzYWdlX2NvdW50GAUgASgFEhMKC3Byb3ZpZGVyX2lkGAYgASgJEg0KBW1vZGVsGAcgASgJEhIKCnNlc3Npb25faWQYCCABKAkSCwoDY3dkGAkgASgJEhEKCWlzX3Bpbm5lZBgKIAEoCBIRCglwaW5uZWRfYXQYCyABKAMSEQoJaXNfc2hhcmVkGAwgASgIEhsKE3NlbGVjdGVkX21vZGVsX2pzb24YDSABKAkiQgoRSGlzdG9yeUdldFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBSLUAQoSSGlzdG9yeUdldFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5IoUBChRIaXN0b3J5UHJlZml4UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFAoMbWF4X21lc3NhZ2VzGAIgASgFEj4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZiLXAQoVSGlzdG9yeVByZWZpeFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij4KFEhpc3RvcnlSZW5hbWVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCSJYChVIaXN0b3J5UmVuYW1lUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSJvChRIaXN0b3J5QnJhbmNoUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSPgoQYmFzZV9tZXNzYWdlX3JlZhgCIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmIlgKFUhpc3RvcnlCcmFuY2hSZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij8KEUhpc3RvcnlQaW5SZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIRCglpc19waW5uZWQYAiABKAgiVQoSSGlzdG9yeVBpblJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkikgEKEkhpc3RvcnlTaGFyZVN0YXR1cxIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBINCgV0b2tlbhgDIAEoCRISCgpjcmVhdGVkX2F0GAQgASgDEhIKCnVwZGF0ZWRfYXQYBSABKAMSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgGIAEoCCIxChZIaXN0b3J5U2hhcmVHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSJSChdIaXN0b3J5U2hhcmVHZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyJ8ChZIaXN0b3J5U2hhcmVTZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEiAKE3JlZGFjdF90b29sX2NvbnRlbnQYAyABKAhIAIgBAUIWChRfcmVkYWN0X3Rvb2xfY29udGVudCJSChdIaXN0b3J5U2hhcmVTZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyIrChpIaXN0b3J5U2hhcmVSZXNvbHZlUmVxdWVzdBINCgV0b2tlbhgBIAEoCSLIAQobSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSPwoMY29udmVyc2F0aW9uGAQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIbChNyZWRhY3RfdG9vbF9jb250ZW50GAUgASgIIhgKFkhpc3RvcnlXb3JrZGlyc1JlcXVlc3QiVQoVSGlzdG9yeVdvcmtkaXJTdW1tYXJ5EgwKBHBhdGgYASABKAkSGgoSY29udmVyc2F0aW9uX2NvdW50GAIgASgFEhIKCnVwZGF0ZWRfYXQYAyABKAMiWAoXSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2USPQoId29ya2RpcnMYASADKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5V29ya2RpclN1bW1hcnkiLwoUSGlzdG9yeURlbGV0ZVJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJIhcKFUhpc3RvcnlEZWxldGVSZXNwb25zZSJ6ChBIaXN0b3J5U3luY0V2ZW50EgwKBGtpbmQYASABKAkSPwoMY29udmVyc2F0aW9uGAIgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIXCg9jb252ZXJzYXRpb25faWQYAyABKAkiFQoTUHJvdmlkZXJMaXN0UmVxdWVzdCIuChRQcm92aWRlckxpc3RSZXNwb25zZRIWCg5wcm92aWRlcnNfanNvbhgBIAEoCSIUChJTZXR0aW5nc0dldFJlcXVlc3QiLAoTU2V0dGluZ3NHZXRSZXNwb25zZRIVCg1zZXR0aW5nc19qc29uGAEgASgJIi4KFVNldHRpbmdzVXBkYXRlUmVxdWVzdBIVCg1zZXR0aW5nc19qc29uGAEgASgJIjsKFlNldHRpbmdzVXBkYXRlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCSI+CiBTZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdBIMCgRob3N0GAEgASgJEgwKBHBvcnQYAiABKA0iNAohU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKA0iKgoRU2V0dGluZ3NTeW5jRXZlbnQSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIXChVTa2lsbEZpbGVzTGlzdFJlcXVlc3QiTAoWU2tpbGxGaWxlc0xpc3RSZXNwb25zZRIQCghyb290X2RpchgBIAEoCRINCgVwYXRocxgCIAMoCRIRCgl0cnVuY2F0ZWQYAyABKAgiKAoYU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkiPgoZU2tpbGxNZXRhZGF0YVJlYWRSZXNwb25zZRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJIkQKFFNraWxsVGV4dFJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkSDgoGb2Zmc2V0GAIgASgNEg4KBmxlbmd0aBgDIAEoDSI7ChVTa2lsbFRleHRSZWFkUmVzcG9uc2USDwoHY29udGVudBgBIAEoCRIRCgl0cnVuY2F0ZWQYAiABKAgiKgoSU2tpbGxNYW5hZ2VSZXF1ZXN0EhQKDHBheWxvYWRfanNvbhgBIAEoCSIqChNTa2lsbE1hbmFnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIncKFkZpbGVNZW50aW9uTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRITCgttYXhfcmVzdWx0cxgCIAEoDRINCgVxdWVyeRgDIAEoCRIYCgtzaG93X2hpZGRlbhgEIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI+ChBGaWxlTWVudGlvbkVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiZQoXRmlsZU1lbnRpb25MaXN0UmVzcG9uc2USNwoHZW50cmllcxgBIAMoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uRW50cnkSEQoJdHJ1bmNhdGVkGAIgASgIIj8KBkZzUm9vdBIKCgJpZBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkSDQoFbGFiZWwYBCABKAkiEAoORnNSb290c1JlcXVlc3QiPgoPRnNSb290c1Jlc3BvbnNlEisKBXJvb3RzGAEgAygLMhwubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290IjYKEUZzTGlzdERpcnNSZXF1ZXN0EgwKBHBhdGgYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0iKAoKRnNEaXJFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkiaAoSRnNMaXN0RGlyc1Jlc3BvbnNlEgwKBHBhdGgYASABKAkSMQoHZW50cmllcxgCIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGlyRW50cnkSEQoJdHJ1bmNhdGVkGAMgASgIIjwKHEZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3QSDgoGcGFyZW50GAEgASgJEgwKBG5hbWUYAiABKAkiLQodRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2USDAoEcGF0aBgBIAEoCSKMAQoNRnNMaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEhgKC3Nob3dfaGlkZGVuGAYgASgISACIAQFCDgoMX3Nob3dfaGlkZGVuIjkKC0ZzTGlzdEVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiuQEKDkZzTGlzdFJlc3BvbnNlEgwKBHBhdGgYASABKAkSEAoIaGFzX3BhdGgYAiABKAgSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEg0KBXRvdGFsGAYgASgNEhAKCGhhc19tb3JlGAcgASgIEjIKB2VudHJpZXMYCCADKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3RFbnRyeSI6ChlGc1JlYWRFZGl0YWJsZVRleHRSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSKMAQoaRnNSZWFkRWRpdGFibGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgJEhAKCG10aW1lX21zGAMgASgEEhQKDGNvbnRlbnRfaGFzaBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgEEhMKC3RvdGFsX2xpbmVzGAYgASgEIjwKG0ZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiiQEKHEZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDAoEZGF0YRgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEhAKCG10aW1lX21zGAUgASgEEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSLbAQoTQ2hhdEZpbGVPcGVuUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHd29ya2RpchgCIAEoCRIMCgRwYXRoGAMgASgJEg4KBnNvdXJjZRgEIAEoCRIRCgRsaW5lGAUgASgNSACIAQESFQoIZW5kX2xpbmUYBiABKA1IAYgBARITCgZjb2x1bW4YByABKA1IAogBARIcChRvcGVuX2luX2ZpbGVfbWFuYWdlchgIIAEoCEIHCgVfbGluZUILCglfZW5kX2xpbmVCCQoHX2NvbHVtbiLOAQoUQ2hhdEZpbGVPcGVuUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEgwKBGtpbmQYAiABKAkSDwoHd29ya2RpchgDIAEoCRIMCgRwYXRoGAQgASgJEhEKBGxpbmUYBSABKA1IAIgBARIVCghlbmRfbGluZRgGIAEoDUgBiAEBEhMKBmNvbHVtbhgHIAEoDUgCiAEBEhkKEW91dHNpZGVfd29ya3NwYWNlGAggASgIQgcKBV9saW5lQgsKCV9lbmRfbGluZUIJCgdfY29sdW1uIs4BChJGc1dyaXRlVGV4dFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg8KB2NvbnRlbnQYAyABKAkSDAoEbW9kZRgEIAEoCRIZChFleHBlY3RlZF9tdGltZV9tcxgFIAEoBBIdChVleHBlY3RlZF9jb250ZW50X2hhc2gYBiABKAkSHQoVaGFzX2V4cGVjdGVkX210aW1lX21zGAcgASgIEiEKGWhhc19leHBlY3RlZF9jb250ZW50X2hhc2gYCCABKAginQEKE0ZzV3JpdGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIMCgRtb2RlGAIgASgJEhYKDmV4aXN0ZWRfYmVmb3JlGAMgASgIEhUKDWJ5dGVzX3dyaXR0ZW4YBCABKAQSEAoIbXRpbWVfbXMYBSABKAQSFAoMY29udGVudF9oYXNoGAYgASgJEhMKC3RvdGFsX2xpbmVzGAcgASgEIjMKEkZzQ3JlYXRlRGlyUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiMQoTRnNDcmVhdGVEaXJSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiRgoPRnNSZW5hbWVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEQoJZnJvbV9wYXRoGAIgASgJEg8KB3RvX3BhdGgYAyABKAkiQQoQRnNSZW5hbWVSZXNwb25zZRIRCglmcm9tX3BhdGgYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJIjAKD0ZzRGVsZXRlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiLgoQRnNEZWxldGVSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiIAoLUGluZ1JlcXVlc3QSEQoJdGltZXN0YW1wGAEgASgDIiEKDFBvbmdSZXNwb25zZRIRCgl0aW1lc3RhbXAYASABKAMiLgoNRXJyb3JSZXNwb25zZRIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkivgEKFVByb3ZpZGVyTW9kZWxzUmVxdWVzdBIVCg1wcm92aWRlcl90eXBlGAEgASgJEhAKCGJhc2VfdXJsGAIgASgJEg8KB2FwaV9rZXkYAyABKAkSGAoQdXNlX3N5c3RlbV9wcm94eRgEIAEoCBISCgptb2RlbHNfdXJsGAUgASgJEhMKC3Byb3ZpZGVyX2lkGAYgASgJEhgKC2lzX2Z1bGxfdXJsGAcgASgISACIAQFCDgoMX2lzX2Z1bGxfdXJsIi0KFlByb3ZpZGVyTW9kZWxzUmVzcG9uc2USEwoLbW9kZWxzX2pzb24YASABKAkiUQoUUHJvdmlkZXJVc2FnZVJlcXVlc3QSEwoLcHJvdmlkZXJfaWQYASABKAkSDwoHcmVmcmVzaBgCIAEoCBITCgtjb25maWdfanNvbhgDIAEoCSIsChVQcm92aWRlclVzYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkiiAEKEENoYXRJbmdyZXNzQmF0Y2gSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIRCglmaXJzdF9zZXEYAyABKAQSOAoHcmVjb3JkcxgEIAMoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzUmVjb3JkIpoCChFDaGF0SW5ncmVzc1JlY29yZBI3CgVkZWx0YRgBIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRGVsdGFIABJBCgpjaGVja3BvaW50GAIgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NDaGVja3BvaW50SAASPQoIdGVybWluYWwYAyABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Rlcm1pbmFsSAASPwoJaGVhcnRiZWF0GAQgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NIZWFydGJlYXRIAEIJCgdwYXlsb2FkIjkKEENoYXRJbmdyZXNzRGVsdGESEgoKZXZlbnRfanNvbhgBIAEoCRIRCgl3b3JrZXJfaWQYAiABKAkiKgoUQ2hhdEluZ3Jlc3NIZWFydGJlYXQSEgoKdXBkYXRlZF9hdBgBIAEoAyLEAQoVQ2hhdEluZ3Jlc3NDaGVja3BvaW50EhoKEmNvdmVyc190aHJvdWdoX3NlcRgBIAEoBBIQCghyZXZpc2lvbhgCIAEoBBIdChVjb21wcmVzc2VkX3Byb2plY3Rpb24YAyABKAwSGgoSdW5jb21wcmVzc2VkX2J5dGVzGAQgASgEEg4KBnNoYTI1NhgFIAEoCRIYChBjb250ZW50X2NvbXBsZXRlGAYgASgIEhgKEGhpc3RvcnlfcmVxdWlyZWQYByABKAgi/AEKE0NoYXRJbmdyZXNzVGVybWluYWwSGgoSY292ZXJzX3Rocm91Z2hfc2VxGAEgASgEEhAKCHJldmlzaW9uGAIgASgEEh0KFWNvbXByZXNzZWRfcHJvamVjdGlvbhgDIAEoDBIaChJ1bmNvbXByZXNzZWRfYnl0ZXMYBCABKAQSDgoGc2hhMjU2GAUgASgJEhgKEGNvbnRlbnRfY29tcGxldGUYBiABKAgSGAoQaGlzdG9yeV9yZXF1aXJlZBgHIAEoCBINCgVzdGF0ZRgIIAEoCRISCgplcnJvcl9jb2RlGAkgASgJEhUKDWVycm9yX21lc3NhZ2UYCiABKAkiTQoRQ2hhdEluZ3Jlc3NSZXN1bWUSOAoEcnVucxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzUnVuUmVzdW1lItUBChRDaGF0SW5ncmVzc1J1blJlc3VtZRIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhcKD3JlcGxheV9mcm9tX3NlcRgDIAEoBBIaChJyZXBsYXlfdGhyb3VnaF9zZXEYBCABKAQSEAoIbmV4dF9zZXEYBSABKAQSHQoVbGF0ZXN0X2NoZWNrcG9pbnRfc2VxGAYgASgEEhQKDHRlcm1pbmFsX3NlcRgHIAEoBBIYChB0ZXJtaW5hbF9wZW5kaW5nGAggASgIIs4BChNDaGF0SW5ncmVzc0ZyYWdtZW50Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSEgoKc291cmNlX3NlcRgDIAEoBBIWCg5mcmFnbWVudF9pbmRleBgEIAEoDRIWCg5mcmFnbWVudF9jb3VudBgFIAEoDRIcChRlbmNvZGVkX3JlY29yZF9jaHVuaxgGIAEoDBIcChRlbmNvZGVkX3JlY29yZF9ieXRlcxgHIAEoBBIOCgZzaGEyNTYYCCABKAki3AIKDkNoYXRJbmdyZXNzQWNrEg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSGQoRY29tbWl0dGVkX3Rocm91Z2gYAyABKAQSFQoNZXhwZWN0ZWRfbmV4dBgEIAEoBBI7CgZhY3Rpb24YBSABKA4yKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0Fjay5BY3Rpb24SGgoSdGVybWluYWxfY29tbWl0dGVkGAYgASgIEhIKCmVycm9yX2NvZGUYByABKAkSFQoNZXJyb3JfbWVzc2FnZRgIIAEoCSJrCgZBY3Rpb24SFgoSQUNUSU9OX1VOU1BFQ0lGSUVEEAASDAoIQ09OVElOVUUQARIYChRSRVBMQVlfRlJPTV9FWFBFQ1RFRBACEhMKD1NFTkRfQ0hFQ0tQT0lOVBADEgwKCFJFSkVDVEVEEAQiPAoUSGlzdG9yeVNldEN3ZFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEgsKA2N3ZBgCIAEoCSJYChVIaXN0b3J5U2V0Q3dkUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSJmChdXb3Jrc3BhY2VSb290R3JhbnREcmFmdBIPCgJpZBgBIAEoCUgAiAEBEg0KBWFsaWFzGAIgASgJEhQKDGRpc3BsYXlfcGF0aBgDIAEoCRIOCgZhY2Nlc3MYBCABKAlCBQoDX2lkItIBChJXb3Jrc3BhY2VSb290R3JhbnQSCgoCaWQYASABKAkSEgoKcHJvamVjdF9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEg0KBWFsaWFzGAQgASgJEhQKDGRpc3BsYXlfcGF0aBgFIAEoCRIWCg5jYW5vbmljYWxfcGF0aBgGIAEoCRIOCgZhY2Nlc3MYByABKAkSDQoFc3RhdGUYCCABKAkSEgoKY3JlYXRlZF9hdBgJIAEoAxISCgp1cGRhdGVkX2F0GAogASgDIpUBChpXb3Jrc3BhY2VSb290R3JhbnRzUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKcHJvamVjdF9pZBgCIAEoCRIUCgxwcm9qZWN0X3BhdGgYAyABKAkSPQoGZ3JhbnRzGAQgAygLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlUm9vdEdyYW50RHJhZnQiVwobV29ya3NwYWNlUm9vdEdyYW50c1Jlc3BvbnNlEjgKBmdyYW50cxgBIAMoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZVJvb3RHcmFudCrlAQoYSW1wb3J0RGlyZWN0b3J5T3BlcmF0aW9uEioKJklNUE9SVF9ESVJFQ1RPUllfT1BFUkFUSU9OX1VOU1BFQ0lGSUVEEAASJAogSU1QT1JUX0RJUkVDVE9SWV9PUEVSQVRJT05fU1RBUlQQARIqCiZJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9XUklURV9DSFVOSxACEiUKIUlNUE9SVF9ESVJFQ1RPUllfT1BFUkFUSU9OX0NPTU1JVBADEiQKIElNUE9SVF9ESVJFQ1RPUllfT1BFUkFUSU9OX0FCT1JUEAQqxgQKD1R1bm5lbEZyYW1lS2luZBIhCh1UVU5ORUxfRlJBTUVfS0lORF9VTlNQRUNJRklFRBAAEigKJFRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVRVUVTVF9TVEFSVBABEicKI1RVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVRVUVTVF9CT0RZEAISJgoiVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX0VORBADEikKJVRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfU1RBUlQQBBIoCiRUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFU1BPTlNFX0JPRFkQBRInCiNUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFU1BPTlNFX0VORBAGEh0KGVRVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUwQBxIgChxUVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMX09LEAgSIwofVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTF9FUlJPUhAJEh4KGlRVTk5FTF9GUkFNRV9LSU5EX1dTX0ZSQU1FEAoSHgoaVFVOTkVMX0ZSQU1FX0tJTkRfV1NfQ0xPU0UQCxIbChdUVU5ORUxfRlJBTUVfS0lORF9FUlJPUhAMEhwKGFRVTk5FTF9GUkFNRV9LSU5EX0NBTkNFTBANEhoKFlRVTk5FTF9GUkFNRV9LSU5EX1BJTkcQDhIaChZUVU5ORUxfRlJBTUVfS0lORF9QT05HEA8qgQEKE1R1bm5lbFdzTWVzc2FnZVR5cGUSJgoiVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9VTlNQRUNJRklFRBAAEh8KG1RVTk5FTF9XU19NRVNTQUdFX1RZUEVfVEVYVBABEiEKHVRVTk5FTF9XU19NRVNTQUdFX1RZUEVfQklOQVJZEAJCQFo+Z2l0aHViLmNvbS9saXZlYWdlbnQvYWdlbnQtZ2F0ZXdheS9pbnRlcm5hbC9wcm90by92MjtnYXRld2F5djJiBnByb3RvMw"); + fileDesc("ChZwcm90by92Mi9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MiLZHQoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXF1ZXN0SAASQAoQY2hhdF9pbmdyZXNzX2FjaxhLIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrSAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlcXVlc3RIABJECg5wcm92aWRlcl91c2FnZRhdIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXF1ZXN0SAASQwoOY2hhdF9maWxlX29wZW4YXiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXF1ZXN0SAASRQoPaGlzdG9yeV9zZXRfY3dkGF8gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNldEN3ZFJlcXVlc3RIABJRChV3b3Jrc3BhY2Vfcm9vdF9ncmFudHMYYCABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VSb290R3JhbnRzUmVxdWVzdEgAEkgKEGltcG9ydF9kaXJlY3RvcnkYYSABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlSZXF1ZXN0SAASPQoKY2hlY2twb2ludBhiIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoZWNrcG9pbnRSZXF1ZXN0SABCCQoHcGF5bG9hZEoECEMQREoECEQQRUoECEUQRkoECEoQSyLVJgoNQWdlbnRFbnZlbG9wZRISCgpyZXF1ZXN0X2lkGAEgASgJEhEKCXRpbWVzdGFtcBgCIAEoAxI1CgpjaGF0X2V2ZW50GAogASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEV2ZW50SAASRAoQY3Jvbl9tYW5hZ2VfcmVzcBgUIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNyb25NYW5hZ2VSZXNwb25zZUgAEkYKEWhpc3RvcnlfbGlzdF9yZXNwGB4gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUxpc3RSZXNwb25zZUgAEkQKEGhpc3RvcnlfZ2V0X3Jlc3AYHyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5R2V0UmVzcG9uc2VIABJKChNoaXN0b3J5X3JlbmFtZV9yZXNwGCAgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVJlbmFtZVJlc3BvbnNlSAASSgoTaGlzdG9yeV9kZWxldGVfcmVzcBghIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlEZWxldGVSZXNwb25zZUgAEj4KDGhpc3Rvcnlfc3luYxgiIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTeW5jRXZlbnRIABJKChNoaXN0b3J5X3ByZWZpeF9yZXNwGCMgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVByZWZpeFJlc3BvbnNlSAASRAoQaGlzdG9yeV9waW5fcmVzcBgkIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQaW5SZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfZ2V0X3Jlc3AYJSABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVHZXRSZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfc2V0X3Jlc3AYJiABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTZXRSZXNwb25zZUgAElcKGmhpc3Rvcnlfc2hhcmVfcmVzb2x2ZV9yZXNwGCcgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlSAASTgoVaGlzdG9yeV93b3JrZGlyc19yZXNwGDggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2VIABJIChJwcm92aWRlcl9saXN0X3Jlc3AYKCABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlckxpc3RSZXNwb25zZUgAEkYKEXNldHRpbmdzX2dldF9yZXNwGCkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NHZXRSZXNwb25zZUgAEkwKFHNldHRpbmdzX3VwZGF0ZV9yZXNwGCogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NVcGRhdGVSZXNwb25zZUgAEkAKDXNldHRpbmdzX3N5bmMYKyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1N5bmNFdmVudEgAEk0KFXNraWxsX2ZpbGVzX2xpc3RfcmVzcBgsIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsRmlsZXNMaXN0UmVzcG9uc2VIABJTChhza2lsbF9tZXRhZGF0YV9yZWFkX3Jlc3AYLSABKAsyLy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbE1ldGFkYXRhUmVhZFJlc3BvbnNlSAASSwoUc2tpbGxfdGV4dF9yZWFkX3Jlc3AYLiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbFRleHRSZWFkUmVzcG9uc2VIABJPChZmaWxlX21lbnRpb25fbGlzdF9yZXNwGC8gASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuRmlsZU1lbnRpb25MaXN0UmVzcG9uc2VIABJXChp1cGxvYWRfcmVhZGFibGVfZmlsZXNfcmVzcBgwIAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZUgAEj4KDWZzX3Jvb3RzX3Jlc3AYMSABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1Jvb3RzUmVzcG9uc2VIABIyCgRwb25nGDIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuUG9uZ1Jlc3BvbnNlSAASRQoRZnNfbGlzdF9kaXJzX3Jlc3AYMyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVzcG9uc2VIABJZCht1cGxvYWRlZF9pbWFnZV9wcmV2aWV3X3Jlc3AYNCABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1Jlc3BvbnNlSAASSAoSbWVtb3J5X21hbmFnZV9yZXNwGDUgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVzcG9uc2VIABJGChFza2lsbF9tYW5hZ2VfcmVzcBg2IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVzcG9uc2VIABJcCh1mc19jcmVhdGVfcHJvamVjdF9mb2xkZXJfcmVzcBg3IAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlSAASQwoRdGVybWluYWxfcmVzcG9uc2UYOSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFJlc3BvbnNlSAASPQoOdGVybWluYWxfZXZlbnQYOiABKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbEV2ZW50SAASPAoMZnNfbGlzdF9yZXNwGDsgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNMaXN0UmVzcG9uc2VIABJHChJmc193cml0ZV90ZXh0X3Jlc3AYPCABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1dyaXRlVGV4dFJlc3BvbnNlSAASRwoSZnNfY3JlYXRlX2Rpcl9yZXNwGD0gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNDcmVhdGVEaXJSZXNwb25zZUgAEkAKDmZzX3JlbmFtZV9yZXNwGD4gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSZW5hbWVSZXNwb25zZUgAEkAKDmZzX2RlbGV0ZV9yZXNwGD8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNEZWxldGVSZXNwb25zZUgAEjkKDGdpdF9yZXNwb25zZRhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLkdpdFJlc3BvbnNlSAASVgoaZnNfcmVhZF9lZGl0YWJsZV90ZXh0X3Jlc3AYQSABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRFZGl0YWJsZVRleHRSZXNwb25zZUgAEloKHGZzX3JlYWRfd29ya3NwYWNlX2ltYWdlX3Jlc3AYQiABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlSAASOwoNc2Z0cF9yZXNwb25zZRhJIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXNwb25zZUgAEjUKCnNmdHBfZXZlbnQYSiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRXZlbnRIABJCCg9jaGF0X3F1ZXVlX3Jlc3AYSyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXNwb25zZUgAEkAKEGNoYXRfcXVldWVfZXZlbnQYTCABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVFdmVudEgAEj4KDGNoYXRfY29udHJvbBhGIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb250cm9sRXZlbnRIABJCCg5ydW50aW1lX3N0YXR1cxhHIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlJ1bnRpbWVTdGF0dXNFdmVudEgAEmUKInNldHRpbmdzX3Jlc2V0X3NzaF9rbm93bl9ob3N0X3Jlc3AYSCABKAsyNy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVzcG9uc2VIABJKChVjaGF0X3J1bnRpbWVfc25hcHNob3QYTSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVudGltZVNuYXBzaG90SAASTAoUcHJvdmlkZXJfbW9kZWxzX3Jlc3AYTyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlck1vZGVsc1Jlc3BvbnNlSAASQgoOdHVubmVsX2Rlc2lyZWQYUCABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxEZXNpcmVkU3RhdGVIABJMChZ0dW5uZWxfbXV0YXRpb25fcmVzdWx0GFEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsTXV0YXRpb25SZXN1bHRIABI5Cgx0dW5uZWxfZnJhbWUYUiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUgAEkYKE3R1bm5lbF9wcm9iZV9yZXBvcnQYUyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlcG9ydEgAEkoKEndvcmtzcGFjZV9hY3Rpdml0eRhaIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZUFjdGl2aXR5RXZlbnRIABJQChhtYW5hZ2VkX3Byb2Nlc3NfcmVzcG9uc2UYWyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlSAASUAoYbWFuYWdlZF9wcm9jZXNzX3NuYXBzaG90GFwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdEgAEkoKE2hpc3RvcnlfYnJhbmNoX3Jlc3AYXSABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5QnJhbmNoUmVzcG9uc2VIABJKChNwcm92aWRlcl91c2FnZV9yZXNwGF4gASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJVc2FnZVJlc3BvbnNlSAASRAoSY2hhdF9pbmdyZXNzX2JhdGNoGF8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NCYXRjaEgAEkYKE2NoYXRfaW5ncmVzc19yZXN1bWUYYCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Jlc3VtZUgAEkoKFWNoYXRfaW5ncmVzc19mcmFnbWVudBhhIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRnJhZ21lbnRIABJJChNjaGF0X2ZpbGVfb3Blbl9yZXNwGGIgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEZpbGVPcGVuUmVzcG9uc2VIABI0CgVlcnJvchhjIAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkVycm9yUmVzcG9uc2VIABJLChRoaXN0b3J5X3NldF9jd2RfcmVzcBhkIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTZXRDd2RSZXNwb25zZUgAElcKGndvcmtzcGFjZV9yb290X2dyYW50c19yZXNwGGUgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlUm9vdEdyYW50c1Jlc3BvbnNlSAASTgoVaW1wb3J0X2RpcmVjdG9yeV9yZXNwGGYgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSW1wb3J0RGlyZWN0b3J5UmVzcG9uc2VIABJDCg9jaGVja3BvaW50X3Jlc3AYZyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGVja3BvaW50UmVzcG9uc2VIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIThBPIlUKEUNoYXRTZWxlY3RlZE1vZGVsEhoKEmN1c3RvbV9wcm92aWRlcl9pZBgBIAEoCRINCgVtb2RlbBgCIAEoCRIVCg1wcm92aWRlcl90eXBlGAMgASgJImUKE0NoYXRSdW50aW1lQ29udHJvbHMSGAoQdGhpbmtpbmdfZW5hYmxlZBgBIAEoCBIhChluYXRpdmVfd2ViX3NlYXJjaF9lbmFibGVkGAIgASgIEhEKCXJlYXNvbmluZxgDIAEoCSJ1ChBDaGF0VXBsb2FkZWRGaWxlEhUKDXJlbGF0aXZlX3BhdGgYASABKAkSFQoNYWJzb2x1dGVfcGF0aBgCIAEoCRIRCglmaWxlX25hbWUYAyABKAkSDAoEa2luZBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgDIksKElVwbG9hZFJlYWRhYmxlRmlsZRIRCglmaWxlX25hbWUYASABKAkSEQoJbWltZV90eXBlGAIgASgJEg8KB2NvbnRlbnQYAyABKAwiZgoaVXBsb2FkUmVhZGFibGVGaWxlc1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRI3CgVmaWxlcxgCIAMoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZSJlChtVcGxvYWRSZWFkYWJsZUZpbGVzUmVzcG9uc2USNQoFZmlsZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0VXBsb2FkZWRGaWxlEg8KB3NraXBwZWQYAiADKAkiPQoTSW1wb3J0RGlyZWN0b3J5RmlsZRIVCg1yZWxhdGl2ZV9wYXRoGAEgASgJEg8KB2NvbnRlbnQYAiABKAwiwwIKFkltcG9ydERpcmVjdG9yeVJlcXVlc3QSDAoEbmFtZRgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSPAoFZmlsZXMYAyADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlGaWxlQgIYARITCgt0cmFuc2Zlcl9pZBgEIAEoCRJBCglvcGVyYXRpb24YBSABKA4yLi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlPcGVyYXRpb24SEwoLdG90YWxfZmlsZXMYBiABKA0SEwoLdG90YWxfYnl0ZXMYByABKAQSFQoNcmVsYXRpdmVfcGF0aBgIIAEoCRIOCgZvZmZzZXQYCSABKAQSDQoFY2h1bmsYCiABKAwSFQoNZmlsZV9jb21wbGV0ZRgLIAEoCCJ+ChdJbXBvcnREaXJlY3RvcnlSZXNwb25zZRIRCglyb290X3BhdGgYASABKAkSEgoKZmlsZV9jb3VudBgCIAEoBRIPCgdza2lwcGVkGAMgAygJEhMKC3RyYW5zZmVyX2lkGAQgASgJEhYKDnJlY2VpdmVkX2J5dGVzGAUgASgEIkUKG1VwbG9hZGVkSW1hZ2VQcmV2aWV3UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEhUKDWFic29sdXRlX3BhdGgYAiABKAkiPwocVXBsb2FkZWRJbWFnZVByZXZpZXdSZXNwb25zZRIRCgltaW1lX3R5cGUYASABKAkSDAoEZGF0YRgCIAEoCSJ7CgpUdW5uZWxTcGVjEgoKAmlkGAEgASgJEhEKCXNsdWdfaGludBgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAxIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJIlkKElR1bm5lbERlc2lyZWRTdGF0ZRIxCgd0dW5uZWxzGAEgAygLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3BlYxIQCghyZXZpc2lvbhgCIAEoBCJmCgxUdW5uZWxIZWFsdGgSDgoGc3RhdHVzGAEgASgJEhMKC2h0dHBfc3RhdHVzGAIgASgNEg0KBWVycm9yGAMgASgJEhIKCmNoZWNrZWRfYXQYBCABKAMSDgoGcnR0X21zGAUgASgNIvABCgxUdW5uZWxTdGF0dXMSCgoCaWQYASABKAkSDAoEc2x1ZxgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEwoLcHVibGljX3BhdGgYBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoAxISCgpleHBpcmVzX2F0GAcgASgDEhoKEmFjdGl2ZV9jb25uZWN0aW9ucxgIIAEoDRIYChBwcm9qZWN0X3BhdGhfa2V5GAkgASgJEjEKBWxvY2FsGAogASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIqUBChNUdW5uZWxTdGF0ZVNuYXBzaG90EjMKB3R1bm5lbHMYASADKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxTdGF0dXMSEAoIcmV2aXNpb24YAiABKAQSFAoMYWdlbnRfb25saW5lGAMgASgIEjEKBXJlbGF5GAQgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIpkBCg5UdW5uZWxNdXRhdGlvbhIOCgZhY3Rpb24YASABKAkSEQoJdHVubmVsX2lkGAIgASgJEhIKCnRhcmdldF91cmwYAyABKAkSDAoEbmFtZRgEIAEoCRIYCgt0dGxfc2Vjb25kcxgFIAEoDUgAiAEBEhgKEHByb2plY3RfcGF0aF9rZXkYBiABKAlCDgoMX3R0bF9zZWNvbmRzIlQKFFR1bm5lbE11dGF0aW9uUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRISCgplcnJvcl9jb2RlGAIgASgJEhUKDWVycm9yX21lc3NhZ2UYAyABKAkiWQoRVHVubmVsUHJvYmVSZXN1bHQSEQoJdHVubmVsX2lkGAEgASgJEjEKBWxvY2FsGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIk0KEVR1bm5lbFByb2JlUmVwb3J0EjgKB3Jlc3VsdHMYASADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlc3VsdCIrCgxUdW5uZWxIZWFkZXISDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSL1AgoLVHVubmVsRnJhbWUSEQoJc3RyZWFtX2lkGAEgASgJEjMKBGtpbmQYAiABKA4yJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUtpbmQSEgoKdGFyZ2V0X3VybBgDIAEoCRIOCgZtZXRob2QYBCABKAkSDAoEcGF0aBgFIAEoCRIzCgdoZWFkZXJzGAYgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhZGVyEg4KBnN0YXR1cxgHIAEoDRIMCgRib2R5GAggASgMEg0KBWVycm9yGAkgASgJEkIKD3dzX21lc3NhZ2VfdHlwZRgKIAEoDjIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFdzTWVzc2FnZVR5cGUSFgoOd3Nfc3VicHJvdG9jb2wYCyABKAkSFQoNd3NfY2xvc2VfY29kZRgMIAEoDRIXCg93c19jbG9zZV9yZWFzb24YDSABKAkiKQoVV29ya3NwYWNlV2F0Y2hSZXF1ZXN0EhAKCHdvcmtkaXJzGAEgAygJIn4KFldvcmtzcGFjZUFjdGl2aXR5RXZlbnQSDwoHd29ya2RpchgBIAEoCRIQCghyZXZpc2lvbhgCIAEoBBIKCgJmcxgDIAEoCBILCgNnaXQYBCABKAgSFQoNY2hhbmdlZF9wYXRocxgFIAMoCRIRCgl0cnVuY2F0ZWQYBiABKAgilgIKFE1hbmFnZWRQcm9jZXNzUmVjb3JkEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEgsKA3BpZBgGIAEoDRIQCghsb2dfcGF0aBgHIAEoCRISCgpzdGFydGVkX2F0GAggASgDEhgKC2ZpbmlzaGVkX2F0GAkgASgDSACIAQESFgoJZXhpdF9jb2RlGAogASgFSAGIAQESDwoHcnVubmluZxgLIAEoCBIQCghpc29sYXRlZBgMIAEoCBIQCghyZXN0b3JlZBgNIAEoCEIOCgxfZmluaXNoZWRfYXRCDAoKX2V4aXRfY29kZSJpChZNYW5hZ2VkUHJvY2Vzc1NuYXBzaG90Ej0KCXByb2Nlc3NlcxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVjb3JkEhAKCHJldmlzaW9uGAIgASgEIk4KFU1hbmFnZWRQcm9jZXNzUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKcHJvY2Vzc19pZBgCIAEoCRIRCgltYXhfYnl0ZXMYAyABKA0itwEKFk1hbmFnZWRQcm9jZXNzUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEj4KCHNuYXBzaG90GAIgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBITCgtsb2dfY29udGVudBgDIAEoCRIQCghsb2dfcGF0aBgEIAEoCRIVCg1sb2dfdHJ1bmNhdGVkGAUgASgIEg8KB3N0b3BwZWQYBiABKAgiOQoTTWVtb3J5TWFuYWdlUmVxdWVzdBIPCgdjb21tYW5kGAEgASgJEhEKCWFyZ3NfanNvbhgCIAEoCSIrChRNZW1vcnlNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSKYAwoPVGVybWluYWxSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEg0KBXRpdGxlGAYgASgJEgwKBGRhdGEYByABKAkSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRITCgtzc2hfaG9zdF9pZBgLIAEoCRIRCglwcm9tcHRfaWQYDCABKAkSFQoNcHJvbXB0X2Fuc3dlchgNIAEoCRIWCg50cnVzdF9ob3N0X2tleRgOIAEoCBIUCgxzZnRwX2VuYWJsZWQYDyABKAgSDgoGdGFiX2lkGBAgASgJEhAKCHRhYl9raW5kGBEgASgJEhMKC3JlbW90ZV9ob3N0GBIgASgJEhMKC3JlbW90ZV9wb3J0GBMgASgNEhIKCmxvY2FsX3BvcnQYFCABKA0SEgoKZm9yd2FyZF9pZBgVIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCKRAwoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgSDwoHY29udGVudBgNIAEoCRIOCgZvZmZzZXQYDiABKAQSEQoJbWF4X2J5dGVzGA8gASgEEhMKC3N0cmljdF91dGY4GBAgASgIEhYKDmV4cGVjdGVkX210aW1lGBEgASgEEhsKE2V4cGVjdGVkX3NpemVfYnl0ZXMYEiABKAQSGgoSY3JlYXRlX3BhcmVudF9kaXJzGBMgASgIIlgKCVNmdHBFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkSDAoEa2luZBgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEg0KBW10aW1lGAUgASgEIvIBCgxTZnRwVHJhbnNmZXISCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIRCglkaXJlY3Rpb24YAyABKAkSDgoGc3RhdHVzGAQgASgJEhMKC3NvdXJjZV9wYXRoGAUgASgJEhMKC3RhcmdldF9wYXRoGAYgASgJEhQKDGN1cnJlbnRfcGF0aBgHIAEoCRISCgpieXRlc19kb25lGAggASgEEhMKC2J5dGVzX3RvdGFsGAkgASgEEhIKCmZpbGVzX2RvbmUYCiABKA0SEwoLZmlsZXNfdG90YWwYCyABKA0SDQoFZXJyb3IYDCABKAkisAIKDFNmdHBSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSDAoEcGF0aBgCIAEoCRIwCgdlbnRyaWVzGAMgAygLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEVudHJ5Ei4KBWVudHJ5GAQgASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEVudHJ5Eg4KBmV4aXN0cxgFIAEoCBI0Cgh0cmFuc2ZlchgGIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBUcmFuc2ZlchIPCgdjb250ZW50GAcgASgJEg4KBm9mZnNldBgIIAEoBBISCgpieXRlc19yZWFkGAkgASgEEhIKCnNpemVfYnl0ZXMYCiABKAQSEQoJdHJ1bmNhdGVkGAsgASgIIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCL9AQoXVGVybWluYWxTc2hMb2NhbEZvcndhcmQSCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEhIKCmxvY2FsX2hvc3QYBCABKAkSEgoKbG9jYWxfcG9ydBgFIAEoDRIPCgdhZGRyZXNzGAYgASgJEhMKC3JlbW90ZV9ob3N0GAcgASgJEhMKC3JlbW90ZV9wb3J0GAggASgNEg4KBnN0YXR1cxgJIAEoCRISCgpjcmVhdGVkX2F0GAogASgEEhIKCnVwZGF0ZWRfYXQYCyABKAQSDQoFZXJyb3IYDCABKAkidQogVGVybWluYWxTc2hMb2NhbEZvcndhcmRzU25hcHNob3QSPwoIZm9yd2FyZHMYASADKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIQCghyZXZpc2lvbhgCIAEoBCJ/Ch1UZXJtaW5hbFNzaExvY2FsRm9yd2FyZEFjdGlvbhI+Cgdmb3J3YXJkGAEgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmQSEAoIcmV2aXNpb24YAiABKAQSDAoEa2luZBgDIAEoCSKnBQoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90ElIKEnNzaF9sb2NhbF9mb3J3YXJkcxgNIAEoCzI2LmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkc1NuYXBzaG90Ek4KEXNzaF9sb2NhbF9mb3J3YXJkGA4gASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRBY3Rpb24SKAogc3NoX2xvY2FsX2ZvcndhcmRfcG9ydF9hdmFpbGFibGUYDyABKAgi2gIKDVRlcm1pbmFsRXZlbnQSDAoEa2luZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSNgoHc2Vzc2lvbhgEIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGAUgASgMEhsKE291dHB1dF9zdGFydF9vZmZzZXQYBiABKAQSGQoRb3V0cHV0X2VuZF9vZmZzZXQYByABKAQSPwoIc3NoX3RhYnMYCCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBJOChFzc2hfbG9jYWxfZm9yd2FyZBgJIAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkQWN0aW9uIrICChNUZXJtaW5hbFN0cmVhbUZyYW1lEgwKBGtpbmQYASABKAkSEQoJc3RyZWFtX2lkGAIgASgJEhIKCnNlc3Npb25faWQYAyABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgEIAEoCRILCgNzZXEYBSABKAQSFAoMc3RhcnRfb2Zmc2V0GAYgASgEEhIKCmVuZF9vZmZzZXQYByABKAQSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRIRCgl0cnVuY2F0ZWQYCyABKAgSDQoFZXJyb3IYDCABKAkSNgoHc2Vzc2lvbhgNIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGA4gASgMIkAKCkdpdFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3dvcmtkaXIYAiABKAkSEQoJYXJnc19qc29uGAMgASgJIjIKC0dpdFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSL0AgoLQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSPwoOc2VsZWN0ZWRfbW9kZWwYAyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0U2VsZWN0ZWRNb2RlbBIWCg5leGVjdXRpb25fbW9kZRgEIAEoCRIPCgd3b3JrZGlyGAUgASgJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAlKBAgGEAdSFXNlbGVjdGVkX3N5c3RlbV90b29scyKKAQoOQ2hhdE1lc3NhZ2VSZWYSFQoNc2VnbWVudF9pbmRleBgBIAEoBRIVCg1tZXNzYWdlX2luZGV4GAIgASgFEhIKCnNlZ21lbnRfaWQYAyABKAkSEgoKbWVzc2FnZV9pZBgEIAEoCRIMCgRyb2xlGAUgASgJEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSI8ChFDYW5jZWxDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDgoGcnVuX2lkGAIgASgJIs8BChJDaGF0Q29tbWFuZFJlcXVlc3QSDAoEdHlwZRgBIAEoCRIyCgdyZXF1ZXN0GAIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJlcXVlc3QSPgoQYmFzZV9tZXNzYWdlX3JlZhgDIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmEjcKBmNhbmNlbBgEIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNhbmNlbENoYXRSZXF1ZXN0IrgBChBDaGF0UXVldWVSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDwoHaXRlbV9pZBgDIAEoCRIRCglkaXJlY3Rpb24YBCABKAkSEAoIcmV2aXNpb24YBSABKAQSEgoKZHJhZnRfanNvbhgGIAEoCRIbChN1cGxvYWRlZF9maWxlc19qc29uGAcgASgJEhQKDHJlcXVlc3RfanNvbhgIIAEoCSKGAQoRQ2hhdFF1ZXVlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCRIVCg1zbmFwc2hvdF9qc29uGAMgASgJEhEKCWl0ZW1fanNvbhgEIAEoCRISCgplcnJvcl9jb2RlGAUgASgJEhAKCHJldmlzaW9uGAYgASgEIlIKDkNoYXRRdWV1ZUV2ZW50EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1zbmFwc2hvdF9qc29uGAIgASgJEhAKCHJldmlzaW9uGAMgASgEIoUCCglDaGF0RXZlbnQSOwoEdHlwZRgBIAEoDjItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRFdmVudC5DaGF0RXZlbnRUeXBlEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIMCgRkYXRhGAMgASgJIpMBCg1DaGF0RXZlbnRUeXBlEgkKBVRPS0VOEAASDAoIVEhJTktJTkcQARINCglUT09MX0NBTEwQAhIPCgtUT09MX1JFU1VMVBADEggKBERPTkUQBBIJCgVFUlJPUhAFEg8KC1RPT0xfU1RBVFVTEAYSEQoNSE9TVEVEX1NFQVJDSBAHEhAKDFVTRVJfTUVTU0FHRRAIIrwBChBDaGF0Q29udHJvbEV2ZW50EhIKCnJlcXVlc3RfaWQYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJEhEKCXJ1bl9lcG9jaBgEIAEoAxIMCgR0eXBlGAUgASgJEg0KBXN0YXRlGAYgASgJEhIKCmVycm9yX2NvZGUYByABKAkSDwoHbWVzc2FnZRgIIAEoCRILCgNzZXEYCSABKAMi/AEKE0NoYXRSdW50aW1lU25hcHNob3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCRIRCgl3b3JrZXJfaWQYBCABKAkSDQoFc3RhdGUYBSABKAkSCwoDY3dkGAYgASgJEhIKCnVwZGF0ZWRfYXQYByABKAMSEAoIcmV2aXNpb24YCCABKAMSFAoMZW50cmllc19qc29uGAkgASgJEhMKC3Rvb2xfc3RhdHVzGAogASgJEiEKGXRvb2xfc3RhdHVzX2lzX2NvbXBhY3Rpb24YCyABKAgi6gEKElJ1bnRpbWVTdGF0dXNFdmVudBIRCgl3b3JrZXJfaWQYASABKAkSDQoFc3RhdGUYAiABKAkSDwoHdmlzaWJsZRgDIAEoCBIYChBhY3RpdmVfcnVuX2NvdW50GAQgASgNEhEKCXRpbWVzdGFtcBgFIAEoAxI4CgthY3RpdmVfcnVucxgGIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQSOgoNZmluaXNoZWRfcnVucxgHIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQigAEKDUNoYXRSdW5SZXBvcnQSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRINCgVzdGF0ZRgDIAEoCRISCgplcnJvcl9jb2RlGAQgASgJEg8KB21lc3NhZ2UYBSABKAkSEgoKdXBkYXRlZF9hdBgGIAEoAyJHChFDcm9uTWFuYWdlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHdGFza19pZBgCIAEoCRIRCgl0YXNrX2pzb24YAyABKAkiOQoSQ3Jvbk1hbmFnZVJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSJVChJIaXN0b3J5TGlzdFJlcXVlc3QSDAoEcGFnZRgBIAEoBRIRCglwYWdlX3NpemUYAiABKAUSCwoDY3dkGAMgASgJEhEKCWN3ZF9lbXB0eRgEIAEoCCJsChNIaXN0b3J5TGlzdFJlc3BvbnNlEkAKDWNvbnZlcnNhdGlvbnMYASADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5EhMKC3RvdGFsX2NvdW50GAIgASgFIooCChNDb252ZXJzYXRpb25TdW1tYXJ5EgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhIKCmNyZWF0ZWRfYXQYAyABKAMSEgoKdXBkYXRlZF9hdBgEIAEoAxIVCg1tZXNzYWdlX2NvdW50GAUgASgFEhMKC3Byb3ZpZGVyX2lkGAYgASgJEg0KBW1vZGVsGAcgASgJEhIKCnNlc3Npb25faWQYCCABKAkSCwoDY3dkGAkgASgJEhEKCWlzX3Bpbm5lZBgKIAEoCBIRCglwaW5uZWRfYXQYCyABKAMSEQoJaXNfc2hhcmVkGAwgASgIEhsKE3NlbGVjdGVkX21vZGVsX2pzb24YDSABKAkiQgoRSGlzdG9yeUdldFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBSLUAQoSSGlzdG9yeUdldFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5IoUBChRIaXN0b3J5UHJlZml4UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFAoMbWF4X21lc3NhZ2VzGAIgASgFEj4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZiLXAQoVSGlzdG9yeVByZWZpeFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij4KFEhpc3RvcnlSZW5hbWVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCSJYChVIaXN0b3J5UmVuYW1lUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSJvChRIaXN0b3J5QnJhbmNoUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSPgoQYmFzZV9tZXNzYWdlX3JlZhgCIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmIlgKFUhpc3RvcnlCcmFuY2hSZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij8KEUhpc3RvcnlQaW5SZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIRCglpc19waW5uZWQYAiABKAgiVQoSSGlzdG9yeVBpblJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkikgEKEkhpc3RvcnlTaGFyZVN0YXR1cxIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBINCgV0b2tlbhgDIAEoCRISCgpjcmVhdGVkX2F0GAQgASgDEhIKCnVwZGF0ZWRfYXQYBSABKAMSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgGIAEoCCIxChZIaXN0b3J5U2hhcmVHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSJSChdIaXN0b3J5U2hhcmVHZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyJ8ChZIaXN0b3J5U2hhcmVTZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEiAKE3JlZGFjdF90b29sX2NvbnRlbnQYAyABKAhIAIgBAUIWChRfcmVkYWN0X3Rvb2xfY29udGVudCJSChdIaXN0b3J5U2hhcmVTZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyIrChpIaXN0b3J5U2hhcmVSZXNvbHZlUmVxdWVzdBINCgV0b2tlbhgBIAEoCSLIAQobSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSPwoMY29udmVyc2F0aW9uGAQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIbChNyZWRhY3RfdG9vbF9jb250ZW50GAUgASgIIhgKFkhpc3RvcnlXb3JrZGlyc1JlcXVlc3QiVQoVSGlzdG9yeVdvcmtkaXJTdW1tYXJ5EgwKBHBhdGgYASABKAkSGgoSY29udmVyc2F0aW9uX2NvdW50GAIgASgFEhIKCnVwZGF0ZWRfYXQYAyABKAMiWAoXSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2USPQoId29ya2RpcnMYASADKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5V29ya2RpclN1bW1hcnkiLwoUSGlzdG9yeURlbGV0ZVJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJIhcKFUhpc3RvcnlEZWxldGVSZXNwb25zZSJ6ChBIaXN0b3J5U3luY0V2ZW50EgwKBGtpbmQYASABKAkSPwoMY29udmVyc2F0aW9uGAIgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIXCg9jb252ZXJzYXRpb25faWQYAyABKAkiFQoTUHJvdmlkZXJMaXN0UmVxdWVzdCIuChRQcm92aWRlckxpc3RSZXNwb25zZRIWCg5wcm92aWRlcnNfanNvbhgBIAEoCSIUChJTZXR0aW5nc0dldFJlcXVlc3QiLAoTU2V0dGluZ3NHZXRSZXNwb25zZRIVCg1zZXR0aW5nc19qc29uGAEgASgJIi4KFVNldHRpbmdzVXBkYXRlUmVxdWVzdBIVCg1zZXR0aW5nc19qc29uGAEgASgJIjsKFlNldHRpbmdzVXBkYXRlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCSI+CiBTZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdBIMCgRob3N0GAEgASgJEgwKBHBvcnQYAiABKA0iNAohU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKA0iKgoRU2V0dGluZ3NTeW5jRXZlbnQSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIXChVTa2lsbEZpbGVzTGlzdFJlcXVlc3QiTAoWU2tpbGxGaWxlc0xpc3RSZXNwb25zZRIQCghyb290X2RpchgBIAEoCRINCgVwYXRocxgCIAMoCRIRCgl0cnVuY2F0ZWQYAyABKAgiKAoYU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkiPgoZU2tpbGxNZXRhZGF0YVJlYWRSZXNwb25zZRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJIkQKFFNraWxsVGV4dFJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkSDgoGb2Zmc2V0GAIgASgNEg4KBmxlbmd0aBgDIAEoDSI7ChVTa2lsbFRleHRSZWFkUmVzcG9uc2USDwoHY29udGVudBgBIAEoCRIRCgl0cnVuY2F0ZWQYAiABKAgiKgoSU2tpbGxNYW5hZ2VSZXF1ZXN0EhQKDHBheWxvYWRfanNvbhgBIAEoCSIqChNTa2lsbE1hbmFnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIncKFkZpbGVNZW50aW9uTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRITCgttYXhfcmVzdWx0cxgCIAEoDRINCgVxdWVyeRgDIAEoCRIYCgtzaG93X2hpZGRlbhgEIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI+ChBGaWxlTWVudGlvbkVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiZQoXRmlsZU1lbnRpb25MaXN0UmVzcG9uc2USNwoHZW50cmllcxgBIAMoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uRW50cnkSEQoJdHJ1bmNhdGVkGAIgASgIIj8KBkZzUm9vdBIKCgJpZBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkSDQoFbGFiZWwYBCABKAkiEAoORnNSb290c1JlcXVlc3QiPgoPRnNSb290c1Jlc3BvbnNlEisKBXJvb3RzGAEgAygLMhwubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290IjYKEUZzTGlzdERpcnNSZXF1ZXN0EgwKBHBhdGgYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0iKAoKRnNEaXJFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkiaAoSRnNMaXN0RGlyc1Jlc3BvbnNlEgwKBHBhdGgYASABKAkSMQoHZW50cmllcxgCIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGlyRW50cnkSEQoJdHJ1bmNhdGVkGAMgASgIIjwKHEZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3QSDgoGcGFyZW50GAEgASgJEgwKBG5hbWUYAiABKAkiLQodRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2USDAoEcGF0aBgBIAEoCSKMAQoNRnNMaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEhgKC3Nob3dfaGlkZGVuGAYgASgISACIAQFCDgoMX3Nob3dfaGlkZGVuIjkKC0ZzTGlzdEVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiuQEKDkZzTGlzdFJlc3BvbnNlEgwKBHBhdGgYASABKAkSEAoIaGFzX3BhdGgYAiABKAgSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEg0KBXRvdGFsGAYgASgNEhAKCGhhc19tb3JlGAcgASgIEjIKB2VudHJpZXMYCCADKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3RFbnRyeSI6ChlGc1JlYWRFZGl0YWJsZVRleHRSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSKMAQoaRnNSZWFkRWRpdGFibGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgJEhAKCG10aW1lX21zGAMgASgEEhQKDGNvbnRlbnRfaGFzaBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgEEhMKC3RvdGFsX2xpbmVzGAYgASgEIjwKG0ZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiiQEKHEZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDAoEZGF0YRgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEhAKCG10aW1lX21zGAUgASgEEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSLbAQoTQ2hhdEZpbGVPcGVuUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHd29ya2RpchgCIAEoCRIMCgRwYXRoGAMgASgJEg4KBnNvdXJjZRgEIAEoCRIRCgRsaW5lGAUgASgNSACIAQESFQoIZW5kX2xpbmUYBiABKA1IAYgBARITCgZjb2x1bW4YByABKA1IAogBARIcChRvcGVuX2luX2ZpbGVfbWFuYWdlchgIIAEoCEIHCgVfbGluZUILCglfZW5kX2xpbmVCCQoHX2NvbHVtbiLOAQoUQ2hhdEZpbGVPcGVuUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEgwKBGtpbmQYAiABKAkSDwoHd29ya2RpchgDIAEoCRIMCgRwYXRoGAQgASgJEhEKBGxpbmUYBSABKA1IAIgBARIVCghlbmRfbGluZRgGIAEoDUgBiAEBEhMKBmNvbHVtbhgHIAEoDUgCiAEBEhkKEW91dHNpZGVfd29ya3NwYWNlGAggASgIQgcKBV9saW5lQgsKCV9lbmRfbGluZUIJCgdfY29sdW1uIs4BChJGc1dyaXRlVGV4dFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg8KB2NvbnRlbnQYAyABKAkSDAoEbW9kZRgEIAEoCRIZChFleHBlY3RlZF9tdGltZV9tcxgFIAEoBBIdChVleHBlY3RlZF9jb250ZW50X2hhc2gYBiABKAkSHQoVaGFzX2V4cGVjdGVkX210aW1lX21zGAcgASgIEiEKGWhhc19leHBlY3RlZF9jb250ZW50X2hhc2gYCCABKAginQEKE0ZzV3JpdGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIMCgRtb2RlGAIgASgJEhYKDmV4aXN0ZWRfYmVmb3JlGAMgASgIEhUKDWJ5dGVzX3dyaXR0ZW4YBCABKAQSEAoIbXRpbWVfbXMYBSABKAQSFAoMY29udGVudF9oYXNoGAYgASgJEhMKC3RvdGFsX2xpbmVzGAcgASgEIjMKEkZzQ3JlYXRlRGlyUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiMQoTRnNDcmVhdGVEaXJSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiRgoPRnNSZW5hbWVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEQoJZnJvbV9wYXRoGAIgASgJEg8KB3RvX3BhdGgYAyABKAkiQQoQRnNSZW5hbWVSZXNwb25zZRIRCglmcm9tX3BhdGgYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJIjAKD0ZzRGVsZXRlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiLgoQRnNEZWxldGVSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiIAoLUGluZ1JlcXVlc3QSEQoJdGltZXN0YW1wGAEgASgDIiEKDFBvbmdSZXNwb25zZRIRCgl0aW1lc3RhbXAYASABKAMiLgoNRXJyb3JSZXNwb25zZRIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkivgEKFVByb3ZpZGVyTW9kZWxzUmVxdWVzdBIVCg1wcm92aWRlcl90eXBlGAEgASgJEhAKCGJhc2VfdXJsGAIgASgJEg8KB2FwaV9rZXkYAyABKAkSGAoQdXNlX3N5c3RlbV9wcm94eRgEIAEoCBISCgptb2RlbHNfdXJsGAUgASgJEhMKC3Byb3ZpZGVyX2lkGAYgASgJEhgKC2lzX2Z1bGxfdXJsGAcgASgISACIAQFCDgoMX2lzX2Z1bGxfdXJsIi0KFlByb3ZpZGVyTW9kZWxzUmVzcG9uc2USEwoLbW9kZWxzX2pzb24YASABKAkiUQoUUHJvdmlkZXJVc2FnZVJlcXVlc3QSEwoLcHJvdmlkZXJfaWQYASABKAkSDwoHcmVmcmVzaBgCIAEoCBITCgtjb25maWdfanNvbhgDIAEoCSIsChVQcm92aWRlclVzYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkiiAEKEENoYXRJbmdyZXNzQmF0Y2gSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIRCglmaXJzdF9zZXEYAyABKAQSOAoHcmVjb3JkcxgEIAMoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzUmVjb3JkIpoCChFDaGF0SW5ncmVzc1JlY29yZBI3CgVkZWx0YRgBIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRGVsdGFIABJBCgpjaGVja3BvaW50GAIgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NDaGVja3BvaW50SAASPQoIdGVybWluYWwYAyABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Rlcm1pbmFsSAASPwoJaGVhcnRiZWF0GAQgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NIZWFydGJlYXRIAEIJCgdwYXlsb2FkIjkKEENoYXRJbmdyZXNzRGVsdGESEgoKZXZlbnRfanNvbhgBIAEoCRIRCgl3b3JrZXJfaWQYAiABKAkiKgoUQ2hhdEluZ3Jlc3NIZWFydGJlYXQSEgoKdXBkYXRlZF9hdBgBIAEoAyLEAQoVQ2hhdEluZ3Jlc3NDaGVja3BvaW50EhoKEmNvdmVyc190aHJvdWdoX3NlcRgBIAEoBBIQCghyZXZpc2lvbhgCIAEoBBIdChVjb21wcmVzc2VkX3Byb2plY3Rpb24YAyABKAwSGgoSdW5jb21wcmVzc2VkX2J5dGVzGAQgASgEEg4KBnNoYTI1NhgFIAEoCRIYChBjb250ZW50X2NvbXBsZXRlGAYgASgIEhgKEGhpc3RvcnlfcmVxdWlyZWQYByABKAgi/AEKE0NoYXRJbmdyZXNzVGVybWluYWwSGgoSY292ZXJzX3Rocm91Z2hfc2VxGAEgASgEEhAKCHJldmlzaW9uGAIgASgEEh0KFWNvbXByZXNzZWRfcHJvamVjdGlvbhgDIAEoDBIaChJ1bmNvbXByZXNzZWRfYnl0ZXMYBCABKAQSDgoGc2hhMjU2GAUgASgJEhgKEGNvbnRlbnRfY29tcGxldGUYBiABKAgSGAoQaGlzdG9yeV9yZXF1aXJlZBgHIAEoCBINCgVzdGF0ZRgIIAEoCRISCgplcnJvcl9jb2RlGAkgASgJEhUKDWVycm9yX21lc3NhZ2UYCiABKAkiTQoRQ2hhdEluZ3Jlc3NSZXN1bWUSOAoEcnVucxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzUnVuUmVzdW1lItUBChRDaGF0SW5ncmVzc1J1blJlc3VtZRIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhcKD3JlcGxheV9mcm9tX3NlcRgDIAEoBBIaChJyZXBsYXlfdGhyb3VnaF9zZXEYBCABKAQSEAoIbmV4dF9zZXEYBSABKAQSHQoVbGF0ZXN0X2NoZWNrcG9pbnRfc2VxGAYgASgEEhQKDHRlcm1pbmFsX3NlcRgHIAEoBBIYChB0ZXJtaW5hbF9wZW5kaW5nGAggASgIIs4BChNDaGF0SW5ncmVzc0ZyYWdtZW50Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSEgoKc291cmNlX3NlcRgDIAEoBBIWCg5mcmFnbWVudF9pbmRleBgEIAEoDRIWCg5mcmFnbWVudF9jb3VudBgFIAEoDRIcChRlbmNvZGVkX3JlY29yZF9jaHVuaxgGIAEoDBIcChRlbmNvZGVkX3JlY29yZF9ieXRlcxgHIAEoBBIOCgZzaGEyNTYYCCABKAki3AIKDkNoYXRJbmdyZXNzQWNrEg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSGQoRY29tbWl0dGVkX3Rocm91Z2gYAyABKAQSFQoNZXhwZWN0ZWRfbmV4dBgEIAEoBBI7CgZhY3Rpb24YBSABKA4yKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0Fjay5BY3Rpb24SGgoSdGVybWluYWxfY29tbWl0dGVkGAYgASgIEhIKCmVycm9yX2NvZGUYByABKAkSFQoNZXJyb3JfbWVzc2FnZRgIIAEoCSJrCgZBY3Rpb24SFgoSQUNUSU9OX1VOU1BFQ0lGSUVEEAASDAoIQ09OVElOVUUQARIYChRSRVBMQVlfRlJPTV9FWFBFQ1RFRBACEhMKD1NFTkRfQ0hFQ0tQT0lOVBADEgwKCFJFSkVDVEVEEAQiPAoUSGlzdG9yeVNldEN3ZFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEgsKA2N3ZBgCIAEoCSJYChVIaXN0b3J5U2V0Q3dkUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSJmChdXb3Jrc3BhY2VSb290R3JhbnREcmFmdBIPCgJpZBgBIAEoCUgAiAEBEg0KBWFsaWFzGAIgASgJEhQKDGRpc3BsYXlfcGF0aBgDIAEoCRIOCgZhY2Nlc3MYBCABKAlCBQoDX2lkItIBChJXb3Jrc3BhY2VSb290R3JhbnQSCgoCaWQYASABKAkSEgoKcHJvamVjdF9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEg0KBWFsaWFzGAQgASgJEhQKDGRpc3BsYXlfcGF0aBgFIAEoCRIWCg5jYW5vbmljYWxfcGF0aBgGIAEoCRIOCgZhY2Nlc3MYByABKAkSDQoFc3RhdGUYCCABKAkSEgoKY3JlYXRlZF9hdBgJIAEoAxISCgp1cGRhdGVkX2F0GAogASgDIpUBChpXb3Jrc3BhY2VSb290R3JhbnRzUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKcHJvamVjdF9pZBgCIAEoCRIUCgxwcm9qZWN0X3BhdGgYAyABKAkSPQoGZ3JhbnRzGAQgAygLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlUm9vdEdyYW50RHJhZnQiVwobV29ya3NwYWNlUm9vdEdyYW50c1Jlc3BvbnNlEjgKBmdyYW50cxgBIAMoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZVJvb3RHcmFudCI8ChdDaGVja3BvaW50RXhwZWN0ZWRFbnRyeRILCgNrZXkYASABKAkSFAoMY3VycmVudF9oYXNoGAIgASgJIqkBChFDaGVja3BvaW50UmVxdWVzdBIOCgZhY3Rpb24YASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhAKCHR1cm5fc2VxGAMgASgEEhgKEGF1dGhvcml6ZWRfcm9vdHMYBCADKAkSPwoIZXhwZWN0ZWQYBSADKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGVja3BvaW50RXhwZWN0ZWRFbnRyeSI5ChJDaGVja3BvaW50UmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJKuUBChhJbXBvcnREaXJlY3RvcnlPcGVyYXRpb24SKgomSU1QT1JUX0RJUkVDVE9SWV9PUEVSQVRJT05fVU5TUEVDSUZJRUQQABIkCiBJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9TVEFSVBABEioKJklNUE9SVF9ESVJFQ1RPUllfT1BFUkFUSU9OX1dSSVRFX0NIVU5LEAISJQohSU1QT1JUX0RJUkVDVE9SWV9PUEVSQVRJT05fQ09NTUlUEAMSJAogSU1QT1JUX0RJUkVDVE9SWV9PUEVSQVRJT05fQUJPUlQQBCrGBAoPVHVubmVsRnJhbWVLaW5kEiEKHVRVTk5FTF9GUkFNRV9LSU5EX1VOU1BFQ0lGSUVEEAASKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX1NUQVJUEAESJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX0JPRFkQAhImCiJUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfRU5EEAMSKQolVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9TVEFSVBAEEigKJFRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfQk9EWRAFEicKI1RVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfRU5EEAYSHQoZVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTBAHEiAKHFRVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfT0sQCBIjCh9UVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMX0VSUk9SEAkSHgoaVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRlJBTUUQChIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19DTE9TRRALEhsKF1RVTk5FTF9GUkFNRV9LSU5EX0VSUk9SEAwSHAoYVFVOTkVMX0ZSQU1FX0tJTkRfQ0FOQ0VMEA0SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUElORxAOEhoKFlRVTk5FTF9GUkFNRV9LSU5EX1BPTkcQDyqBAQoTVHVubmVsV3NNZXNzYWdlVHlwZRImCiJUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1VOU1BFQ0lGSUVEEAASHwobVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9URVhUEAESIQodVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9CSU5BUlkQAkJAWj5naXRodWIuY29tL2xpdmVhZ2VudC9hZ2VudC1nYXRld2F5L2ludGVybmFsL3Byb3RvL3YyO2dhdGV3YXl2MmIGcHJvdG8z"); /** * @generated from message liveagent.gateway.v2.GatewayEnvelope @@ -341,6 +341,12 @@ export type GatewayEnvelope = Message<"liveagent.gateway.v2.GatewayEnvelope"> & */ value: ImportDirectoryRequest; case: "importDirectory"; + } | { + /** + * @generated from field: liveagent.gateway.v2.CheckpointRequest checkpoint = 98; + */ + value: CheckpointRequest; + case: "checkpoint"; } | { case: undefined; value?: undefined }; }; @@ -758,6 +764,12 @@ export type AgentEnvelope = Message<"liveagent.gateway.v2.AgentEnvelope"> & { */ value: ImportDirectoryResponse; case: "importDirectoryResp"; + } | { + /** + * @generated from field: liveagent.gateway.v2.CheckpointResponse checkpoint_resp = 103; + */ + value: CheckpointResponse; + case: "checkpointResp"; } | { case: undefined; value?: undefined }; }; @@ -6256,6 +6268,89 @@ export type WorkspaceRootGrantsResponse = Message<"liveagent.gateway.v2.Workspac export const WorkspaceRootGrantsResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_proto_v2_gateway, 159); +/** + * @generated from message liveagent.gateway.v2.CheckpointExpectedEntry + */ +export type CheckpointExpectedEntry = Message<"liveagent.gateway.v2.CheckpointExpectedEntry"> & { + /** + * @generated from field: string key = 1; + */ + key: string; + + /** + * @generated from field: string current_hash = 2; + */ + currentHash: string; +}; + +/** + * Describes the message liveagent.gateway.v2.CheckpointExpectedEntry. + * Use `create(CheckpointExpectedEntrySchema)` to create a new message. + */ +export const CheckpointExpectedEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_proto_v2_gateway, 160); + +/** + * @generated from message liveagent.gateway.v2.CheckpointRequest + */ +export type CheckpointRequest = Message<"liveagent.gateway.v2.CheckpointRequest"> & { + /** + * "list" | "diff" | "rewind" + * + * @generated from field: string action = 1; + */ + action: string; + + /** + * @generated from field: string conversation_id = 2; + */ + conversationId: string; + + /** + * @generated from field: uint64 turn_seq = 3; + */ + turnSeq: bigint; + + /** + * @generated from field: repeated string authorized_roots = 4; + */ + authorizedRoots: string[]; + + /** + * @generated from field: repeated liveagent.gateway.v2.CheckpointExpectedEntry expected = 5; + */ + expected: CheckpointExpectedEntry[]; +}; + +/** + * Describes the message liveagent.gateway.v2.CheckpointRequest. + * Use `create(CheckpointRequestSchema)` to create a new message. + */ +export const CheckpointRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_proto_v2_gateway, 161); + +/** + * @generated from message liveagent.gateway.v2.CheckpointResponse + */ +export type CheckpointResponse = Message<"liveagent.gateway.v2.CheckpointResponse"> & { + /** + * @generated from field: string action = 1; + */ + action: string; + + /** + * @generated from field: string result_json = 2; + */ + resultJson: string; +}; + +/** + * Describes the message liveagent.gateway.v2.CheckpointResponse. + * Use `create(CheckpointResponseSchema)` to create a new message. + */ +export const CheckpointResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_proto_v2_gateway, 162); + /** * @generated from enum liveagent.gateway.v2.ImportDirectoryOperation */ diff --git a/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs b/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs index dbde8523a..e806a6657 100644 --- a/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs +++ b/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs @@ -240,6 +240,76 @@ test("workspace root grants round-trip through the agent-scoped gateway protocol ); }); +test("checkpoint requests and responses round-trip through the agent-scoped gateway protocol", () => { + const common = { + conversation_id: "conversation-1", + turn_seq: 7, + authorized_roots: ["C:/work/project", "C:/work/shared"], + }; + for (const [type, expectedCount] of [ + ["checkpoint.list", 0], + ["checkpoint.diff", 0], + ["checkpoint.rewind", 1], + ]) { + const encoded = encodeRequestFrame( + `checkpoint-${type}`, + type, + { + ...common, + expected: + expectedCount > 0 + ? [{ key: "C:/work/project\u0001src/a.ts", current_hash: "hash-at-preview" }] + : undefined, + }, + "agent-1", + ); + const frame = decodeClientFrame(encoded); + assert.equal(frame.agentId, "agent-1"); + assert.equal(frame.payload.value.payload.case, "checkpoint"); + assert.equal(frame.payload.value.payload.value.action, type.slice("checkpoint.".length)); + assert.equal(frame.payload.value.payload.value.conversationId, common.conversation_id); + assert.equal(Number(frame.payload.value.payload.value.turnSeq), common.turn_seq); + assert.deepEqual(frame.payload.value.payload.value.authorizedRoots, common.authorized_roots); + assert.equal(frame.payload.value.payload.value.expected.length, expectedCount); + } + + const decoded = decodeServerFrame( + roundtrip( + serverFrame({ + request_id: "checkpoint-list", + agent_id: "agent-1", + agent_response: { + checkpoint_resp: { + action: "list", + result_json: JSON.stringify([ + { + turnSeq: 7, + turnId: "message-7", + fileCount: 1, + dirCount: 0, + incomplete: false, + firstCapturedAt: 1700000000000, + }, + ]), + }, + }, + }), + ), + { agentOnline: true }, + ); + assert.equal(decoded.kind, "response"); + assert.deepEqual(decoded.payload, [ + { + turnSeq: 7, + turnId: "message-7", + fileCount: 1, + dirCount: 0, + incomplete: false, + firstCapturedAt: 1700000000000, + }, + ]); +}); + test("adapters convert int64/uint64 fields to Number at realistic maxima", () => { // 毫秒时间戳(2100 年)与 MAX_SAFE_INTEGER 边界都必须无损转换。 const year2100Ms = 4102444800000; diff --git a/crates/agent-gui/src-tauri/src/commands/workspace/checkpoint.rs b/crates/agent-gui/src-tauri/src/commands/workspace/checkpoint.rs index 619658be9..d5ab8422c 100644 --- a/crates/agent-gui/src-tauri/src/commands/workspace/checkpoint.rs +++ b/crates/agent-gui/src-tauri/src/commands/workspace/checkpoint.rs @@ -17,7 +17,7 @@ //! 后两类不接受调用方传入,所以不构成新的授权入口;仓库根的向上推导在 //! 主目录与文件系统根处封顶,避免"主目录本身是 dotfiles 仓库"时把整个 //! 主目录变成可写的回退目标。 -//! - turn 身份:TS 侧只传稳定的 turnId(每轮唯一的随机 ID),turn_seq 由 +//! - turn 身份:TS 侧只传稳定的 turnId(用户消息 ID),turn_seq 由 //! 本模块在 INDEX_LOCK 下按会话单调分配——时钟回拨/重复 ID 都不会打乱 //! 回退顺序。UI 展示时间用 firstCapturedAt,不再复用序号。 //! - blob 是原始字节拷贝(不内嵌 JSON),索引是追加式 index.jsonl; @@ -53,7 +53,7 @@ const MAX_RECORDS_PER_CONVERSATION: usize = 10_000; const RECORD_CAP_ERROR_RESERVE: usize = 64; /// TS 侧随 fs 变更命令附带的检查点上下文;缺省(None)表示该调用不捕获。 -/// turnId 是每轮唯一的稳定 ID(与时钟无关),序号由 Rust 侧分配。 +/// turnId 是用户消息的稳定 ID(与时钟无关),序号由 Rust 侧分配。 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CheckpointCtx { @@ -62,7 +62,7 @@ pub struct CheckpointCtx { } /// index.jsonl 里的一条记录(schema v2)。 -/// kind:"file" | "dir" | "error"(捕获失败标记) | "rewind"(回退审计标记)。 +/// kind:"turn" | "file" | "dir" | "error"(捕获失败标记) | "rewind"(回退审计标记)。 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CheckpointRecord { @@ -150,7 +150,8 @@ fn sanitize_conversation_id(id: &str) -> Option { } fn checkpoints_root() -> Result { - let home = dirs::home_dir().ok_or_else(|| "Failed to locate the user home directory".to_string())?; + let home = + dirs::home_dir().ok_or_else(|| "Failed to locate the user home directory".to_string())?; Ok(home.join(".liveagent").join("checkpoints")) } @@ -533,6 +534,53 @@ fn record_capture_skip(ctx: &CheckpointCtx, root: &Path, rel_path: &Path, reason ); } +fn begin_turn_at(dir: &Path, turn_id: &str) -> Result<(), String> { + let turn_id = turn_id.trim(); + if turn_id.is_empty() { + return Err("checkpoint turnId is empty".to_string()); + } + ensure_conversation_dirs(dir)?; + let _guard = INDEX_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let existing = read_index(dir); + if existing.iter().any(|record| record.turn_id == turn_id) { + return Ok(()); + } + if existing.len() + RECORD_CAP_ERROR_RESERVE >= MAX_RECORDS_PER_CONVERSATION { + return Err(format!( + "checkpoint record cap reached ({MAX_RECORDS_PER_CONVERSATION})" + )); + } + let turn_seq = resolve_turn_seq(&existing, turn_id); + append_record( + dir, + &CheckpointRecord { + schema: 2, + turn_seq, + turn_id: turn_id.to_string(), + root: String::new(), + rel_path: String::new(), + kind: "turn".to_string(), + existed_before: false, + blob: None, + size: 0, + mtime_ms: 0, + captured_at: now_ms(), + note: None, + mode: None, + }, + ) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn checkpoint_begin_turn(conversation_id: String, turn_id: String) -> Result<(), String> { + tauri::async_runtime::spawn_blocking(move || { + let dir = conversation_dir(&conversation_id)?; + begin_turn_at(&dir, &turn_id) + }) + .await + .map_err(|e| format!("checkpoint_begin_turn join failed: {e}"))? +} + /// fs 变更命令的捕获入口:尽力而为,失败追加 error 记录 + 日志, /// 绝不阻断文件写入本身。 pub fn capture_pre_image( @@ -590,9 +638,8 @@ fn live_records(records: Vec) -> Vec { /// 会话内可回退的轮列表,按 turn_seq 升序。error 记录不计入文件数, /// 但使该轮标记 incomplete;被回退作废的陈旧轮已由 live_records 剔除。 -fn checkpoint_list_sync(conversation_id: String) -> Result, String> { - let dir = conversation_dir(&conversation_id)?; - let records = live_records(read_index(&dir)); +fn checkpoint_turn_summaries(records: Vec) -> Vec { + let records = live_records(records); let mut turns: Vec = Vec::new(); for record in records { let summary = match turns.iter_mut().find(|t| t.turn_seq == record.turn_seq) { @@ -612,15 +659,22 @@ fn checkpoint_list_sync(conversation_id: String) -> Result summary.dir_count += 1, "error" => summary.incomplete = true, - _ => summary.file_count += 1, + "file" => summary.file_count += 1, + // "turn" 边界记录与未知类型都不计数,只把该轮钉进列表: + // 零文件轮也要成为合法回退点(回退=撤销该轮及之后的一切改动)。 + _ => {} } if record.captured_at < summary.first_captured_at { summary.first_captured_at = record.captured_at; } } - turns.retain(|t| t.file_count > 0 || t.dir_count > 0 || t.incomplete); turns.sort_by_key(|t| t.turn_seq); - Ok(turns) + turns +} + +fn checkpoint_list_sync(conversation_id: String) -> Result, String> { + let dir = conversation_dir(&conversation_id)?; + Ok(checkpoint_turn_summaries(read_index(&dir))) } #[tauri::command(rename_all = "snake_case")] @@ -679,6 +733,9 @@ fn earliest_records_since(dir: &Path, turn_seq: u64) -> (Vec, errors += 1; continue; } + if record.kind == "turn" { + continue; + } let key = record_key(&record.root, &record.rel_path); if seen.iter().any(|p| p == &key) { continue; @@ -1007,11 +1064,7 @@ fn atomic_write(target: &Path, bytes: &[u8]) -> Result<(), String> { .parent() .ok_or_else(|| "target has no parent".to_string())?; fs::create_dir_all(parent).map_err(|e| e.to_string())?; - let tmp = parent.join(format!( - ".ckpt-tmp-{}-{}", - std::process::id(), - now_ms() - )); + let tmp = parent.join(format!(".ckpt-tmp-{}-{}", std::process::id(), now_ms())); fs::write(&tmp, bytes).map_err(|e| e.to_string())?; match fs::rename(&tmp, target) { Ok(()) => Ok(()), @@ -1063,7 +1116,12 @@ fn checkpoint_rewind_code_sync( // 的改动一并当作"陈旧未来"埋掉,前像就再也找不回来了。锁守的是 `()`, // 中毒不代表数据不一致,直接取回内部值,不因此让回退失败。 let _guard = INDEX_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - Ok(rewind_and_mark_at(&dir, turn_seq, &authorized, Some(&expected))) + Ok(rewind_and_mark_at( + &dir, + turn_seq, + &authorized, + Some(&expected), + )) } /// 锁内的"回退 + 写审计/剪枝标记":目录可注入,便于单测覆盖完整/部分 @@ -1173,9 +1231,7 @@ fn rewind_at( Err(e) => result.failed.push(format!("{display}: {e}")), Ok(md) => { if !md.is_file() { - result - .failed - .push(format!("{display}: not a regular file")); + result.failed.push(format!("{display}: not a regular file")); continue; } if let Err(e) = reject_multi_hardlink(&md) { @@ -1296,9 +1352,7 @@ fn classify_worktree_pre_image(abs: &Path) -> Result, String> Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(PreImage::Missing), Err(e) => Err(format!("pre-image stat failed: {e}")), // 符号链接不做前像捕获,与 fs_delete 的语义保持一致。 - Ok(md) if md.file_type().is_symlink() => { - Err("symlink pre-image not captured".to_string()) - } + Ok(md) if md.file_type().is_symlink() => Err("symlink pre-image not captured".to_string()), Ok(md) if md.is_file() => Ok(PreImage::File(None)), Ok(md) if md.is_dir() => Ok(PreImage::Dir), Ok(_) => Err("unsupported file type; pre-image not captured".to_string()), @@ -1393,8 +1447,14 @@ mod tests { fs::write(&file, "v1").unwrap(); // 第 1 轮改写:先捕获前像再改。 - let seq = capture_at(&ckpt, "turn-1", &root, &rel(&file, &root), PreImage::File(None)) - .unwrap(); + let seq = capture_at( + &ckpt, + "turn-1", + &root, + &rel(&file, &root), + PreImage::File(None), + ) + .unwrap(); fs::write(&file, "v2").unwrap(); // 回退到第 1 轮之前应恢复 v1。 @@ -1410,8 +1470,14 @@ mod tests { let ckpt = root.join("ckpt"); let file = root.join("new.txt"); - let seq = - capture_at(&ckpt, "turn-1", &root, &rel(&file, &root), PreImage::Missing).unwrap(); + let seq = capture_at( + &ckpt, + "turn-1", + &root, + &rel(&file, &root), + PreImage::Missing, + ) + .unwrap(); fs::write(&file, "created").unwrap(); let result = rewind_at(&ckpt, seq, &roots(&root), None); @@ -1498,6 +1564,84 @@ mod tests { assert_eq!(s2, s1 + 1); } + #[test] + fn begin_turn_creates_stable_zero_file_boundary() { + let tmp = tempfile::tempdir().unwrap(); + let root = fs::canonicalize(tmp.path()).unwrap(); + let ckpt = root.join("ckpt"); + let file = root.join("a.txt"); + + begin_turn_at(&ckpt, "turn-1").unwrap(); + let records = read_index(&ckpt); + assert_eq!(records.len(), 1); + assert_eq!(records[0].kind, "turn"); + assert_eq!(records[0].turn_seq, 1); + + // 文件捕获发生在轮次开始标记之后时,仍必须绑定同一条用户消息。 + fs::write(&file, "created").unwrap(); + let seq = capture_at( + &ckpt, + "turn-1", + &root, + &rel(&file, &root), + PreImage::Missing, + ) + .unwrap(); + assert_eq!(seq, records[0].turn_seq); + let records = read_index(&ckpt); + assert!(records.iter().any(|record| record.kind == "turn")); + assert!(records + .iter() + .any(|record| record.kind == "file" && record.turn_seq == seq)); + } + + #[test] + fn rewind_from_zero_file_turn_rewinds_later_file_changes() { + let tmp = tempfile::tempdir().unwrap(); + let root = fs::canonicalize(tmp.path()).unwrap(); + let ckpt = root.join("ckpt"); + let file = root.join("later.txt"); + + begin_turn_at(&ckpt, "turn-without-files").unwrap(); + let later_seq = capture_at( + &ckpt, + "later-turn", + &root, + &rel(&file, &root), + PreImage::Missing, + ) + .unwrap(); + fs::write(&file, "created later").unwrap(); + + let result = rewind_at(&ckpt, 1, &roots(&root), None); + assert_eq!(later_seq, 2); + assert_eq!(result.deleted_files, 1); + assert!(!file.exists()); + } + + #[test] + fn turn_boundary_is_not_a_file_rewind_entry() { + let tmp = tempfile::tempdir().unwrap(); + let root = fs::canonicalize(tmp.path()).unwrap(); + let ckpt = root.join("ckpt"); + + begin_turn_at(&ckpt, "turn-only").unwrap(); + let (records, errors) = earliest_records_since(&ckpt, 1); + assert!(records.is_empty()); + assert_eq!(errors, 0); + let summaries = checkpoint_turn_summaries(read_index(&ckpt)); + assert_eq!(summaries.len(), 1); + assert_eq!(summaries[0].turn_id, "turn-only"); + assert_eq!(summaries[0].file_count, 0); + assert_eq!(summaries[0].dir_count, 0); + + let result = rewind_at(&ckpt, 1, &roots(&root), None); + assert_eq!(result.restored_files, 0); + assert_eq!(result.deleted_files, 0); + assert_eq!(result.clean_files, 0); + assert!(result.failed.is_empty()); + } + #[test] fn rewind_marker_is_never_reused_as_a_turn_seq() { let tmp = tempfile::tempdir().unwrap(); @@ -1522,8 +1666,14 @@ mod tests { let dir_path = root.join("subdir"); fs::create_dir_all(&dir_path).unwrap(); - let seq = - capture_at(&ckpt, "turn-1", &root, &rel(&dir_path, &root), PreImage::Dir).unwrap(); + let seq = capture_at( + &ckpt, + "turn-1", + &root, + &rel(&dir_path, &root), + PreImage::Dir, + ) + .unwrap(); fs::remove_dir_all(&dir_path).unwrap(); let result = rewind_at(&ckpt, seq, &roots(&root), None); @@ -1540,8 +1690,14 @@ mod tests { fs::create_dir_all(nested.parent().unwrap()).unwrap(); fs::write(&nested, "v1").unwrap(); - let seq = capture_at(&ckpt, "turn-1", &root, &rel(&nested, &root), PreImage::File(None)) - .unwrap(); + let seq = capture_at( + &ckpt, + "turn-1", + &root, + &rel(&nested, &root), + PreImage::File(None), + ) + .unwrap(); fs::remove_dir_all(root.join("x")).unwrap(); let result = rewind_at(&ckpt, seq, &roots(&root), None); @@ -1617,8 +1773,14 @@ mod tests { let file = sub.join("a.txt"); fs::write(&file, "v1").unwrap(); - let seq = capture_at(&ckpt, "turn-1", &root, &rel(&file, &root), PreImage::File(None)) - .unwrap(); + let seq = capture_at( + &ckpt, + "turn-1", + &root, + &rel(&file, &root), + PreImage::File(None), + ) + .unwrap(); // 捕获后把父目录整个换成指向别处的符号链接。 let elsewhere = root.join("elsewhere"); fs::create_dir_all(&elsewhere).unwrap(); @@ -1775,10 +1937,26 @@ mod tests { fs::write(&a, "aaaa").unwrap(); fs::write(&b, "bbbb").unwrap(); - capture_at_with_limits(&ckpt, "t-1", &root, &rel(&a, &root), PreImage::File(None), 64, 6) - .unwrap(); - capture_at_with_limits(&ckpt, "t-2", &root, &rel(&b, &root), PreImage::File(None), 64, 6) - .unwrap(); + capture_at_with_limits( + &ckpt, + "t-1", + &root, + &rel(&a, &root), + PreImage::File(None), + 64, + 6, + ) + .unwrap(); + capture_at_with_limits( + &ckpt, + "t-2", + &root, + &rel(&b, &root), + PreImage::File(None), + 64, + 6, + ) + .unwrap(); let records = read_index(&ckpt); assert_eq!(records.len(), 2); @@ -1903,12 +2081,29 @@ mod tests { fs::write(&dirty, "v1").unwrap(); fs::write(&clean, "same").unwrap(); - capture_at(&ckpt, "turn-1", &root, &rel(&dirty, &root), PreImage::File(None)).unwrap(); - capture_at(&ckpt, "turn-1", &root, &rel(&clean, &root), PreImage::File(None)).unwrap(); + capture_at( + &ckpt, + "turn-1", + &root, + &rel(&dirty, &root), + PreImage::File(None), + ) + .unwrap(); + capture_at( + &ckpt, + "turn-1", + &root, + &rel(&clean, &root), + PreImage::File(None), + ) + .unwrap(); fs::write(&dirty, "v2").unwrap(); let (records, _) = earliest_records_since(&ckpt, 1); - let entries: Vec<_> = records.iter().map(|r| classify_entry(&ckpt, r, &roots(&root))).collect(); + let entries: Vec<_> = records + .iter() + .map(|r| classify_entry(&ckpt, r, &roots(&root))) + .collect(); let dirty_entry = entries .iter() .find(|e| e.path.ends_with("dirty.txt")) @@ -2057,8 +2252,14 @@ mod tests { fs::write(&dirty, "v1").unwrap(); fs::write(&untouched, "same").unwrap(); - let seq = - capture_at(&ckpt, "turn-1", &root, &rel(&dirty, &root), PreImage::File(None)).unwrap(); + let seq = capture_at( + &ckpt, + "turn-1", + &root, + &rel(&dirty, &root), + PreImage::File(None), + ) + .unwrap(); capture_at( &ckpt, "turn-1", diff --git a/crates/agent-gui/src-tauri/src/lib.rs b/crates/agent-gui/src-tauri/src/lib.rs index 39d7cfcd8..1271b1442 100644 --- a/crates/agent-gui/src-tauri/src/lib.rs +++ b/crates/agent-gui/src-tauri/src/lib.rs @@ -86,6 +86,7 @@ macro_rules! app_invoke_handler { commands::fs::fs_grep, commands::fs::fs_mention_list, // 会话检查点(rewind) + commands::checkpoint::checkpoint_begin_turn, commands::checkpoint::checkpoint_list, commands::checkpoint::checkpoint_diff_stats, commands::checkpoint::checkpoint_rewind_code, diff --git a/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs b/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs index 0953cc1a6..2f58b0e16 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs @@ -796,6 +796,19 @@ impl GatewayController { Err(error) => self.send_error_response(request_id, 400, error).await, } } + Some(proto::gateway_envelope::Payload::Checkpoint(request)) => { + match gateway_bridge::handle_checkpoint(request).await { + Ok(response) => { + self.send_agent_envelope(proto::AgentEnvelope { + request_id, + timestamp: now_unix_seconds(), + payload: Some(proto::agent_envelope::Payload::CheckpointResp(response)), + }) + .await + } + Err(error) => self.send_error_response(request_id, 400, error).await, + } + } Some(proto::gateway_envelope::Payload::FsListDirs(request)) => { match gateway_bridge::handle_fs_list_dirs(request).await { Ok(response) => { diff --git a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs index b8b92c598..3093b2b87 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs @@ -7,6 +7,7 @@ use crate::commands::{ chat_file_links::open_chat_file_link_for_conversation, chat_history, chat_history::ChatHistoryMessageRef, + checkpoint, fs::{ fs_create_dir_sync, fs_delete_sync, fs_list_dirs_sync, fs_list_sync, fs_mention_list_sync, fs_read_editable_text_sync, fs_read_workspace_image_sync, fs_rename_sync, fs_roots_sync, @@ -67,6 +68,52 @@ pub async fn handle_provider_usage( provider_usage_response(result) } +pub async fn handle_checkpoint( + request: proto::CheckpointRequest, +) -> Result { + let action = request.action.trim().to_string(); + let result_json = match action.as_str() { + "list" => { + serde_json::to_string(&checkpoint::checkpoint_list(request.conversation_id).await?) + .map_err(|error| format!("serialize checkpoint list failed: {error}"))? + } + "diff" => serde_json::to_string( + &checkpoint::checkpoint_diff_stats( + request.conversation_id, + request.turn_seq, + request.authorized_roots, + ) + .await?, + ) + .map_err(|error| format!("serialize checkpoint diff failed: {error}"))?, + "rewind" => { + let expected = request + .expected + .into_iter() + .map(|entry| checkpoint::CheckpointExpectedEntry { + key: entry.key, + current_hash: entry.current_hash, + }) + .collect(); + serde_json::to_string( + &checkpoint::checkpoint_rewind_code( + request.conversation_id, + request.turn_seq, + request.authorized_roots, + expected, + ) + .await?, + ) + .map_err(|error| format!("serialize checkpoint rewind failed: {error}"))? + } + _ => return Err(format!("unsupported checkpoint action: {action}")), + }; + Ok(proto::CheckpointResponse { + action, + result_json, + }) +} + #[derive(Debug, Deserialize)] struct HistorySharedListArgs { page: i64, @@ -856,14 +903,16 @@ pub async fn handle_fs_rename( pub async fn handle_fs_delete( request: proto::FsDeleteRequest, ) -> Result { - tauri::async_runtime::spawn_blocking(move || fs_delete_sync(request.workdir, request.path, None)) - .await - .map_err(|e| format!("gateway fs delete join failed: {e}"))? - .map_err(|e| e.message) - .map(|response| proto::FsDeleteResponse { - path: response.path, - kind: response.kind, - }) + tauri::async_runtime::spawn_blocking(move || { + fs_delete_sync(request.workdir, request.path, None) + }) + .await + .map_err(|e| format!("gateway fs delete join failed: {e}"))? + .map_err(|e| e.message) + .map(|response| proto::FsDeleteResponse { + path: response.path, + kind: response.kind, + }) } pub async fn handle_git_request( diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index 3eb807703..71821d149 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -16,6 +16,7 @@ import { useWorkspaceOverlays } from "@liveagent/ui/components/workspace-editor/ import { WorkspaceOverlayHost } from "@liveagent/ui/components/workspace-editor/WorkspaceOverlayHost"; import { useLocale } from "@liveagent/ui/i18n/index"; import { getAutomationState, useAutomation } from "@liveagent/ui/lib/automation/index"; +import { formatCheckpointRewoundNotification } from "@liveagent/ui/lib/chat/checkpointRewind"; import { useChangedFilesActions } from "@liveagent/ui/lib/chat/useChangedFilesActions"; import { useChatFileLinkNavigation } from "@liveagent/ui/lib/chat/useChatFileLinkNavigation"; import { @@ -105,7 +106,7 @@ import { usePendingUploads, } from "./chat"; import type { ChatPageProps } from "./chat/chatPageTypes"; -import { CheckpointRewindMenu } from "./chat/components/CheckpointRewindMenu"; +import { DesktopCheckpointRewindProvider } from "./chat/components/DesktopCheckpointRewindProvider"; import { CurrentTaskProgress } from "./chat/components/CurrentTaskProgress"; import { PendingToolApprovalBar } from "./chat/components/PendingToolApprovalBar"; import { useComposerDraftCache } from "./chat/composer/useComposerDraftCache"; @@ -1736,31 +1737,6 @@ export function ChatPage(props: ChatPageProps) { onOpenSidebar={handleOpenSidebar} trailingActions={ <> - { - // 显式回退通知:让用户明确知道工作区刚被回退过。文件工具缓存 - // 无需手动失效——注册表与 fileState 每用户轮都会重建。 - // - // 已知残留:压缩摘要里的 fileLedger 是持久化在历史里的,不随轮次 - // 重建,回退后仍会列出那些路径。账本语义是"曾被触碰的路径",不断言 - // 当前内容,所以不算失真;真正会过时的是摘要正文里模型写的完成情况, - // 那要改写已落库的摘要才能修,不在本功能范围内。 - const zhLocale = locale === "zh-CN"; - const summary = zhLocale - ? `已回退代码:恢复 ${info.restoredFiles} 个、删除 ${info.deletedFiles} 个${info.conflicts > 0 ? `,冲突跳过 ${info.conflicts} 个` : ""}${info.failed > 0 ? `,失败 ${info.failed} 个` : ""}${info.captureErrors > 0 ? `,${info.captureErrors} 个无前像未回退` : ""}` - : `Code rewound: restored ${info.restoredFiles}, deleted ${info.deletedFiles}${info.conflicts > 0 ? `, ${info.conflicts} conflict(s) skipped` : ""}${info.failed > 0 ? `, ${info.failed} failed` : ""}${info.captureErrors > 0 ? `, ${info.captureErrors} without pre-image` : ""}`; - addNotify( - info.failed > 0 || info.conflicts > 0 || info.captureErrors > 0 - ? "error" - : "success", - summary, - ); - }} - /> - + project={activeWorkspaceProject} + disabled={!currentConversationId || isSending} + onRewound={(info) => { + // 显式回退通知:让用户明确知道工作区刚被回退过。文件工具缓存 + // 无需手动失效——注册表与 fileState 每用户轮都会重建。 + // + // 已知残留:压缩摘要里的 fileLedger 是持久化在历史里的,不随轮次 + // 重建,回退后仍会列出那些路径。账本语义是"曾被触碰的路径",不断言 + // 当前内容,所以不算失真;真正会过时的是摘要正文里模型写的完成情况, + // 那要改写已落库的摘要才能修,不在本功能范围内。 + const notice = formatCheckpointRewoundNotification( + info, + locale === "zh-CN", + ); + addNotify(notice.level, notice.message); + }} + > + + } composer={ diff --git a/crates/agent-gui/src/pages/chat/components/CheckpointRewindMenu.tsx b/crates/agent-gui/src/pages/chat/components/CheckpointRewindMenu.tsx deleted file mode 100644 index 0b4da703a..000000000 --- a/crates/agent-gui/src/pages/chat/components/CheckpointRewindMenu.tsx +++ /dev/null @@ -1,332 +0,0 @@ -import { History, Loader2 } from "@liveagent/ui/components/IconSet"; -import { Button } from "@liveagent/ui/components/ui/button"; -import { useConfirmDialog } from "@liveagent/ui/components/ui/confirm-dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@liveagent/ui/components/ui/dropdown-menu"; -import { useLocale } from "@liveagent/ui/i18n/index"; -import { invoke } from "@tauri-apps/api/core"; -import { useState } from "react"; -import type { WorkspaceProject } from "../../../lib/settings"; -import { listWorkspaceRootGrants } from "../../../lib/workspaceRootGrants"; - -type CheckpointTurnSummary = { - turnSeq: number; - turnId: string; - fileCount: number; - dirCount: number; - /** 该轮存在捕获失败记录(前像不完整),回退可能遗漏部分文件。 */ - incomplete: boolean; - firstCapturedAt: number; -}; - -type CheckpointDiffStats = { - turnSeq: number; - restoreFiles: number; - deleteFiles: number; - cleanFiles: number; - skippedDirs: number; - missingBlobs: number; - /** 根已不在当前授权工作区集合内、或路径链上出现符号链接的条目:一律不回退。 */ - unresolvableFiles: number; - captureErrors: number; - entries: { path: string; key: string; action: string; currentHash?: string }[]; -}; - -type CheckpointRewindResult = { - turnSeq: number; - restoredFiles: number; - deletedFiles: number; - cleanFiles: number; - skippedDirs: number; - /** 目标范围内捕获阶段就失败的记录数:这些文件没有前像,回退没有碰它们。 */ - captureErrors: number; - /** 预览后被外部修改、被跳过未覆盖的文件(冲突检测)。 */ - conflicts: string[]; - failed: string[]; -}; - -// 仅覆盖 Write/Edit/Delete 三个文件工具的改动;Bash 等 shell 写入不在检查点内。 -// 桌面端专属入口:检查点数据只存在于桌面本机,WebUI 暂不提供(P2)。 -export function CheckpointRewindMenu(props: { - conversationId: string; - /** 当前会话的工作区根:授权集合的基准项。 */ - workspaceRoot?: string; - /** 当前激活项目:用于取额外授权根(workspace root grants)。 */ - project?: Pick | null; - disabled?: boolean; - /** 回退完成后回调(通知/转录记录由宿主页面处理)。 */ - onRewound?: (info: { - turnSeq: number; - restoredFiles: number; - deletedFiles: number; - conflicts: number; - failed: number; - /** 捕获阶段就失败的文件数:没有前像,回退没有碰它们。 */ - captureErrors: number; - }) => void; -}) { - const { conversationId, workspaceRoot, project, disabled, onRewound } = props; - const { locale } = useLocale(); - const zh = locale === "zh-CN"; - const { confirm, dialog } = useConfirmDialog(); - const [turns, setTurns] = useState(null); - const [loading, setLoading] = useState(false); - const [busyTurn, setBusyTurn] = useState(null); - const [open, setOpen] = useState(false); - - const loadTurns = async () => { - setLoading(true); - try { - const list = await invoke("checkpoint_list", { - conversation_id: conversationId, - }); - setTurns(list); - } catch { - setTurns([]); - } finally { - setLoading(false); - } - }; - - // 回退授权的唯一来源:当前会话工作区根 + 仍处于 active 且可写的额外授权根。 - // 后端只认这个集合里的 root,记录里存的绝对路径本身不构成授权。 - // - // access 必须一并过滤:回退是写操作(覆盖/删除),只读根不该被写。普通 - // 文件工具把 access 一路带到 pathUtils 的 canMutate 门禁上拦,而这里只往 - // 后端传路径、access 当场就丢了,所以这道门只能在这一步补上。 - const resolveAuthorizedRoots = async () => { - const roots: string[] = []; - const push = (raw?: string | null) => { - const value = raw?.trim(); - if (value && !roots.includes(value)) roots.push(value); - }; - push(workspaceRoot); - if (project) { - try { - const grants = await listWorkspaceRootGrants(project); - for (const grant of grants) { - if (grant.state === "active" && grant.access === "write") push(grant.canonicalPath); - } - } catch { - // 取不到额外授权根时只保留工作区根:宁可少回退,不可越权写入。 - } - } - return roots; - }; - - const rewindTo = async (turn: CheckpointTurnSummary) => { - const turnSeq = turn.turnSeq; - setBusyTurn(turnSeq); - try { - const authorizedRoots = await resolveAuthorizedRoots(); - const stats = await invoke("checkpoint_diff_stats", { - conversation_id: conversationId, - turn_seq: turnSeq, - authorized_roots: authorizedRoots, - }); - const parts: string[] = []; - if (stats.restoreFiles > 0) - parts.push( - zh ? `恢复 ${stats.restoreFiles} 个文件` : `restore ${stats.restoreFiles} file(s)`, - ); - if (stats.deleteFiles > 0) - parts.push(zh ? `删除 ${stats.deleteFiles} 个文件` : `delete ${stats.deleteFiles} file(s)`); - if (stats.cleanFiles > 0) - parts.push( - zh ? `${stats.cleanFiles} 个文件已一致` : `${stats.cleanFiles} file(s) unchanged`, - ); - if (stats.skippedDirs > 0) - parts.push( - zh - ? `${stats.skippedDirs} 个目录删除不可恢复` - : `${stats.skippedDirs} deleted dir(s) not restorable`, - ); - if (stats.missingBlobs > 0) - parts.push( - zh ? `${stats.missingBlobs} 个前像缺失` : `${stats.missingBlobs} blob(s) missing`, - ); - if (stats.unresolvableFiles > 0) - parts.push( - zh - ? `${stats.unresolvableFiles} 个路径已不可回退(根未授权或路径含符号链接)` - : `${stats.unresolvableFiles} path(s) not rewindable (root unauthorized or symlinked)`, - ); - if (stats.captureErrors > 0 || turn.incomplete) - parts.push( - zh - ? `⚠ 该轮有 ${Math.max(stats.captureErrors, 1)} 次前像捕获失败,回退可能不完整` - : `⚠ ${Math.max(stats.captureErrors, 1)} pre-image capture failure(s); rewind may be incomplete`, - ); - const actionable = stats.entries.filter( - (entry) => entry.action === "restore" || entry.action === "delete", - ); - // 检查点只记录 agent 工具写入前的前像,编辑器/文件树里的手改既不入账、 - // 也无法与工具写入区分。回退按前像整体覆盖,手改会被一并抹掉,先说清楚。 - if (actionable.length > 0) - parts.push( - zh - ? "手动编辑(编辑器/文件树)不在检查点内,会被一并覆盖" - : "Manual edits (editor / file tree) are not checkpointed and will be overwritten", - ); - const detailPaths = actionable.map((entry) => entry.path); - const confirmed = await confirm({ - title: zh ? "回退代码到此轮开始前" : "Rewind code to before this turn", - subtitle: new Date(turn.firstCapturedAt).toLocaleString(), - description: - parts.length > 0 - ? parts.join(zh ? "," : ", ") - : zh - ? "没有需要回退的改动" - : "Nothing to rewind", - detail: detailPaths.length > 0 ? detailPaths.join("\n") : undefined, - confirmLabel: zh ? "回退" : "Rewind", - cancelLabel: zh ? "取消" : "Cancel", - tone: "warning", - }); - if (!confirmed) return; - // 把预览时的现状哈希传回后端,回退前逐个复核:预览到执行之间被外部 - // 修改的文件会被跳过并报告为冲突,绝不覆盖(TOCTOU 防护)。 - // 必须回传全部可解析条目(含 clean)——后端对缺哈希的条目一律判冲突, - // 只带 restore/delete 会让确认期间被手改的 clean 文件被静默覆盖。 - const expected = stats.entries.flatMap((entry) => - entry.currentHash == null ? [] : [{ key: entry.key, currentHash: entry.currentHash }], - ); - const result = await invoke("checkpoint_rewind_code", { - conversation_id: conversationId, - turn_seq: turnSeq, - authorized_roots: authorizedRoots, - expected, - }); - onRewound?.({ - turnSeq, - restoredFiles: result.restoredFiles, - deletedFiles: result.deletedFiles, - conflicts: result.conflicts.length, - failed: result.failed.length, - captureErrors: result.captureErrors, - }); - if ( - result.failed.length > 0 || - result.conflicts.length > 0 || - result.captureErrors > 0 || - result.skippedDirs > 0 - ) { - const issueLines = [ - ...result.conflicts.map((path) => - zh ? `冲突(已跳过): ${path}` : `conflict (skipped): ${path}`, - ), - ...result.failed.map((path) => (zh ? `失败: ${path}` : `failed: ${path}`)), - ]; - // 捕获缺口/不可恢复目录没有具体路径列表,单独一行说明。 - if (result.captureErrors > 0) - issueLines.push( - zh - ? `该轮有 ${result.captureErrors} 个文件没有前像(捕获失败),未被回退` - : `${result.captureErrors} file(s) had no pre-image (capture failed) and were not rewound`, - ); - if (result.skippedDirs > 0) - issueLines.push( - zh - ? `${result.skippedDirs} 个被删除目录无法恢复` - : `${result.skippedDirs} deleted dir(s) could not be restored`, - ); - await confirm({ - title: zh ? "回退部分未完成" : "Rewind partially completed", - description: zh - ? `已恢复 ${result.restoredFiles} 个、删除 ${result.deletedFiles} 个;冲突跳过 ${result.conflicts.length} 个、失败 ${result.failed.length} 个` - : `Restored ${result.restoredFiles}, deleted ${result.deletedFiles}; ${result.conflicts.length} conflict(s) skipped, ${result.failed.length} failed`, - detail: issueLines.join("\n"), - confirmLabel: zh ? "知道了" : "OK", - cancelLabel: "", - hideCancel: true, - tone: "destructive", - }); - } - } catch (error) { - await confirm({ - title: zh ? "回退失败" : "Rewind failed", - description: String(error), - confirmLabel: zh ? "知道了" : "OK", - cancelLabel: "", - hideCancel: true, - tone: "destructive", - }); - } finally { - setBusyTurn(null); - } - }; - - const title = zh ? "回退代码改动" : "Rewind code changes"; - return ( - <> - { - setOpen(next); - if (next) void loadTurns(); - }} - > - - } - > - - - - - {title} - - {loading ? ( -
- -
- ) : !turns || turns.length === 0 ? ( -
- {zh ? "本会话暂无文件改动检查点" : "No file-change checkpoints in this conversation"} -
- ) : ( - turns.map((turn) => ( - void rewindTo(turn)} - className="flex items-center justify-between gap-3" - > - {new Date(turn.firstCapturedAt).toLocaleString()} - - {busyTurn === turn.turnSeq ? ( - - ) : ( - `${turn.fileCount}${zh ? " 个文件" : " file(s)"}${turn.incomplete ? " ⚠" : ""}` - )} - - - )) - )} -
- {zh - ? "仅覆盖文件工具的写入/编辑/删除;Shell 命令产生的改动不在回退范围。" - : "Covers file-tool write/edit/delete only; shell-made changes are not tracked."} -
-
-
- {dialog} - - ); -} diff --git a/crates/agent-gui/src/pages/chat/components/DesktopCheckpointRewindProvider.tsx b/crates/agent-gui/src/pages/chat/components/DesktopCheckpointRewindProvider.tsx new file mode 100644 index 000000000..4b0cde5ef --- /dev/null +++ b/crates/agent-gui/src/pages/chat/components/DesktopCheckpointRewindProvider.tsx @@ -0,0 +1,80 @@ +import { + type CheckpointRewindClient, + CheckpointRewindProvider, + type CheckpointRewoundInfo, +} from "@liveagent/ui/lib/chat/checkpointRewind"; +import { invoke } from "@tauri-apps/api/core"; +import { type ReactNode, useCallback } from "react"; +import type { WorkspaceProject } from "../../../lib/settings"; +import { listWorkspaceRootGrants } from "../../../lib/workspaceRootGrants"; + +// 桌面端传输层:检查点数据只存在于桌面本机,直接走 Tauri invoke。 +// WebUI 的对应实现在 GatewayAppView(经网关 checkpoint 直通臂中继到同一批命令)。 +const desktopCheckpointRewindClient: CheckpointRewindClient = { + list: (conversationId) => invoke("checkpoint_list", { conversation_id: conversationId }), + preview: ({ conversationId, turnSeq, authorizedRoots }) => + invoke("checkpoint_diff_stats", { + conversation_id: conversationId, + turn_seq: turnSeq, + authorized_roots: authorizedRoots, + }), + rewind: ({ conversationId, turnSeq, authorizedRoots, expected }) => + invoke("checkpoint_rewind_code", { + conversation_id: conversationId, + turn_seq: turnSeq, + authorized_roots: authorizedRoots, + expected, + }), +}; + +export function DesktopCheckpointRewindProvider(props: { + children: ReactNode; + conversationId: string; + /** 当前会话的工作区根:授权集合的基准项。 */ + workspaceRoot?: string; + /** 当前激活项目:用于取额外授权根(workspace root grants)。 */ + project?: Pick | null; + disabled?: boolean; + /** 回退完成后回调(通知/转录记录由宿主页面处理)。 */ + onRewound?: (info: CheckpointRewoundInfo) => void; +}) { + const { children, conversationId, workspaceRoot, project, disabled, onRewound } = props; + + // 回退授权的唯一来源:当前会话工作区根 + 仍处于 active 且可写的额外授权根。 + // 后端只认这个集合里的 root,记录里存的绝对路径本身不构成授权。 + // + // access 必须一并过滤:回退是写操作(覆盖/删除),只读根不该被写。普通 + // 文件工具把 access 一路带到 pathUtils 的 canMutate 门禁上拦,而这里只往 + // 后端传路径、access 当场就丢了,所以这道门只能在这一步补上。 + const resolveAuthorizedRoots = useCallback(async () => { + const roots: string[] = []; + const push = (raw?: string | null) => { + const value = raw?.trim(); + if (value && !roots.includes(value)) roots.push(value); + }; + push(workspaceRoot); + if (project) { + try { + const grants = await listWorkspaceRootGrants(project); + for (const grant of grants) { + if (grant.state === "active" && grant.access === "write") push(grant.canonicalPath); + } + } catch { + // 取不到额外授权根时只保留工作区根:宁可少回退,不可越权写入。 + } + } + return roots; + }, [project, workspaceRoot]); + + return ( + + {children} + + ); +} diff --git a/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts b/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts index 5db69e338..d42b154a5 100644 --- a/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts +++ b/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts @@ -1159,6 +1159,16 @@ export function useSendChatTurn(params: UseSendChatTurnParams) { // this message can anchor its rebase without a history round-trip. messageRef: findHistoryMessageRefByMessageId(nextConversationState, pendingUserMessage.id), }); + if (effectiveIsAgentMode) { + try { + await invoke("checkpoint_begin_turn", { + conversation_id: conversationId, + turn_id: pendingUserMessage.id, + }); + } catch (error) { + console.warn("checkpoint turn boundary failed", error); + } + } if (await finishRequestedStopBeforeRuntime()) { return true; } @@ -1612,6 +1622,7 @@ export function useSendChatTurn(params: UseSendChatTurnParams) { sessionId, taskStateStore, conversationId, + checkpointTurnId: pendingUserMessage.id, conversationCwd, fallbackTitle, createdAt, diff --git a/crates/agent-gui/src/pages/chat/transcript/RowActions.tsx b/crates/agent-gui/src/pages/chat/transcript/RowActions.tsx index cecf6dc6e..395df4781 100644 --- a/crates/agent-gui/src/pages/chat/transcript/RowActions.tsx +++ b/crates/agent-gui/src/pages/chat/transcript/RowActions.tsx @@ -63,11 +63,12 @@ export type UserRowFooterProps = { text: string; timestamp: number; hasStableRef: boolean; + messageId?: string; onStartEdit: (key: string) => void; }; export function UserRowFooter(props: UserRowFooterProps) { - const { itemKey, text, timestamp, hasStableRef, onStartEdit } = props; + const { itemKey, text, timestamp, hasStableRef, messageId, onStartEdit } = props; const { t } = useLocale(); const { copied, markCopied } = useCopiedFlag(); const { isSending } = useRowInteraction(); @@ -85,6 +86,7 @@ export function UserRowFooter(props: UserRowFooterProps) { onEdit={() => { if (hasStableRef) onStartEdit(itemKey); }} + rewindTurnId={messageId} /> ); } diff --git a/crates/agent-gui/src/pages/chat/transcript/UserMessageRow.tsx b/crates/agent-gui/src/pages/chat/transcript/UserMessageRow.tsx index 2a5ac9a8f..6e82f4891 100644 --- a/crates/agent-gui/src/pages/chat/transcript/UserMessageRow.tsx +++ b/crates/agent-gui/src/pages/chat/transcript/UserMessageRow.tsx @@ -89,6 +89,7 @@ export const UserMessageRow = memo(function UserMessageRow(props: UserMessageRow text={item.text} timestamp={item.timestamp} hasStableRef={!!effectiveMessageRef} + messageId={effectiveMessageRef?.messageId} onStartEdit={onStartEdit} /> diff --git a/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts b/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts index 3b243b5d6..1814f143c 100644 --- a/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts +++ b/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts @@ -247,6 +247,7 @@ export type RunAgentConversationTurnParams = { /** Run 级任务状态存储:由 send 管线构建,提交走非终态持久化。 */ taskStateStore: TaskStateStore; conversationId: string; + checkpointTurnId?: string; conversationCwd?: string; fallbackTitle: string; createdAt: number; @@ -317,6 +318,7 @@ export async function runAgentConversationTurn(params: RunAgentConversationTurnP sessionId, taskStateStore, conversationId, + checkpointTurnId, conversationCwd, fallbackTitle, createdAt, @@ -497,9 +499,10 @@ export async function runAgentConversationTurn(params: RunAgentConversationTurnP fileState, taskStateStore, askUserQuestionConversationId: conversationId, - // 检查点粒度=一次注册表构建(每用户轮重建)。turnId 用随机 UUID 保证 - // 时钟无关的稳定标识,轮内序号由 Rust 侧按 turnId 分配。 - checkpoint: { conversationId, turnId: crypto.randomUUID() }, + checkpoint: { + conversationId, + turnId: checkpointTurnId?.trim() || crypto.randomUUID(), + }, skillsEnabled: effectiveSkillsEnabled, skillsRootDir, skillAccessPolicy, diff --git a/crates/agent-ui/src/components/chat/TranscriptMessageActions.tsx b/crates/agent-ui/src/components/chat/TranscriptMessageActions.tsx index 3c5e67b73..6a7ff820d 100644 --- a/crates/agent-ui/src/components/chat/TranscriptMessageActions.tsx +++ b/crates/agent-ui/src/components/chat/TranscriptMessageActions.tsx @@ -5,8 +5,10 @@ import { Loader2, Pencil, RefreshCw, + Undo2, } from "@liveagent/ui/components/IconSet"; import { useLocale } from "../../i18n/index"; +import { useCheckpointRewindAction } from "../../lib/chat/checkpointRewind"; import { cn } from "../../lib/shared/utils"; import { ConfirmActionPopover } from "../ui/confirm-action-popover"; @@ -43,6 +45,8 @@ export function TranscriptUserMessageActions( editTitle: string; onEdit: () => void; readOnly?: boolean; + /** 本行用户消息的稳定 ID:检查点回退按 turnId=消息 ID 命中该轮。 */ + rewindTurnId?: string; }, ) { const { @@ -55,8 +59,12 @@ export function TranscriptUserMessageActions( editTitle, onEdit, readOnly = false, + rewindTurnId, } = props; const { t } = useLocale(); + // Provider 外(只读分享页等)返回 null:整颗按钮不渲染。 + const rewind = useCheckpointRewindAction(rewindTurnId); + const rewindTitle = rewind?.available ? t("chat.rewindCode") : t("chat.rewindUnavailable"); return (
@@ -87,6 +95,22 @@ export function TranscriptUserMessageActions( > + {rewind ? ( + + ) : null}
) : null} diff --git a/crates/agent-ui/src/i18n/translations/enUSCommon.ts b/crates/agent-ui/src/i18n/translations/enUSCommon.ts index ee4d59a4f..c776d13bc 100644 --- a/crates/agent-ui/src/i18n/translations/enUSCommon.ts +++ b/crates/agent-ui/src/i18n/translations/enUSCommon.ts @@ -324,6 +324,8 @@ export const EN_US_COMMON_TRANSLATIONS = { "chat.composer.noMatchingEnabledSkills": "No matching enabled Skills", "chat.copy": "Copy", "chat.edit": "Edit", + "chat.rewindCode": "Rewind code changes from this turn", + "chat.rewindUnavailable": "No code checkpoint is available for this turn", "chat.retry": "Retry", "chat.retryConfirmTitle": "Retry this reply?", "chat.retryConfirmDescription": diff --git a/crates/agent-ui/src/i18n/translations/zhCNCommon.ts b/crates/agent-ui/src/i18n/translations/zhCNCommon.ts index 6e858e7d4..918816664 100644 --- a/crates/agent-ui/src/i18n/translations/zhCNCommon.ts +++ b/crates/agent-ui/src/i18n/translations/zhCNCommon.ts @@ -5,8 +5,7 @@ export const ZH_CN_COMMON_TRANSLATIONS = { "app.errorBoundaryTitle": "页面出现异常", "app.loading": "正在加载设置...", "app.name": "LiveAgent", - "app.settingsSshSettingsChanged": - "SSH 设置已在另一端更新,已刷新为最新状态,请重新提交。", + "app.settingsSshSettingsChanged": "SSH 设置已在另一端更新,已刷新为最新状态,请重新提交。", "common.currentUser": "当前用户", "common.dismissNotification": "关闭通知", "common.logout": "退出登录", @@ -296,6 +295,8 @@ export const ZH_CN_COMMON_TRANSLATIONS = { "chat.composer.noMatchingEnabledSkills": "未找到匹配的已启用 Skills", "chat.copy": "复制", "chat.edit": "编辑", + "chat.rewindCode": "回退本轮代码改动", + "chat.rewindUnavailable": "本轮没有可用的代码检查点", "chat.retry": "重试", "chat.retryConfirmTitle": "重试该回复?", "chat.retryConfirmDescription": "将以原始提问重新发送,该回复及之后的对话内容将被删除。", diff --git a/crates/agent-ui/src/lib/chat/checkpointRewind.tsx b/crates/agent-ui/src/lib/chat/checkpointRewind.tsx new file mode 100644 index 000000000..d307a7e97 --- /dev/null +++ b/crates/agent-ui/src/lib/chat/checkpointRewind.tsx @@ -0,0 +1,403 @@ +import { useConfirmDialog } from "@liveagent/ui/components/ui/confirm-dialog"; +import { useLocale } from "@liveagent/ui/i18n/index"; +import { + createContext, + type ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +export type CheckpointTurnSummary = { + turnSeq: number; + turnId: string; + fileCount: number; + dirCount: number; + /** 该轮存在捕获失败记录(前像不完整),回退可能遗漏部分文件。 */ + incomplete: boolean; + firstCapturedAt: number; +}; + +export type CheckpointDiffStats = { + turnSeq: number; + restoreFiles: number; + deleteFiles: number; + cleanFiles: number; + skippedDirs: number; + missingBlobs: number; + /** 根已不在当前授权工作区集合内、或路径链上出现符号链接的条目:一律不回退。 */ + unresolvableFiles: number; + captureErrors: number; + entries: { path: string; key: string; action: string; currentHash?: string }[]; +}; + +export type CheckpointRewindResult = { + turnSeq: number; + restoredFiles: number; + deletedFiles: number; + cleanFiles: number; + skippedDirs: number; + /** 目标范围内捕获阶段就失败的记录数:这些文件没有前像,回退没有碰它们。 */ + captureErrors: number; + /** 预览后被外部修改、被跳过未覆盖的文件(冲突检测)。 */ + conflicts: string[]; + failed: string[]; +}; + +/** 两端各自实现的传输层:桌面端走 Tauri invoke,WebUI 走网关 checkpoint 直通臂。 */ +export type CheckpointRewindClient = { + list: (conversationId: string) => Promise; + preview: (params: { + conversationId: string; + turnSeq: number; + authorizedRoots: string[]; + }) => Promise; + rewind: (params: { + conversationId: string; + turnSeq: number; + authorizedRoots: string[]; + expected: { key: string; currentHash: string }[]; + }) => Promise; +}; + +export type CheckpointRewoundInfo = { + turnSeq: number; + restoredFiles: number; + deletedFiles: number; + conflicts: number; + failed: number; + /** 捕获阶段就失败的文件数:没有前像,回退没有碰它们。 */ + captureErrors: number; +}; + +/** + * 回退完成通知的共享文案:两端 onRewound 都用它,避免模板漂移。 + * - 零计数一律不展示;完全没有文件改动时明确说"没有文件需要改动"。 + * - 数字与量词之间用不换行空格(U+00A0)钉住,窄 toast 里"1 个"不会被折成两行。 + * - 问题项(冲突/失败/无前像)collect 进括号尾注,与主结果分层。 + */ +export function formatCheckpointRewoundNotification( + info: CheckpointRewoundInfo, + zh: boolean, +): { level: "success" | "error"; message: string } { + const nb = (value: number) => `\u00A0${value}\u00A0`; + const files = (value: number) => `${value} ${value === 1 ? "file" : "files"}`; + const changes: string[] = []; + const issues: string[] = []; + if (zh) { + if (info.restoredFiles > 0) changes.push(`恢复${nb(info.restoredFiles)}个`); + if (info.deletedFiles > 0) changes.push(`删除${nb(info.deletedFiles)}个`); + // 组尾统一补"文件":单项时"删除 1 个文件",双项时"恢复 2 个、删除 1 个文件"。 + if (changes.length > 0) changes[changes.length - 1] += "文件"; + if (info.conflicts > 0) issues.push(`冲突跳过${nb(info.conflicts)}个`); + if (info.failed > 0) issues.push(`失败${nb(info.failed)}个`); + if (info.captureErrors > 0) issues.push(`${info.captureErrors}\u00A0个无前像未回退`); + } else { + if (info.restoredFiles > 0) changes.push(`restored ${files(info.restoredFiles)}`); + if (info.deletedFiles > 0) changes.push(`deleted ${files(info.deletedFiles)}`); + if (info.conflicts > 0) + issues.push(`${info.conflicts} conflict${info.conflicts === 1 ? "" : "s"} skipped`); + if (info.failed > 0) issues.push(`${info.failed} failed`); + if (info.captureErrors > 0) issues.push(`${info.captureErrors} without pre-image`); + } + const head = zh + ? changes.length > 0 + ? `已回退代码:${changes.join("、")}` + : "已回退代码:没有文件需要改动" + : changes.length > 0 + ? `Code rewound: ${changes.join(", ")}` + : "Code rewound: no file changes were needed"; + const message = + issues.length > 0 + ? zh + ? `${head}(${issues.join("、")})` + : `${head} (${issues.join(", ")})` + : head; + return { + level: info.failed > 0 || info.conflicts > 0 || info.captureErrors > 0 ? "error" : "success", + message, + }; +} + +/** 行内回退按钮所需的全部状态:null 表示当前 turn 无检查点(按钮禁用展示)。 */ +export type CheckpointRewindAction = { + available: boolean; + pending: boolean; + disabled: boolean; + onRewind?: () => void; +}; + +type CheckpointRewindContextValue = { + turns: Map; + loading: boolean; + disabled: boolean; + busyTurn: number | null; + rewind: (turn: CheckpointTurnSummary) => void; +}; + +const CheckpointRewindContext = createContext(null); + +// 仅覆盖 Write/Edit/Delete 三个文件工具的改动;Bash 等 shell 写入不在检查点内。 +// 回退点 = 用户消息:turnId 就是用户消息 ID,行内按钮经 useCheckpointRewindAction +// 按 messageId 命中本轮(对齐 Claude Code 的每消息回退)。 +export function CheckpointRewindProvider(props: { + children: ReactNode; + conversationId?: string; + /** 发送/流式中为 true:行内按钮全体禁用,且暂停列表刷新。 */ + disabled?: boolean; + client: CheckpointRewindClient; + /** + * 回退授权的唯一来源:当前会话工作区根 + 仍处于 active 且可写的额外授权根。 + * 后端只认这个集合里的 root,记录里存的绝对路径本身不构成授权;access 必须 + * 由调用方过滤(回退是写操作,只读根不该被写)。 + */ + resolveAuthorizedRoots: () => Promise; + onRewound?: (info: CheckpointRewoundInfo) => void; +}) { + const { + children, + conversationId, + disabled = false, + client, + resolveAuthorizedRoots, + onRewound, + } = props; + const { locale } = useLocale(); + const zh = locale === "zh-CN"; + const { confirm, dialog } = useConfirmDialog(); + const [turns, setTurns] = useState([]); + const [loading, setLoading] = useState(false); + const [busyTurn, setBusyTurn] = useState(null); + + // latest-ref:宿主通常内联传这两个回调(每渲染新身份)。若做依赖,rewind 与 + // context value 会随宿主每帧重建,流式期间放大成全部用户行重渲染。 + const resolveRootsRef = useRef(resolveAuthorizedRoots); + const onRewoundRef = useRef(onRewound); + const disabledRef = useRef(disabled); + useEffect(() => { + resolveRootsRef.current = resolveAuthorizedRoots; + onRewoundRef.current = onRewound; + disabledRef.current = disabled; + }); + + // 列表加载代际:慢响应(切会话前发出的)一律丢弃,防乱序覆盖。 + const loadEpochRef = useRef(0); + const loadTurns = useCallback(async () => { + const epoch = ++loadEpochRef.current; + if (!conversationId) { + setTurns([]); + return; + } + setLoading(true); + try { + const list = await client.list(conversationId); + if (loadEpochRef.current === epoch) setTurns(list); + } catch { + if (loadEpochRef.current === epoch) setTurns([]); + } finally { + if (loadEpochRef.current === epoch) setLoading(false); + } + }, [client, conversationId]); + + // 切会话立刻清空旧列表:分支复制会保留消息 ID,旧会话的轮残留可能错配到 + // 新会话的同 ID 气泡上。 + useEffect(() => { + loadEpochRef.current += 1; + setTurns([]); + }, [conversationId]); + + // 空闲(挂载/切会话/轮次结束)时刷新;发送中不拉取,半截时间线没有展示价值。 + useEffect(() => { + if (!disabled) void loadTurns(); + }, [disabled, loadTurns]); + + // busy 守卫走 ref:rewind 不依赖 busyTurn state,身份保持稳定。 + const busyTurnRef = useRef(null); + const rewind = useCallback( + async (turn: CheckpointTurnSummary) => { + if (!conversationId || disabledRef.current || busyTurnRef.current !== null) return; + busyTurnRef.current = turn.turnSeq; + setBusyTurn(turn.turnSeq); + try { + const authorizedRoots = await resolveRootsRef.current(); + const stats = await client.preview({ + conversationId, + turnSeq: turn.turnSeq, + authorizedRoots, + }); + const parts: string[] = []; + if (stats.restoreFiles > 0) + parts.push( + zh ? `恢复 ${stats.restoreFiles} 个文件` : `restore ${stats.restoreFiles} file(s)`, + ); + if (stats.deleteFiles > 0) + parts.push( + zh ? `删除 ${stats.deleteFiles} 个文件` : `delete ${stats.deleteFiles} file(s)`, + ); + if (stats.cleanFiles > 0) + parts.push( + zh ? `${stats.cleanFiles} 个文件已一致` : `${stats.cleanFiles} file(s) unchanged`, + ); + if (stats.skippedDirs > 0) + parts.push( + zh + ? `${stats.skippedDirs} 个目录删除不可恢复` + : `${stats.skippedDirs} deleted dir(s) not restorable`, + ); + if (stats.missingBlobs > 0) + parts.push( + zh ? `${stats.missingBlobs} 个前像缺失` : `${stats.missingBlobs} blob(s) missing`, + ); + if (stats.unresolvableFiles > 0) + parts.push( + zh + ? `${stats.unresolvableFiles} 个路径已不可回退(根未授权或路径含符号链接)` + : `${stats.unresolvableFiles} path(s) not rewindable (root unauthorized or symlinked)`, + ); + if (stats.captureErrors > 0 || turn.incomplete) + parts.push( + zh + ? `⚠ 该轮有 ${Math.max(stats.captureErrors, 1)} 次前像捕获失败,回退可能不完整` + : `⚠ ${Math.max(stats.captureErrors, 1)} pre-image capture failure(s); rewind may be incomplete`, + ); + const actionable = stats.entries.filter( + (entry) => entry.action === "restore" || entry.action === "delete", + ); + // 检查点只记录 agent 工具写入前的前像,编辑器/文件树里的手改既不入账、 + // 也无法与工具写入区分。回退按前像整体覆盖,手改会被一并抹掉,先说清楚。 + if (actionable.length > 0) + parts.push( + zh + ? "手动编辑(编辑器/文件树)不在检查点内,会被一并覆盖" + : "Manual edits (editor / file tree) are not checkpointed and will be overwritten", + ); + const confirmed = await confirm({ + title: zh ? "回退代码到此轮开始前" : "Rewind code to before this turn", + subtitle: new Date(turn.firstCapturedAt).toLocaleString(), + description: + parts.length > 0 + ? parts.join(zh ? "," : ", ") + : zh + ? "没有需要回退的改动" + : "Nothing to rewind", + detail: + actionable.length > 0 ? actionable.map((entry) => entry.path).join("\n") : undefined, + confirmLabel: zh ? "回退" : "Rewind", + cancelLabel: zh ? "取消" : "Cancel", + tone: "warning", + }); + if (!confirmed) return; + // 把预览时的现状哈希传回后端,回退前逐个复核:预览到执行之间被外部 + // 修改的文件会被跳过并报告为冲突,绝不覆盖(TOCTOU 防护)。 + // 必须回传全部可解析条目(含 clean)——后端对缺哈希的条目一律判冲突, + // 只带 restore/delete 会让确认期间被手改的 clean 文件被静默覆盖。 + const expected = stats.entries.flatMap((entry) => + entry.currentHash == null ? [] : [{ key: entry.key, currentHash: entry.currentHash }], + ); + const result = await client.rewind({ + conversationId, + turnSeq: turn.turnSeq, + authorizedRoots, + expected, + }); + onRewoundRef.current?.({ + turnSeq: turn.turnSeq, + restoredFiles: result.restoredFiles, + deletedFiles: result.deletedFiles, + conflicts: result.conflicts.length, + failed: result.failed.length, + captureErrors: result.captureErrors, + }); + // 完整回退会在后端剪掉 turnSeq 及之后的轮,重拉让按钮态跟上。 + await loadTurns(); + if ( + result.failed.length > 0 || + result.conflicts.length > 0 || + result.captureErrors > 0 || + result.skippedDirs > 0 + ) { + const issueLines = [ + ...result.conflicts.map((path) => + zh ? `冲突(已跳过): ${path}` : `conflict (skipped): ${path}`, + ), + ...result.failed.map((path) => (zh ? `失败: ${path}` : `failed: ${path}`)), + ]; + // 捕获缺口/不可恢复目录没有具体路径列表,单独一行说明。 + if (result.captureErrors > 0) + issueLines.push( + zh + ? `该轮有 ${result.captureErrors} 个文件没有前像(捕获失败),未被回退` + : `${result.captureErrors} file(s) had no pre-image (capture failed) and were not rewound`, + ); + if (result.skippedDirs > 0) + issueLines.push( + zh + ? `${result.skippedDirs} 个被删除目录无法恢复` + : `${result.skippedDirs} deleted dir(s) could not be restored`, + ); + await confirm({ + title: zh ? "回退部分未完成" : "Rewind partially completed", + description: zh + ? `已恢复 ${result.restoredFiles} 个、删除 ${result.deletedFiles} 个;冲突跳过 ${result.conflicts.length} 个、失败 ${result.failed.length} 个` + : `Restored ${result.restoredFiles}, deleted ${result.deletedFiles}; ${result.conflicts.length} conflict(s) skipped, ${result.failed.length} failed`, + detail: issueLines.join("\n"), + confirmLabel: zh ? "知道了" : "OK", + cancelLabel: "", + hideCancel: true, + tone: "destructive", + }); + } + } catch (error) { + await confirm({ + title: zh ? "回退失败" : "Rewind failed", + description: String(error), + confirmLabel: zh ? "知道了" : "OK", + cancelLabel: "", + hideCancel: true, + tone: "destructive", + }); + } finally { + busyTurnRef.current = null; + setBusyTurn(null); + } + }, + [client, confirm, conversationId, loadTurns, zh], + ); + + const value = useMemo( + () => ({ + turns: new Map(turns.map((turn) => [turn.turnId, turn])), + loading, + disabled, + busyTurn, + rewind: (turn) => void rewind(turn), + }), + [busyTurn, disabled, loading, rewind, turns], + ); + + return ( + + {children} + {dialog} + + ); +} + +/** + * 按用户消息 ID 取本行的回退动作。Provider 之外返回 null(按钮以禁用态展示, + * 只读页等不渲染动作区的场景自然无感)。 + */ +export function useCheckpointRewindAction(turnId?: string): CheckpointRewindAction | null { + const context = useContext(CheckpointRewindContext); + if (!context) return null; + const turn = turnId ? context.turns.get(turnId) : undefined; + return { + available: !!turn, + pending: !!turn && context.busyTurn === turn.turnSeq, + disabled: context.disabled || context.loading || context.busyTurn !== null || !turn, + onRewind: turn ? () => context.rewind(turn) : undefined, + }; +} From a00fa1d24e3bcaca5296ec3d5b8ddd258103cbbd Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Mon, 17 Aug 2026 15:12:59 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(lint):=20ChatPage=20=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E8=BF=87=20biome=20organizeImports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/agent-gui/src/pages/ChatPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index 71821d149..dc03e8fea 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -106,8 +106,8 @@ import { usePendingUploads, } from "./chat"; import type { ChatPageProps } from "./chat/chatPageTypes"; -import { DesktopCheckpointRewindProvider } from "./chat/components/DesktopCheckpointRewindProvider"; import { CurrentTaskProgress } from "./chat/components/CurrentTaskProgress"; +import { DesktopCheckpointRewindProvider } from "./chat/components/DesktopCheckpointRewindProvider"; import { PendingToolApprovalBar } from "./chat/components/PendingToolApprovalBar"; import { useComposerDraftCache } from "./chat/composer/useComposerDraftCache"; import { useComposerHistoryPrompts } from "./chat/composer/useComposerHistoryPrompts";