cdx-manager 0.21.6 → 0.23.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/src/cli_args.py CHANGED
@@ -33,26 +33,28 @@ CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text..
33
33
  MEMORY_USAGE = "Usage: cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]"
34
34
  HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--source-conversation ID | --source-native-transcript PATH | --source-transcript PATH] [--json]"
35
35
  LABEL_USAGE = "Usage: cdx label <name> <label> [--json] | cdx label <name> --clear [--json]"
36
- SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--notify on|off] [--notify-preview on|off] [--scrub-env on|off] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]"
37
- UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--scrub-env|--model|--fallback-model|--budget|--extra-args|--priority|--all) [--json]"
36
+ SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--notify on|off] [--notify-preview on|off] [--scrub-env on|off] [--shared-memory on|off] [--retro on|off] [--policy use|ask-first|never] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]"
37
+ UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--scrub-env|--shared-memory|--retro|--policy|--model|--fallback-model|--budget|--extra-args|--priority|--all) [--json]"
38
38
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
39
39
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
40
40
  CONFIGS_USAGE = "Usage: cdx configs [--json]"
41
41
  HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
42
- STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
42
+ STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]"
43
+ STATS_GROUPS = ("session", "project", "model")
43
44
  LAST_USAGE = "Usage: cdx last [--json]"
44
- RESUME_USAGE = "Usage: cdx resume <name> [--json]"
45
- CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
45
+ RESUME_USAGE = "Usage: cdx resume <name> [--conversation <id>] [--json]"
46
+ CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--conversation <id>] [--json]"
47
+ SESSIONS_USAGE = "Usage: cdx sessions [--all] --json"
46
48
  SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
47
49
  NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
48
- RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--failover] [--refresh] --json"
50
+ RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--label TEXT] [--max-parallel N] [--detach] [--failover] [--refresh] --json"
49
51
  RUN_JSON_REQUIRED = "cdx run: --json is required."
50
52
  RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
51
53
  RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
52
54
  RUN_CWD_REQUIRED = "cdx run: --cwd PATH is required."
53
55
  RUN_PROMPT_SOURCE_REQUIRED = "cdx run: specify exactly one prompt source: --prompt TEXT or --prompt-file PATH."
54
56
  RUN_DETACH_FAILOVER_CONFLICT = "cdx run: --failover needs to watch the run, which --detach gives up."
55
- RUN_KIND_VALUES = ("assistant", "code-review")
57
+ RUN_KIND_VALUES = ("assistant", "code-review", "retro")
56
58
  # Aliases of the shared definitions in config, kept only so the existing
57
59
  # RUN_* spellings in this module keep reading naturally. They are references,
58
60
  # not copies: config.py is the single owner.
@@ -60,7 +62,7 @@ RUN_PERMISSION_VALUES = PERMISSION_INPUT_VALUES
60
62
  RUN_PERMISSION_CANONICAL_VALUES = PERMISSION_VALUES
61
63
  RUN_PERMISSION_ALIASES = PERMISSION_ALIASES
62
64
  RUN_EFFORT_VALUES = REASONING_EFFORT_VALUES
63
- RUNS_USAGE = "Usage: cdx runs [--limit N] [--since 7d|today|DATE] --json"
65
+ RUNS_USAGE = "Usage: cdx runs [--limit N] [--since 7d|today|DATE] [--running] --json"
64
66
  RUN_STATUS_USAGE = "Usage: cdx run-status <run_id> --json"
65
67
  RUN_REPORT_USAGE = "Usage: cdx run-report <run_id> --json"
66
68
  RUN_TAIL_USAGE = "Usage: cdx run-tail <run_id> [--lines N] --json"
@@ -79,6 +81,28 @@ ARG_CODE_OUT_OF_RANGE = "argument_value_out_of_range"
79
81
  ARG_CODE_UNKNOWN = "unknown_argument"
80
82
 
81
83
 
84
+ def _pop_value_flag(args, flag, usage):
85
+ """Remove `flag VALUE` / `flag=VALUE` from args; return (value or None, rest)."""
86
+ value, rest, index = None, [], 0
87
+ while index < len(args):
88
+ arg = args[index]
89
+ name, sep, inline = arg.partition("=")
90
+ if name != flag:
91
+ rest.append(arg)
92
+ index += 1
93
+ continue
94
+ if sep:
95
+ candidate, index = inline, index + 1
96
+ elif index + 1 < len(args):
97
+ candidate, index = args[index + 1], index + 2
98
+ else:
99
+ raise CdxError(usage)
100
+ if value is not None or not candidate:
101
+ raise CdxError(usage)
102
+ value = candidate
103
+ return value, rest
104
+
105
+
82
106
  def _parse_json_flag(args):
83
107
  json_flag = "--json" in args
84
108
  cleaned = [arg for arg in args if arg != "--json"]
@@ -253,11 +277,14 @@ def _parse_set_args(args):
253
277
  "--notify": {"key": "notify", "type": "str", "default": None, "transform": _on_off("notify")},
254
278
  "--notify-preview": {"key": "notify_preview", "type": "str", "default": None, "transform": _on_off("notify preview")},
255
279
  "--scrub-env": {"key": "scrub_env", "type": "str", "default": None, "transform": _on_off("scrub env")},
280
+ "--shared-memory": {"key": "shared_memory", "type": "str", "default": None, "transform": _on_off("shared memory")},
281
+ "--retro": {"key": "retro", "type": "str", "default": None, "transform": _on_off("retro")},
256
282
  "--model": {"key": "model", "type": "str", "default": None},
257
283
  "--fallback-model": {"key": "fallback_model", "type": "str", "default": None},
258
284
  "--budget": {"key": "budget", "type": "str", "default": None, "transform": _parse_budget_value},
259
285
  "--extra-args": {"key": "extra_args", "type": "str", "default": None},
260
286
  "--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
287
+ "--policy": {"key": "policy", "type": "str", "default": None},
261
288
  "--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
262
289
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
263
290
  "--json": {"key": "json", "type": "bool", "default": False},
@@ -274,7 +301,7 @@ def _parse_set_args(args):
274
301
  raise CdxError(SET_USAGE)
275
302
  settings = {
276
303
  key: parsed[key]
277
- for key in ("power", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "model", "fallback_model", "budget", "extra_args", "priority")
304
+ for key in ("power", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy")
278
305
  if parsed[key] is not None
279
306
  }
280
307
  if not settings:
@@ -299,11 +326,14 @@ def _parse_unset_args(args):
299
326
  "--notify": {"key": "notify", "type": "bool", "default": False},
300
327
  "--notify-preview": {"key": "notify_preview", "type": "bool", "default": False},
301
328
  "--scrub-env": {"key": "scrub_env", "type": "bool", "default": False},
329
+ "--shared-memory": {"key": "shared_memory", "type": "bool", "default": False},
330
+ "--retro": {"key": "retro", "type": "bool", "default": False},
302
331
  "--model": {"key": "model", "type": "bool", "default": False},
303
332
  "--fallback-model": {"key": "fallback_model", "type": "bool", "default": False},
304
333
  "--budget": {"key": "budget", "type": "bool", "default": False},
305
334
  "--extra-args": {"key": "extra_args", "type": "bool", "default": False},
306
335
  "--priority": {"key": "priority", "type": "bool", "default": False},
336
+ "--policy": {"key": "policy", "type": "bool", "default": False},
307
337
  "--all": {"key": "all", "type": "bool", "default": False},
308
338
  "--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
309
339
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, UNSET_USAGE)},
@@ -320,8 +350,8 @@ def _parse_unset_args(args):
320
350
  if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
321
351
  raise CdxError(UNSET_USAGE)
322
352
  _UNSET_KEYS = (
323
- "power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "scrub_env",
324
- "model", "fallback_model", "budget", "extra_args", "priority",
353
+ "power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "shared_memory", "retro",
354
+ "model", "fallback_model", "budget", "extra_args", "priority", "policy",
325
355
  )
326
356
  keys = list(_UNSET_KEYS) if parsed["all"] else [key for key in _UNSET_KEYS if parsed[key]]
327
357
  if not keys:
@@ -513,6 +543,9 @@ def cdx_schema():
513
543
  "provider_failed",
514
544
  "provider_timeout",
515
545
  "run_cancelled",
546
+ "too_many_runs",
547
+ "failover_exhausted",
548
+ "failover_limit_reached",
516
549
  "invalid_request",
517
550
  "cdx_error",
518
551
  ],
@@ -611,6 +644,17 @@ def _parse_run_effort(reasoning_effort, power):
611
644
  ) from error
612
645
 
613
646
 
647
+ def _parse_max_parallel(value):
648
+ try:
649
+ parsed = int(value)
650
+ except (TypeError, ValueError):
651
+ parsed = 0
652
+ if parsed < 1:
653
+ raise CdxArgumentError("cdx run: --max-parallel takes a whole number of at least 1.",
654
+ code=ARG_CODE_INVALID_VALUE, arguments=["--max-parallel"])
655
+ return parsed
656
+
657
+
614
658
  def _parse_run_args(args):
615
659
  parsed = _parse_run_flag_args(args, {
616
660
  "--cwd": {"key": "cwd", "type": "str", "default": None},
@@ -627,6 +671,8 @@ def _parse_run_args(args):
627
671
  "--detach": {"key": "detach", "type": "bool", "default": False},
628
672
  "--failover": {"key": "failover", "type": "bool", "default": False},
629
673
  "--refresh": {"key": "refresh", "type": "bool", "default": False},
674
+ "--label": {"key": "label", "type": "str", "default": None},
675
+ "--max-parallel": {"key": "max_parallel", "type": "str", "default": None, "transform": _parse_max_parallel},
630
676
  "--json": {"key": "json", "type": "bool", "default": False},
631
677
  }, RUN_USAGE, positionals_key="names", max_positionals=1)
632
678
  if not parsed["json"]:
@@ -681,6 +727,8 @@ def _parse_run_args(args):
681
727
  "timeout_seconds": parsed["timeout_seconds"],
682
728
  "detach": parsed["detach"],
683
729
  "failover": parsed["failover"],
730
+ "label": parsed["label"],
731
+ "max_parallel": parsed["max_parallel"],
684
732
  "refresh": parsed["refresh"],
685
733
  "reasoning_effort": effort.get("reasoning_effort"),
686
734
  "power": effort.get("power"),
@@ -845,12 +893,16 @@ def _parse_stats_args(args, now=None):
845
893
  "--since": {"key": "since", "type": "str", "default": None},
846
894
  "--from": {"key": "from", "type": "str", "default": None},
847
895
  "--to": {"key": "to", "type": "str", "default": None},
896
+ "--by": {"key": "by", "type": "str", "default": "session"},
848
897
  "--json": {"key": "json", "type": "bool", "default": False},
849
898
  }, STATS_USAGE, positionals_key="names", max_positionals=1)
899
+ if parsed["by"] not in STATS_GROUPS:
900
+ raise CdxError(STATS_USAGE)
850
901
  period = _parse_history_period(parsed, now, STATS_USAGE)
851
902
  return {
852
903
  "name": parsed["names"][0] if parsed["names"] else None,
853
904
  "period": period,
905
+ "by": parsed["by"],
854
906
  "json": parsed["json"],
855
907
  }
856
908
 
@@ -1004,8 +1056,13 @@ def _parse_runs_args(rest, now=None):
1004
1056
  parsed = _parse_flag_args(rest, {
1005
1057
  "--limit": {"key": "limit", "type": "str", "default": 20, "transform": _parse_positive_int},
1006
1058
  "--since": {"key": "since", "type": "str", "default": None},
1059
+ "--running": {"key": "running", "type": "bool", "default": False},
1007
1060
  "--json": {"key": "json", "type": "bool", "default": False},
1008
1061
  }, RUNS_USAGE, max_positionals=0)
1062
+ if parsed["running"] and parsed["since"] is not None:
1063
+ # --since returns completed runs only, --running the ones not completed:
1064
+ # together they can only ever answer "nothing".
1065
+ raise CdxError("cdx runs: --running and --since are mutually exclusive.")
1009
1066
  since = None
1010
1067
  if parsed["since"] is not None:
1011
1068
  # Same cursor forms as `cdx history --since`, parsed by the same code:
@@ -1019,6 +1076,7 @@ def _parse_runs_args(rest, now=None):
1019
1076
  # a watchdog behind by more than `limit` completions would never see
1020
1077
  # the older ones at all.
1021
1078
  "limit_given": any(arg == "--limit" or arg.startswith("--limit=") for arg in rest),
1079
+ "running": parsed["running"],
1022
1080
  "json": parsed["json"],
1023
1081
  }
1024
1082
 
@@ -265,6 +265,9 @@ from .commands.status import (
265
265
  from .commands.status import (
266
266
  handle_next as handle_next,
267
267
  )
268
+ from .commands.status import (
269
+ handle_sessions as handle_sessions,
270
+ )
268
271
  from .commands.status import (
269
272
  handle_stats as handle_stats,
270
273
  )
@@ -249,13 +249,23 @@ def _bootstrap_claude_setup_token(session, ctx):
249
249
  ctx["out"](
250
250
  "Minting a long-lived Claude setup-token for this session.\n"
251
251
  )
252
- run_info = _run_interactive_provider_command(
253
- session,
254
- "setup-token",
255
- spawn=ctx.get("spawn"),
256
- env_override=ctx.get("env"),
257
- signal_emitter=ctx.get("signal_emitter"),
258
- )
252
+ try:
253
+ run_info = _run_interactive_provider_command(
254
+ session,
255
+ "setup-token",
256
+ spawn=ctx.get("spawn"),
257
+ env_override=ctx.get("env"),
258
+ signal_emitter=ctx.get("signal_emitter"),
259
+ )
260
+ except Exception as error:
261
+ # A failed or interrupted run can still have printed the token.
262
+ failed_path = (getattr(error, "run_info", None) or {}).get("transcript_path")
263
+ if failed_path:
264
+ try:
265
+ os.remove(failed_path)
266
+ except OSError:
267
+ pass
268
+ raise
259
269
  transcript_path = run_info.get("transcript_path")
260
270
  if not transcript_path or not os.path.isfile(transcript_path):
261
271
  raise CdxError(
@@ -304,11 +314,14 @@ def _format_launch_config(session, use_color=False):
304
314
  ("notify", "Agent alerts"),
305
315
  ("notify_preview", "Response preview"),
306
316
  ("scrub_env", "Scrub agent env"),
317
+ ("shared_memory", "Shared memory"),
318
+ ("retro", "Session retro"),
307
319
  ("model", "Model"),
308
320
  ("fallback_model", "Fallback"),
309
321
  ("budget", "Budget"),
310
322
  ("extra_args", "Extra args"),
311
323
  ("priority", "Priority"),
324
+ ("policy", "Policy"),
312
325
  ]:
313
326
  rows.append([
314
327
  _dim(label, use_color),
@@ -330,14 +343,14 @@ def _format_launch_settings_hint(name="<name>"):
330
343
  )
331
344
 
332
345
  def _format_launch_setting_value(launch, key, use_color=False):
333
- if key in ("fast", "rtk", "logics", "notify", "notify_preview", "scrub_env"):
346
+ if key in ("fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "shared_memory", "retro"):
334
347
  if launch.get(key) is True:
335
348
  return _style("on", "32", use_color)
336
349
  if launch.get(key) is False:
337
350
  return _style("off", "2", use_color)
338
351
  if key == "logics":
339
352
  return _dim("auto", use_color)
340
- if key in ("notify", "notify_preview", "scrub_env"):
353
+ if key in ("notify", "notify_preview", "scrub_env", "shared_memory", "retro"):
341
354
  return _style("off", "2", use_color)
342
355
  return _dim("default", use_color)
343
356
  value = launch.get(key)
@@ -6,8 +6,9 @@ Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
6
6
  import os
7
7
  import shlex
8
8
 
9
+ from .. import retro, shared_memory
9
10
  from ..agent_notify import notifications_enabled, provision, supports_agent_alerts
10
- from ..cli_args import CAN_RESUME_USAGE, HANDOFF_USAGE, RESUME_USAGE, _parse_json_flag
11
+ from ..cli_args import CAN_RESUME_USAGE, HANDOFF_USAGE, RESUME_USAGE, _parse_json_flag, _pop_value_flag
11
12
  from ..cli_helpers import (
12
13
  API_SCHEMA_VERSION,
13
14
  _json_failure,
@@ -19,7 +20,7 @@ from ..cli_helpers import (
19
20
  _write_update_notice,
20
21
  )
21
22
  from ..cli_render import _dim, _info, _success, _warn
22
- from ..config import PROVIDER_CODEX
23
+ from ..config import PROVIDER_CLAUDE, PROVIDER_CODEX
23
24
  from ..errors import CdxError
24
25
  from ..handoff import HandoffSourceError, launch_prompt, load_entry, prepare_entry, resolve_source
25
26
  from ..interactive_usage import extract_interactive_usage, transcript_predates_run, usage_delta
@@ -29,16 +30,29 @@ from ..provider_runtime import (
29
30
  _ensure_session_authentication,
30
31
  _get_auth_home,
31
32
  _run_interactive_provider_command,
33
+ with_requested_conversation,
32
34
  )
33
35
  from ..run_command import PROMPT_STDIN, read_run_prompt
34
36
  from ..run_usage import estimate_cost
35
37
 
36
- LAUNCH_USAGE = "Usage: cdx <name> [--dir PATH] [--prompt TEXT|--prompt-file PATH] [--resume] [--json]"
38
+ LAUNCH_USAGE = "Usage: cdx <name> [--dir PATH] [--prompt TEXT|--prompt-file PATH|--task TEXT [--end-state TEXT]] [--resume] [--json]"
39
+
40
+ #: The first message of a session started with --task. The end state is what the
41
+ #: user's go-ahead covers: a bare "show me first" made sessions stop after a commit
42
+ #: and wait for a push they could have made.
43
+ TASK_PROMPT = """Task: {task}
44
+
45
+ {end_state}
46
+
47
+ Before starting, read this repository's instructions (AGENTS.md, CLAUDE.md and what they reference) and use its skills where one fits; they override general habits."""
48
+ END_STATE = """End state: {end_state}
49
+ Reaching it is approved: take every step it needs, including the commits, pushes or pull requests it names, without asking again. Stop and ask only for a decision it does not cover."""
50
+ NO_END_STATE = """No end state was given: stop and show the result before any commit, push or pull request."""
37
51
 
38
52
 
39
53
  def _parse_launch_args(args):
40
54
  """Return the per-launch directory, initial prompt source and ordinary launch flags."""
41
- values = {"--dir": None, "--prompt": None, "--prompt-file": None}
55
+ values = {"--dir": None, "--prompt": None, "--prompt-file": None, "--task": None, "--end-state": None}
42
56
  cleaned = []
43
57
  index = 0
44
58
  while index < len(args):
@@ -59,6 +73,13 @@ def _parse_launch_args(args):
59
73
  values[flag] = value
60
74
  if values["--prompt"] is not None and values["--prompt-file"] is not None:
61
75
  raise CdxError("cdx <name>: --prompt and --prompt-file are mutually exclusive.")
76
+ if values["--end-state"] is not None and values["--task"] is None:
77
+ raise CdxError("cdx <name>: --end-state goes with --task.")
78
+ if values["--task"] is not None:
79
+ if values["--prompt"] is not None or values["--prompt-file"] is not None:
80
+ raise CdxError("cdx <name>: --task replaces --prompt and --prompt-file.")
81
+ end = END_STATE.format(end_state=values["--end-state"]) if values["--end-state"] else NO_END_STATE
82
+ values["--prompt"] = TASK_PROMPT.format(task=values["--task"], end_state=end)
62
83
  if values["--prompt-file"] == PROMPT_STDIN:
63
84
  raise CdxError("cdx <name>: --prompt-file - is not supported; the interactive session needs standard input.")
64
85
  prompt = {"prompt": values["--prompt"], "prompt_file": values["--prompt-file"]}
@@ -140,12 +161,14 @@ def _format_resume_capability(capability, use_color=False):
140
161
  )
141
162
 
142
163
  def handle_can_resume(rest, ctx):
164
+ conversation_id, rest = _pop_value_flag(rest, "--conversation", CAN_RESUME_USAGE)
143
165
  json_flag, args = _parse_json_flag(rest)
144
166
  if len(args) != 1:
145
167
  raise CdxError(CAN_RESUME_USAGE)
146
168
  session = ctx["service"]["get_session"](args[0])
147
169
  if not session:
148
170
  raise CdxError(f"Unknown session: {args[0]}")
171
+ requested = with_requested_conversation(session, conversation_id) if conversation_id else session
149
172
  if session.get("enabled", True) is False:
150
173
  capability = {
151
174
  "session": session["name"],
@@ -155,8 +178,19 @@ def handle_can_resume(rest, ctx):
155
178
  "reason": "session_disabled",
156
179
  "command_preview": [],
157
180
  }
181
+ elif requested is None:
182
+ capability = {
183
+ "session": session["name"],
184
+ "provider": session["provider"],
185
+ "resumable": False,
186
+ "strategy": "provider_conversation_id",
187
+ "identity": conversation_id,
188
+ "provenance": "requested",
189
+ "reason": "conversation_not_found",
190
+ "command_preview": [],
191
+ }
158
192
  else:
159
- capability = _resume_capability_for_session(session, ctx)
193
+ capability = _resume_capability_for_session(requested, ctx)
160
194
  if json_flag:
161
195
  _write_json(ctx, {
162
196
  "schema_version": API_SCHEMA_VERSION,
@@ -168,13 +202,15 @@ def handle_can_resume(rest, ctx):
168
202
  return 0
169
203
 
170
204
  def handle_resume(rest, ctx):
205
+ conversation_id, rest = _pop_value_flag(rest, "--conversation", RESUME_USAGE)
171
206
  directory, rest, prompt = _parse_launch_args(rest)
172
207
  if prompt["prompt"] is not None or prompt["prompt_file"] is not None:
173
208
  raise CdxError("cdx resume: --prompt and --prompt-file are not supported when resuming.")
174
209
  json_flag, args = _parse_json_flag(rest)
175
210
  if len(args) != 1:
176
211
  raise CdxError(RESUME_USAGE)
177
- return handle_launch(args[0], ctx, resume=True, force_json=json_flag, directory=directory)
212
+ return handle_launch(args[0], ctx, resume=True, force_json=json_flag, directory=directory,
213
+ conversation_id=conversation_id)
178
214
 
179
215
 
180
216
  def _notification_provisioning_enabled(session, ctx, json_flag):
@@ -202,7 +238,23 @@ def _notification_provisioning_enabled(session, ctx, json_flag):
202
238
  return True
203
239
 
204
240
 
205
- def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None, directory=None):
241
+ def _link_shared_memory(session, ctx, cwd, json_flag):
242
+ if session["provider"] != PROVIDER_CLAUDE or (session.get("launch") or {}).get("shared_memory") is not True:
243
+ return
244
+ try:
245
+ status, detail = shared_memory.link(_get_auth_home(session), ctx["service"]["base_dir"], cwd)
246
+ except OSError as error:
247
+ status, detail = "unsupported", str(error)
248
+ if json_flag or status == "linked":
249
+ return
250
+ if status == "conflict":
251
+ detail = f"merge these files by hand first: {', '.join(detail)}"
252
+ message = f"Shared memory not linked for {session['name']}"
253
+ ctx["out"](f"{_warn(message, ctx['use_color'])} {detail}\n")
254
+
255
+
256
+ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None, directory=None,
257
+ conversation_id=None):
206
258
  json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
207
259
  if force_json is not None:
208
260
  json_flag = force_json
@@ -215,6 +267,8 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
215
267
  raise CdxError(f"Unknown session: {command}")
216
268
  if session.get("enabled", True) is False:
217
269
  raise CdxError(f"Session is disabled: {command}")
270
+ if conversation_id and with_requested_conversation(session, conversation_id) is None:
271
+ raise CdxError(f"Conversation {conversation_id} not found in session {command}.", code="conversation_not_found")
218
272
  capability = _resume_capability_for_session(session, ctx) if resume else None
219
273
  if capability and not capability["resumable"]:
220
274
  raise CdxError(
@@ -222,6 +276,8 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
222
276
  )
223
277
  cwd = _launch_directory(session, ctx, directory, json_flag)
224
278
  session = ctx["service"]["launch_session"](command, resume=resume)
279
+ if conversation_id:
280
+ session = with_requested_conversation(session, conversation_id)
225
281
  capability = _resume_capability_for_session(session, ctx) if resume else None
226
282
  if not json_flag and not ctx["stdin_is_tty"]:
227
283
  ctx["out"](f"Using directory: {cwd}\n")
@@ -262,6 +318,7 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
262
318
  # a real turn — so it gets no instruction it does not need.
263
319
  notice += " — approve them once in Codex to enable"
264
320
  ctx["out"](f"{_info(notice, ctx['use_color'])}\n")
321
+ _link_shared_memory(session, ctx, cwd, json_flag)
265
322
  runtime_run_id = None
266
323
 
267
324
  def runtime_lifecycle(event, info):
@@ -335,6 +392,8 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
335
392
  })
336
393
  if not json_flag:
337
394
  ctx["out"](f"{_goodbye_line(session, success_run_info, ctx['use_color'])}\n")
395
+ if retro.start(session, cwd, success_run_info.get("provider_transcript_path"), env=ctx.get("env")) and not json_flag:
396
+ ctx["out"](f"{_dim('Session retro started; read it later with `cdx retro`', ctx['use_color'])}\n")
338
397
  if json_flag:
339
398
  extra = {"session": ctx["service"]["get_session"](session["name"]), "cwd": cwd}
340
399
  if capability:
@@ -775,6 +775,7 @@ def handle_update(rest, ctx):
775
775
  package_root=ctx["options"].get("packageRoot"),
776
776
  prefix=ctx["options"].get("prefix"),
777
777
  base_prefix=ctx["options"].get("basePrefix"),
778
+ env=ctx.get("env"),
778
779
  )
779
780
  results = run_update_plan(plan, runner=ctx["options"].get("runUpdate"), env=ctx.get("env"))
780
781
  failed = any((result.get("returncode") != 0) for result in results)