ai_co_discuss 0.2.99 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/discuss-host/SKILL.md +3 -3
- package/.agents/skills/discuss-host/references/discussion.md +16 -16
- package/.agents/skills/discuss-host/references/launch.md +1 -3
- package/.agents/skills/discuss-host/references/manual.md +14 -77
- package/.agents/skills/discuss-participant/SKILL.md +3 -3
- package/.agents/skills/discuss-participant/references/discussion.md +29 -27
- package/.agents/skills/discuss-participant/references/free.md +2 -2
- package/.agents/skills/discuss-participant/references/manual.md +8 -10
- package/.agents/skills/discuss-participant/references/writing.md +9 -9
- package/.github/SECURITY.md +1 -1
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/backend/free_room.py +360 -68
- package/backend/room_import.py +75 -15
- package/backend/runtime.py +81 -169
- package/backend/schema.sql +136 -244
- package/backend/server.py +602 -166
- package/backend/session_room.py +919 -0
- package/docs/architecture.md +3 -2
- package/docs/en/architecture.md +4 -3
- package/docs/en/runner.md +3 -1
- package/docs/en/workflow.md +9 -6
- package/docs/runner.md +3 -1
- package/docs/workflow.md +11 -8
- package/package.json +2 -2
- package/runner/api_participant.mjs +47 -44
- package/runner/discuss.py +77 -58
- package/runner/discussion_adapters.mjs +6 -3
- package/runner/discussion_launch.mjs +98 -70
- package/runner/discussion_runner.mjs +38 -496
- package/runner/discussion_service.mjs +21 -74
- package/runner/discussion_sidecar.mjs +4 -13
- package/runner/expert_prompts.mjs +29 -21
- package/runner/free_runner.mjs +84 -19
- package/runner/session_driver.mjs +175 -0
- package/web/app.js +193 -128
- package/web/backup.js +1 -1
- package/web/modern.css +1 -1
- package/web/modern.js +5482 -6646
package/runner/discuss.py
CHANGED
|
@@ -196,6 +196,8 @@ def room_commands(subcommands, registry):
|
|
|
196
196
|
parser = add_leaf(subcommands, registry, f"room {action}", description, action != "list")
|
|
197
197
|
if action == "list":
|
|
198
198
|
parser.add_argument("--submission-id", type=submission_id, help="按建房提交编号精确查询")
|
|
199
|
+
elif action in ("view", "brief"):
|
|
200
|
+
parser.add_argument("--request", type=positive_int, help="手动成员:按自己的有效请求限制可读原文")
|
|
199
201
|
limits = add_leaf(subcommands, registry, "room limits", "查看上限;--check 检查并执行到限处理;--done 通知程序目标已完成", True)
|
|
200
202
|
limits.add_argument("--check", action="store_true")
|
|
201
203
|
limits.add_argument("--done", action="store_true", help="主持人宣告目标已完成:结束讨论,房间回到自由协作;须带 --conclusion")
|
|
@@ -221,8 +223,10 @@ def message_commands(subcommands, registry):
|
|
|
221
223
|
listing.add_argument("--mention", type=participant)
|
|
222
224
|
listing.add_argument("--kind", choices=READ_KINDS, help="筛选观点、主持说明、协作消息或旧房间的历史回执")
|
|
223
225
|
listing.add_argument("--submission-id", type=submission_id, help="按提交编号精确查询结果")
|
|
226
|
+
listing.add_argument("--request", type=positive_int, help="手动成员:按自己的有效请求限制可读原文")
|
|
224
227
|
view = add_leaf(subcommands, registry, "message view", "读取一条精确原文与来源", True)
|
|
225
228
|
view.add_argument("id", type=positive_int, help="消息编号")
|
|
229
|
+
view.add_argument("--request", type=positive_int, help="手动成员:按自己的有效请求限制可读原文")
|
|
226
230
|
post = add_leaf(subcommands, registry, "message post", "提交观点、主持说明、协作回答或代发", True)
|
|
227
231
|
add_text(post, "body")
|
|
228
232
|
post.add_argument("--attachment", action="append", type=attachment_id, default=[],
|
|
@@ -230,8 +234,7 @@ def message_commands(subcommands, registry):
|
|
|
230
234
|
add_text(post, "summary")
|
|
231
235
|
post.add_argument("--summary-by", type=participant, help="摘要撰写者:原作者或实际操作者;默认操作者")
|
|
232
236
|
post.add_argument("--author", type=participant, help="原作者;默认等于 --as;不同即代发")
|
|
233
|
-
post.add_argument("--kind", choices=
|
|
234
|
-
post.add_argument("--turn", type=positive_int, help="所回答的安排编号,仅 opinion 可用;省略时回答你当前有效的安排")
|
|
237
|
+
post.add_argument("--kind", choices=("chat", "moderation"), default="chat")
|
|
235
238
|
post.add_argument("--execution", type=positive_int, help="自由房间:本次执行编号,仅 chat 可用;执行失效后不能改作普通发言")
|
|
236
239
|
post.add_argument("--to", type=ai_name, action="append", default=[],
|
|
237
240
|
help="自由房间:主持人点名派活,普通成员提出回应建议(须房主确认);可重复")
|
|
@@ -261,15 +264,15 @@ def attachment_commands(subcommands, registry):
|
|
|
261
264
|
|
|
262
265
|
|
|
263
266
|
def turn_commands(subcommands, registry):
|
|
264
|
-
"""
|
|
267
|
+
"""Read and control discussion plans through their dedicated operations."""
|
|
265
268
|
descriptions = {"list": "分页读取安排历史", "current": "读取阶段和最近安排;需核对状态及专家",
|
|
266
|
-
"view": "读取一个安排",
|
|
269
|
+
"view": "读取一个安排",
|
|
267
270
|
"roster": "申请最新专家状态(在场专家、本轮待发言者与 revision);整轮安排前必做",
|
|
268
271
|
"order": "读取发言顺序(已发言、正在发言、待发言与 revision);调整顺序前必做",
|
|
269
272
|
"reorder": "房主调整尚未发言者的顺序(须带 turn order 的 --revision);已发言与正在发言者不动",
|
|
270
|
-
"create": "安排一位专家,或用 --plan-file
|
|
273
|
+
"create": "安排一位专家,或用 --plan-file 一次安排整轮(须带最新 --roster)",
|
|
271
274
|
"continue": "处理用户操作或打断后,按原排队继续下一位",
|
|
272
|
-
"
|
|
275
|
+
"cancel": "取消指定未完成安排"}
|
|
273
276
|
for action, description in descriptions.items():
|
|
274
277
|
parser = add_leaf(subcommands, registry, f"turn {action}", description, True)
|
|
275
278
|
if action == "list":
|
|
@@ -281,18 +284,21 @@ def turn_commands(subcommands, registry):
|
|
|
281
284
|
parser.add_argument("--expert", type=ai_name, help="单个安排的专家;与 --plan-file 二选一")
|
|
282
285
|
add_text(parser, "question")
|
|
283
286
|
parser.add_argument("--plan-file", help='整轮安排 JSON 数组 [{"expert":"Claude","question":"…"}],按顺序接力;- 从标准输入读取')
|
|
284
|
-
parser.add_argument("--roster", help="turn roster 返回的 revision
|
|
287
|
+
parser.add_argument("--roster", required=True, help="turn roster 返回的 revision;任何新安排前必读,专家状态变化后需重新获取")
|
|
285
288
|
parser.add_argument("--submission-id", type=submission_id, required=True,
|
|
286
289
|
help="新安排唯一编号;重试沿用同编号和内容")
|
|
287
290
|
if action == "reorder":
|
|
288
291
|
parser.add_argument("--revision", required=True, help="turn order 返回的 revision;顺序变化后需重新获取")
|
|
289
292
|
parser.add_argument("--member", action="append", type=ai_name, required=True,
|
|
290
293
|
help="按新顺序重复指定,须恰好包含 turn order 中 movable 的全部成员")
|
|
294
|
+
if action == "continue":
|
|
295
|
+
parser.add_argument("--roster", required=True, help="turn roster 返回的 revision;状态变化后须重新读取")
|
|
291
296
|
|
|
292
297
|
|
|
293
298
|
def state_commands(subcommands, registry):
|
|
294
299
|
"""Register separately requested host summaries with explicit cutoffs."""
|
|
295
|
-
add_leaf(subcommands, registry, "state view", "读取主持摘要及材料截止编号", True)
|
|
300
|
+
view = add_leaf(subcommands, registry, "state view", "读取主持摘要及材料截止编号", True)
|
|
301
|
+
view.add_argument("--request", type=positive_int, help="手动成员:按自己的有效请求限制可读原文")
|
|
296
302
|
parser = add_leaf(subcommands, registry, "state set", "保存主持摘要;不自动推定已读范围", True)
|
|
297
303
|
add_text(parser, "body", required=True)
|
|
298
304
|
add_text(parser, "brief")
|
|
@@ -300,24 +306,24 @@ def state_commands(subcommands, registry):
|
|
|
300
306
|
|
|
301
307
|
|
|
302
308
|
def run_commands(subcommands, registry):
|
|
303
|
-
"""Expose
|
|
304
|
-
for action in ("view", "
|
|
305
|
-
parser = add_leaf(subcommands, registry, f"run {action}", "
|
|
309
|
+
"""Expose discussion-session lifecycle controls independently of the resident service."""
|
|
310
|
+
for action in ("view", "start", "pause", "resume", "stop"):
|
|
311
|
+
parser = add_leaf(subcommands, registry, f"run {action}", "查看或显式控制讨论会话", True)
|
|
306
312
|
if action != "view":
|
|
307
|
-
parser.add_argument("--revision",
|
|
308
|
-
parser.add_argument("--mode", choices=("manual", "automatic"))
|
|
313
|
+
parser.add_argument("--revision", required=True, help="run view 返回的 revision;状态变化后须重新读取")
|
|
309
314
|
if action == "start":
|
|
310
|
-
parser
|
|
311
|
-
|
|
315
|
+
add_text(parser, "goal")
|
|
316
|
+
for name in ("max-rounds", "max-minutes", "execution-minutes"):
|
|
317
|
+
parser.add_argument(f"--{name}", type=non_negative_int, default=argparse.SUPPRESS)
|
|
318
|
+
parser.add_argument("--submission-id", type=submission_id,
|
|
319
|
+
help="可选启动提交编号;重试须沿用编号和原请求")
|
|
312
320
|
|
|
313
321
|
|
|
314
322
|
def execution_commands(subcommands, registry):
|
|
315
|
-
"""
|
|
316
|
-
for action in ("view", "
|
|
317
|
-
parser = add_leaf(subcommands, registry, f"execution {action}", "
|
|
323
|
+
"""Read an execution or report its failure through its durable identifier."""
|
|
324
|
+
for action in ("view", "fail"):
|
|
325
|
+
parser = add_leaf(subcommands, registry, f"execution {action}", "读取执行或报告本次执行失败", True)
|
|
318
326
|
parser.add_argument("id", type=positive_int)
|
|
319
|
-
if action == "enter":
|
|
320
|
-
parser.add_argument("--session-id", help="实际软件会话 ID,默认从受限任务文件读取")
|
|
321
327
|
if action == "fail":
|
|
322
328
|
parser.add_argument("--reason", required=True)
|
|
323
329
|
|
|
@@ -429,6 +435,23 @@ def topic_payload(args):
|
|
|
429
435
|
return payload
|
|
430
436
|
|
|
431
437
|
|
|
438
|
+
def read_context_params(args):
|
|
439
|
+
"""Select an explicit manual request or the execution bound to this access file."""
|
|
440
|
+
request_id = getattr(args, "request", None)
|
|
441
|
+
execution_value = load_access(args).get("execution_id")
|
|
442
|
+
if request_id is not None and execution_value is not None:
|
|
443
|
+
raise CliError("读取时 --request 不能与凭据文件中的 execution_id 同用")
|
|
444
|
+
if request_id is not None:
|
|
445
|
+
return {"request_id": request_id}
|
|
446
|
+
if execution_value is None:
|
|
447
|
+
return {}
|
|
448
|
+
try:
|
|
449
|
+
execution_id = positive_int(str(execution_value))
|
|
450
|
+
except argparse.ArgumentTypeError as error:
|
|
451
|
+
raise CliError("凭据文件中的 execution_id 无效") from error
|
|
452
|
+
return {"execution_id": execution_id}
|
|
453
|
+
|
|
454
|
+
|
|
432
455
|
def plan_room(args):
|
|
433
456
|
"""Translate room operations to one existing HTTP endpoint."""
|
|
434
457
|
if args.action == "list":
|
|
@@ -459,23 +482,24 @@ def plan_room(args):
|
|
|
459
482
|
return RequestPlan(base + "/conclusion")
|
|
460
483
|
if args.action == "protocol":
|
|
461
484
|
return RequestPlan(f"/r/{args.room}/protocol")
|
|
462
|
-
return RequestPlan(base, view="brief" if args.action == "brief" else "overview")
|
|
485
|
+
return RequestPlan(base, view="brief" if args.action == "brief" else "overview", params=read_context_params(args))
|
|
463
486
|
|
|
464
487
|
|
|
465
488
|
def plan_message(args):
|
|
466
489
|
"""Preserve operator, original author, message role, and replay identifier."""
|
|
467
490
|
base = f"/api/rooms/{args.room}/messages"
|
|
468
491
|
if args.action == "view":
|
|
469
|
-
return RequestPlan(f"{base}/{args.id}")
|
|
492
|
+
return RequestPlan(f"{base}/{args.id}", params=read_context_params(args))
|
|
470
493
|
if args.action == "list":
|
|
471
494
|
params = {"page": args.page, "q": args.query, "author": args.author, "mention": args.mention,
|
|
472
|
-
"after": args.after, "kind": args.kind, "submission_id": args.submission_id
|
|
495
|
+
"after": args.after, "kind": args.kind, "submission_id": args.submission_id,
|
|
496
|
+
**read_context_params(args)}
|
|
473
497
|
return RequestPlan(base, params={key: value for key, value in params.items() if value is not None})
|
|
474
498
|
actor = require_actor(args)
|
|
475
|
-
if args.
|
|
476
|
-
raise CliError("
|
|
477
|
-
if
|
|
478
|
-
raise CliError("--execution
|
|
499
|
+
if (args.to or args.handoff) and args.kind != "chat":
|
|
500
|
+
raise CliError("--to / --handoff 仅用于 chat 消息")
|
|
501
|
+
if args.kind == "moderation" and not (args.execution or args.request):
|
|
502
|
+
raise CliError("主持说明须关联本次 --execution 或有效 --request")
|
|
479
503
|
if args.execution and args.request:
|
|
480
504
|
raise CliError("--execution 用于接入程序交付的执行,--request 用于手动接入成员,二者不能同用")
|
|
481
505
|
payload = {"author": args.author or actor, "submitted_by": actor, "source": "cli",
|
|
@@ -493,8 +517,6 @@ def plan_message(args):
|
|
|
493
517
|
payload["summary"] = summary
|
|
494
518
|
if args.summary_by:
|
|
495
519
|
payload["summary_by"] = args.summary_by
|
|
496
|
-
if args.turn:
|
|
497
|
-
payload["turn_id"] = args.turn
|
|
498
520
|
if args.execution:
|
|
499
521
|
payload["execution_id"] = args.execution
|
|
500
522
|
if args.request:
|
|
@@ -557,12 +579,12 @@ def plan_turn(args):
|
|
|
557
579
|
if args.action == "create":
|
|
558
580
|
return RequestPlan(base + "/turns", "POST", turn_payload(args))
|
|
559
581
|
if args.action == "continue":
|
|
560
|
-
return RequestPlan(base + "/turns/continue", "POST", {"actor": require_actor(args)})
|
|
582
|
+
return RequestPlan(base + "/turns/continue", "POST", {"actor": require_actor(args), "revision": args.roster})
|
|
561
583
|
path = f"{base}/turns/{args.id}"
|
|
562
|
-
if args.action
|
|
563
|
-
status = "
|
|
584
|
+
if args.action == "cancel":
|
|
585
|
+
status = "cancelled"
|
|
564
586
|
return RequestPlan(path, "POST", {"actor": require_actor(args), "status": status})
|
|
565
|
-
return RequestPlan(path
|
|
587
|
+
return RequestPlan(path)
|
|
566
588
|
|
|
567
589
|
|
|
568
590
|
def turn_payload(args):
|
|
@@ -578,17 +600,17 @@ def turn_payload(args):
|
|
|
578
600
|
raise CliError(f"无效 JSON: {error}") from error
|
|
579
601
|
if not isinstance(turns, list) or not all(isinstance(item, dict) for item in turns):
|
|
580
602
|
raise CliError("--plan-file 必须是由 expert、question 对象组成的数组")
|
|
581
|
-
return {**payload, "turns": turns,
|
|
603
|
+
return {**payload, "turns": turns, "roster": args.roster}
|
|
582
604
|
if not args.expert or question is None:
|
|
583
605
|
raise CliError("需要 --expert 与 --question/--question-file,或改用 --plan-file")
|
|
584
|
-
return {**payload, "expert": args.expert, "question": question}
|
|
606
|
+
return {**payload, "expert": args.expert, "question": question, "roster": args.roster}
|
|
585
607
|
|
|
586
608
|
|
|
587
609
|
def plan_state(args):
|
|
588
610
|
"""Keep a host summary's cutoff explicit instead of assuming latest means read."""
|
|
589
611
|
base = f"/api/rooms/{args.room}"
|
|
590
612
|
if args.action == "view":
|
|
591
|
-
return RequestPlan(base, view="state")
|
|
613
|
+
return RequestPlan(base, view="state", params=read_context_params(args))
|
|
592
614
|
if args.until < 0:
|
|
593
615
|
raise CliError("--until 不能小于 0")
|
|
594
616
|
payload = {"actor": require_actor(args), "text": text_field(args, "body"), "until": args.until}
|
|
@@ -615,28 +637,30 @@ def plan_api(args):
|
|
|
615
637
|
|
|
616
638
|
|
|
617
639
|
def plan_run(args):
|
|
618
|
-
"""Build one
|
|
619
|
-
path = f"/api/rooms/{args.room}/
|
|
640
|
+
"""Build one revision-bound discussion-session lifecycle request."""
|
|
641
|
+
path = f"/api/rooms/{args.room}/discussion"
|
|
620
642
|
if args.action == "view":
|
|
621
643
|
return RequestPlan(path)
|
|
622
|
-
body = {"action": args.action, "revision": args.revision}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
644
|
+
body = {"action": "end" if args.action == "stop" else args.action, "revision": args.revision}
|
|
645
|
+
if args.action == "start":
|
|
646
|
+
goal = text_field(args, "goal")
|
|
647
|
+
if goal is not None:
|
|
648
|
+
body["goal"] = goal
|
|
649
|
+
limits = {key: getattr(args, key) for key in ("max_rounds", "max_minutes", "execution_minutes")
|
|
650
|
+
if hasattr(args, key)}
|
|
651
|
+
if limits:
|
|
652
|
+
body["limits"] = limits
|
|
653
|
+
if args.submission_id:
|
|
654
|
+
body["submission_id"] = args.submission_id
|
|
626
655
|
return RequestPlan(path, "POST", body)
|
|
627
656
|
|
|
628
657
|
|
|
629
658
|
def plan_execution(args):
|
|
630
|
-
"""
|
|
631
|
-
|
|
632
|
-
if args.action == "
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if not body["session_id"]:
|
|
636
|
-
raise CliError("尚未取得实际 session_id,请先核对接入回执")
|
|
637
|
-
if args.action == "fail":
|
|
638
|
-
body["reason"] = args.reason
|
|
639
|
-
return RequestPlan(f"/api/rooms/{args.room}/events/{args.id}", "POST", body)
|
|
659
|
+
"""Read or fail one durable member execution."""
|
|
660
|
+
path = f"/api/rooms/{args.room}/executions/{args.id}"
|
|
661
|
+
if args.action == "view":
|
|
662
|
+
return RequestPlan(path)
|
|
663
|
+
return RequestPlan(path, "POST", {"action": "fail", "reason": args.reason})
|
|
640
664
|
|
|
641
665
|
|
|
642
666
|
def plan_member(args):
|
|
@@ -785,11 +809,6 @@ def project_result(value, plan, site):
|
|
|
785
809
|
result["guidance"] = GUIDANCE.get(value["phase"], "按本次主持或专家职责,结合最新阶段、安排和必要原文决定下一步。")
|
|
786
810
|
result["url"] = f"{site}/r/{value['id']}"
|
|
787
811
|
return result
|
|
788
|
-
if plan.view == "prompt":
|
|
789
|
-
entry = f"{site}/r/{value['room_id']}?participant={value['expert']}"
|
|
790
|
-
prompt = f"请 {value['expert']} 回应:{value['question']}\n安排编号:{value['id']}\n按需阅读:{entry}\n回答附简短摘要及完整正文,保留关键条件和不确定性。"
|
|
791
|
-
return {"turn_id": value["id"], "status": value["status"], "prompt": prompt,
|
|
792
|
-
"delivery": "未发送;仅生成文字,实际通知后使用 turn mark-notified"}
|
|
793
812
|
return value
|
|
794
813
|
|
|
795
814
|
|
|
@@ -810,7 +829,7 @@ def command_schema(args, registry):
|
|
|
810
829
|
item = {"name": action.dest, "flags": action.option_strings,
|
|
811
830
|
"required": action.required, "help": action.help}
|
|
812
831
|
if action.dest == "actor" and name in ("room create", "room update", "message post",
|
|
813
|
-
"turn create", "turn
|
|
832
|
+
"turn create", "turn cancel", "state set", "attachment upload"):
|
|
814
833
|
item["required"] = True
|
|
815
834
|
if action.choices is not None:
|
|
816
835
|
item["choices"] = list(action.choices)
|
|
@@ -542,7 +542,7 @@ export function claude_window(session_id, env = process.env) {
|
|
|
542
542
|
return null;
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
-
/** Whether this execution goes into a
|
|
545
|
+
/** Whether this execution goes into a manual member's own open window (taken over with `own` permissions) rather than a background process. */
|
|
546
546
|
export function claude_inbox_ready(profile, binding) {
|
|
547
547
|
return profile.adapter === 'claude' && profile.project_access === 'own' && Boolean(profile.session_id)
|
|
548
548
|
&& binding.session_id === profile.session_id && Boolean(claude_window(profile.session_id)?.inbox);
|
|
@@ -573,6 +573,8 @@ async function claude_inbox_delivery(config, inbox, task) {
|
|
|
573
573
|
});
|
|
574
574
|
socket.on('error', error => written ? undefined : reject(not_sent(error)));
|
|
575
575
|
});
|
|
576
|
+
// The discussion runner enters the execution once the session is known, as it does when a process reports it.
|
|
577
|
+
task.on_binding?.(config.session_id);
|
|
576
578
|
const marker = `${task.id}.task.json`;
|
|
577
579
|
setTimeout(() => {
|
|
578
580
|
if (!claude_session_mentions(config.session_id, marker)) {
|
|
@@ -878,6 +880,7 @@ async function cli_delivery(config, binding, task, spawn_process, user_home) {
|
|
|
878
880
|
|
|
879
881
|
/** Queue one immutable request for the official App-managed Sidecar. */
|
|
880
882
|
async function antigravity_delivery(config, binding, task) {
|
|
883
|
+
if (!task.id.startsWith('free-')) throw new Error('Sidecar 只支持自由协作执行请求,旧讨论事件已停用');
|
|
881
884
|
const directory = resolve(config.ipc_directory);
|
|
882
885
|
try {
|
|
883
886
|
mkdirSync(directory, { recursive: true });
|
|
@@ -892,8 +895,8 @@ async function antigravity_delivery(config, binding, task) {
|
|
|
892
895
|
} catch (error) { throw not_sent(error); }
|
|
893
896
|
const path = join(directory, `${task.id}.request.json`);
|
|
894
897
|
if (existsSync(path)) throw new Error('Sidecar 请求已存在;必须核对回执,不自动重复发送');
|
|
895
|
-
atomic_json(path, { id: task.id, mode:
|
|
896
|
-
event_id: Number(task.id.replace(/^
|
|
898
|
+
atomic_json(path, { id: task.id, mode: 'free',
|
|
899
|
+
event_id: Number(task.id.replace(/^free-/, '')), prompt: task.prompt,
|
|
897
900
|
session_id: binding.session_id || '', access_path: task.access_path });
|
|
898
901
|
return { session_id: binding.session_id || '', status: 'sidecar_pending' };
|
|
899
902
|
}
|