unique-sdk 2026.36.0.dev6__tar.gz → 2026.36.0.dev8__tar.gz

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.
Files changed (89) hide show
  1. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/PKG-INFO +1 -1
  2. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/pyproject.toml +1 -1
  3. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/cli.py +83 -27
  4. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/identity.py +57 -4
  5. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/README.md +0 -0
  6. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/__init__.py +0 -0
  7. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_api_requestor.py +0 -0
  8. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_api_resource.py +0 -0
  9. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_api_version.py +0 -0
  10. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_error.py +0 -0
  11. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_http_client.py +0 -0
  12. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_list_object.py +0 -0
  13. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_object_classes.py +0 -0
  14. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_request_options.py +0 -0
  15. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_unique_object.py +0 -0
  16. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_unique_ql.py +0 -0
  17. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_unique_response.py +0 -0
  18. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_util.py +0 -0
  19. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_version.py +0 -0
  20. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/_webhook.py +0 -0
  21. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/__init__.py +0 -0
  22. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_acronyms.py +0 -0
  23. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_agentic_table.py +0 -0
  24. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_analytics_order.py +0 -0
  25. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_benchmarking.py +0 -0
  26. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_briefing.py +0 -0
  27. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_chat_completion.py +0 -0
  28. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_content.py +0 -0
  29. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_elicitation.py +0 -0
  30. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_embedding.py +0 -0
  31. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_event.py +0 -0
  32. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_folder.py +0 -0
  33. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_group.py +0 -0
  34. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_integrated.py +0 -0
  35. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_llm_models.py +0 -0
  36. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_mcp.py +0 -0
  37. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_message.py +0 -0
  38. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_message_assessment.py +0 -0
  39. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_message_execution.py +0 -0
  40. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_message_log.py +0 -0
  41. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_message_tool.py +0 -0
  42. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_module.py +0 -0
  43. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  44. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_search.py +0 -0
  45. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_search_string.py +0 -0
  46. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  47. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_space.py +0 -0
  48. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_user.py +0 -0
  49. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/api_resources/_web_search.py +0 -0
  50. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/__init__.py +0 -0
  51. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/__main__.py +0 -0
  52. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/__init__.py +0 -0
  53. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  54. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/agentic_table.py +0 -0
  55. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/agentic_table_write.py +0 -0
  56. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/browser.py +0 -0
  57. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/cite_file.py +0 -0
  58. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/elicitation.py +0 -0
  59. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/files.py +0 -0
  60. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/folders.py +0 -0
  61. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/mcp.py +0 -0
  62. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/navigation.py +0 -0
  63. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/read.py +0 -0
  64. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  65. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/search.py +0 -0
  66. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/subagent.py +0 -0
  67. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/web_search.py +0 -0
  68. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/commands/web_search_config.py +0 -0
  69. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/config.py +0 -0
  70. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/formatting.py +0 -0
  71. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/metadata_filter.py +0 -0
  72. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/shell.py +0 -0
  73. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
  74. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  75. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  76. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  77. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  78. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  79. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  80. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
  81. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  82. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/cli/state.py +0 -0
  83. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/utils/analytics_order_run.py +0 -0
  84. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/utils/benchmarking_run.py +0 -0
  85. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/utils/chat_history.py +0 -0
  86. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/utils/chat_in_space.py +0 -0
  87. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/utils/file_io.py +0 -0
  88. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/utils/sources.py +0 -0
  89. {unique_sdk-2026.36.0.dev6 → unique_sdk-2026.36.0.dev8}/unique_sdk/utils/token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-sdk
3
- Version: 2026.36.0.dev6
3
+ Version: 2026.36.0.dev8
4
4
  Summary:
5
5
  Author: Martin Fadler, Konstantin Krauss, Andreas Hauri
6
6
  Author-email: Martin Fadler <martin.fadler@unique.ch>, Konstantin Krauss <konstantin@unique.ch>, Andreas Hauri <andreas@unique.ch>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_sdk"
3
- version = "2026.36.0.dev6"
3
+ version = "2026.36.0.dev8"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -94,7 +94,11 @@ from unique_sdk.cli.commands.web_search import (
94
94
  )
95
95
  from unique_sdk.cli.commands.web_search_config import ENV_CONFIG_PATH
96
96
  from unique_sdk.cli.config import load_config
97
- from unique_sdk.cli.identity import TurnIdentityError, resolve_message_id
97
+ from unique_sdk.cli.identity import (
98
+ TurnIdentityError,
99
+ resolve_chat_id,
100
+ resolve_message_id,
101
+ )
98
102
  from unique_sdk.cli.shell import UniqueShell
99
103
  from unique_sdk.cli.state import ShellState
100
104
 
@@ -124,6 +128,33 @@ def _resolve_cli_message_id(
124
128
  return resolved
125
129
 
126
130
 
131
+ def _resolve_cli_chat_id(
132
+ ctx: click.Context,
133
+ explicit: str | None,
134
+ *,
135
+ required: bool = False,
136
+ ) -> str | None:
137
+ """Resolve chat id for Click commands; exit on turn-identity errors.
138
+
139
+ Preboot-placeholder ids (``chat_preboot*``) are re-resolved through the
140
+ turn-identity file so adopted preboot clients never send a chat id the
141
+ platform will reject as nonexistent.
142
+ """
143
+ try:
144
+ resolved = resolve_chat_id(explicit)
145
+ except TurnIdentityError as exc:
146
+ click.echo(f"Error: {exc}", err=True)
147
+ ctx.exit(2)
148
+ if required and not resolved:
149
+ click.echo(
150
+ "Error: chat id is required. Pass --chat-id, or set "
151
+ "UNIQUE_TURN_IDENTITY_FILE / UNIQUE_CHAT_ID.",
152
+ err=True,
153
+ )
154
+ ctx.exit(2)
155
+ return resolved
156
+
157
+
127
158
  MAIN_HELP = """\
128
159
  Unique CLI -- Linux-like file explorer for the Unique AI Platform.
129
160
 
@@ -736,8 +767,12 @@ def uploaded_search(
736
767
  @click.option(
737
768
  "--chat-id",
738
769
  "-c",
739
- required=True,
740
- help="Chat ID for the MCP tool call context.",
770
+ default=None,
771
+ help=(
772
+ "Chat ID for the MCP tool call context. Defaults to the current "
773
+ "turn identity file ($UNIQUE_TURN_IDENTITY_FILE), then "
774
+ "$UNIQUE_CHAT_ID."
775
+ ),
741
776
  )
742
777
  @click.option(
743
778
  "--message-id",
@@ -767,7 +802,7 @@ def uploaded_search(
767
802
  def mcp(
768
803
  ctx: click.Context,
769
804
  payload: str | None,
770
- chat_id: str,
805
+ chat_id: str | None,
771
806
  message_id: str | None,
772
807
  file_path: str | None,
773
808
  use_stdin: bool,
@@ -780,9 +815,9 @@ def mcp(
780
815
 
781
816
  \b
782
817
  The JSON is forwarded 1:1 to the MCP call-tool API. Chat ID and
783
- message ID are provided as separate flags to identify the
784
- conversation context. When --message-id is omitted, the CLI resolves
785
- it from $UNIQUE_TURN_IDENTITY_FILE (preferred) or $UNIQUE_MESSAGE_ID.
818
+ message ID identify the conversation context. When --message-id or
819
+ --chat-id is omitted, the CLI resolves it from
820
+ $UNIQUE_TURN_IDENTITY_FILE (preferred) or the matching env var.
786
821
 
787
822
  \b
788
823
  Input sources (exactly one required):
@@ -800,10 +835,11 @@ def mcp(
800
835
  cat payload.json | unique-cli mcp -c chat_123 -m msg_456 --stdin
801
836
  """
802
837
  resolved_message_id = _resolve_cli_message_id(ctx, message_id, required=True)
838
+ resolved_chat_id = _resolve_cli_chat_id(ctx, chat_id, required=True)
803
839
  click.echo(
804
840
  cmd_mcp(
805
841
  LazyState.get(ctx),
806
- chat_id=chat_id,
842
+ chat_id=resolved_chat_id or "",
807
843
  message_id=resolved_message_id or "",
808
844
  payload=payload,
809
845
  file=file_path,
@@ -826,8 +862,11 @@ def mcp(
826
862
  "--chat-id",
827
863
  "parent_chat_id",
828
864
  default=None,
829
- envvar="UNIQUE_CHAT_ID",
830
- help="Parent chat ID for message correlation.",
865
+ help=(
866
+ "Parent chat ID for message correlation. Defaults to the current "
867
+ "turn identity file ($UNIQUE_TURN_IDENTITY_FILE), then "
868
+ "$UNIQUE_CHAT_ID."
869
+ ),
831
870
  )
832
871
  @click.option(
833
872
  "--message-id",
@@ -881,12 +920,13 @@ def subagent(
881
920
  unique-cli subagent Finance "Summarize Q4 revenue" --reset-chat
882
921
  """
883
922
  resolved_message_id = _resolve_cli_message_id(ctx, parent_message_id)
923
+ resolved_chat_id = _resolve_cli_chat_id(ctx, parent_chat_id)
884
924
  output = cmd_subagent(
885
925
  LazyState.get(ctx),
886
926
  tool_name=tool_name,
887
927
  message=message,
888
928
  config_path=config_path,
889
- parent_chat_id=parent_chat_id,
929
+ parent_chat_id=resolved_chat_id,
890
930
  parent_message_id=resolved_message_id,
891
931
  parent_assistant_id=parent_assistant_id,
892
932
  reset_chat=reset_chat,
@@ -1144,7 +1184,15 @@ def elicit() -> None:
1144
1184
  '"answer" string field when omitted.'
1145
1185
  ),
1146
1186
  )
1147
- @click.option("--chat-id", "-c", default=None, help="Associated chat ID.")
1187
+ @click.option(
1188
+ "--chat-id",
1189
+ "-c",
1190
+ default=None,
1191
+ help=(
1192
+ "Associated chat ID. Defaults to the current turn identity file "
1193
+ "($UNIQUE_TURN_IDENTITY_FILE), then $UNIQUE_CHAT_ID."
1194
+ ),
1195
+ )
1148
1196
  @click.option(
1149
1197
  "--message-id",
1150
1198
  "-m",
@@ -1276,7 +1324,7 @@ def elicit_ask(
1276
1324
  "message": message,
1277
1325
  "tool_name": tool_name,
1278
1326
  "schema": schema,
1279
- "chat_id": chat_id,
1327
+ "chat_id": _resolve_cli_chat_id(ctx, chat_id),
1280
1328
  "message_id": _resolve_cli_message_id(ctx, message_id),
1281
1329
  "timeout": timeout,
1282
1330
  "expires_in_seconds": expires_in_seconds,
@@ -1313,7 +1361,15 @@ def elicit_ask(
1313
1361
  help="JSON schema (required for --mode FORM).",
1314
1362
  )
1315
1363
  @click.option("--url", default=None, help="External URL (required for --mode URL).")
1316
- @click.option("--chat-id", "-c", default=None, help="Associated chat ID.")
1364
+ @click.option(
1365
+ "--chat-id",
1366
+ "-c",
1367
+ default=None,
1368
+ help=(
1369
+ "Associated chat ID. Defaults to the current turn identity file "
1370
+ "($UNIQUE_TURN_IDENTITY_FILE), then $UNIQUE_CHAT_ID."
1371
+ ),
1372
+ )
1317
1373
  @click.option(
1318
1374
  "--message-id",
1319
1375
  "-m",
@@ -1426,7 +1482,7 @@ def elicit_create(
1426
1482
  "tool_name": tool_name,
1427
1483
  "schema": schema,
1428
1484
  "url": url,
1429
- "chat_id": chat_id,
1485
+ "chat_id": _resolve_cli_chat_id(ctx, chat_id),
1430
1486
  "message_id": _resolve_cli_message_id(ctx, message_id),
1431
1487
  "expires_in_seconds": expires_in_seconds,
1432
1488
  "external_elicitation_id": external_elicitation_id,
@@ -2074,12 +2130,12 @@ Examples:
2074
2130
  "--chat-id",
2075
2131
  "chat_id",
2076
2132
  default=None,
2077
- envvar="UNIQUE_CHAT_ID",
2078
2133
  help=(
2079
- "Chat id this call is made on behalf of. Defaults to $UNIQUE_CHAT_ID "
2080
- "(auto-set in Conduct sandboxes). When set, the space's Web Search "
2081
- "toggle is enforced server-side and the call is rejected if Web "
2082
- "Search is disabled for that space."
2134
+ "Chat id this call is made on behalf of. Defaults to the current "
2135
+ "turn identity file ($UNIQUE_TURN_IDENTITY_FILE), then "
2136
+ "$UNIQUE_CHAT_ID (auto-set in Conduct sandboxes). When set, the "
2137
+ "space's Web Search toggle is enforced server-side and the call is "
2138
+ "rejected if Web Search is disabled for that space."
2083
2139
  ),
2084
2140
  )
2085
2141
  @click.pass_context
@@ -2105,7 +2161,7 @@ def web_search_search_cmd(
2105
2161
  crawler_config_raw=crawler_config_raw,
2106
2162
  output_json=output_json,
2107
2163
  config_path=resolved_config,
2108
- chat_id=chat_id,
2164
+ chat_id=_resolve_cli_chat_id(ctx, chat_id),
2109
2165
  )
2110
2166
  _emit_web_search(ctx, output)
2111
2167
 
@@ -2165,12 +2221,12 @@ Examples:
2165
2221
  "--chat-id",
2166
2222
  "chat_id",
2167
2223
  default=None,
2168
- envvar="UNIQUE_CHAT_ID",
2169
2224
  help=(
2170
- "Chat id this call is made on behalf of. Defaults to $UNIQUE_CHAT_ID "
2171
- "(auto-set in Conduct sandboxes). When set, the space's Web Search "
2172
- "toggle is enforced server-side and the call is rejected if Web "
2173
- "Search is disabled for that space."
2225
+ "Chat id this call is made on behalf of. Defaults to the current "
2226
+ "turn identity file ($UNIQUE_TURN_IDENTITY_FILE), then "
2227
+ "$UNIQUE_CHAT_ID (auto-set in Conduct sandboxes). When set, the "
2228
+ "space's Web Search toggle is enforced server-side and the call is "
2229
+ "rejected if Web Search is disabled for that space."
2174
2230
  ),
2175
2231
  )
2176
2232
  @click.pass_context
@@ -2200,7 +2256,7 @@ def web_search_crawl_cmd(
2200
2256
  crawler_config_raw=crawler_config_raw,
2201
2257
  output_json=output_json,
2202
2258
  config_path=resolved_config,
2203
- chat_id=chat_id,
2259
+ chat_id=_resolve_cli_chat_id(ctx, chat_id),
2204
2260
  )
2205
2261
  _emit_web_search(ctx, output)
2206
2262
 
@@ -14,6 +14,12 @@ Resolution precedence for message IDs:
14
14
 
15
15
  When ``UNIQUE_TURN_IDENTITY_FILE`` is set but the file is missing or malformed,
16
16
  resolution fails loudly — silent fallback to a stale env value is forbidden.
17
+
18
+ Chat IDs resolve the same way, with one twist: a client spawned during a
19
+ chatless preboot carries a synthetic ``chat_preboot*`` placeholder in its
20
+ frozen environment. The placeholder never names a real chat (the platform
21
+ rejects it), so it is treated as absent at every precedence level and the
22
+ turn-identity file supplies the adopted chat's real ID.
17
23
  """
18
24
 
19
25
  from __future__ import annotations
@@ -25,6 +31,12 @@ from pathlib import Path
25
31
 
26
32
  TURN_IDENTITY_ENV_VAR = "UNIQUE_TURN_IDENTITY_FILE"
27
33
  MESSAGE_ID_ENV_VAR = "UNIQUE_MESSAGE_ID"
34
+ CHAT_ID_ENV_VAR = "UNIQUE_CHAT_ID"
35
+
36
+ # Synthetic chat-id prefix used by the sandbox runner for clients spawned
37
+ # before their chat exists (chatless preboot). Adoption re-keys the client
38
+ # to the real chat, but the spawn env keeps the placeholder forever.
39
+ PREBOOT_CHAT_ID_PREFIX = "chat_preboot"
28
40
 
29
41
 
30
42
  class TurnIdentityError(ValueError):
@@ -36,12 +48,14 @@ class TurnIdentity:
36
48
  """Parsed contents of the per-turn identity file.
37
49
 
38
50
  The runner writes a richer JSON object (chat/user/company/assistant IDs,
39
- turn counter), but the CLI only consumes ``message_id`` — the one value
40
- that goes stale in a persistent process environment. Extra keys are
41
- ignored; parse a field here only once the CLI actually relies on it.
51
+ turn counter), but the CLI only consumes the values that go stale in a
52
+ persistent process environment: ``message_id`` (every turn) and
53
+ ``chat_id`` (adopted chatless-preboot clients). Extra keys are ignored;
54
+ parse a field here only once the CLI actually relies on it.
42
55
  """
43
56
 
44
57
  message_id: str
58
+ chat_id: str | None = None
45
59
 
46
60
 
47
61
  def read_turn_identity(
@@ -88,7 +102,11 @@ def read_turn_identity(
88
102
  "'message_id' string"
89
103
  )
90
104
 
91
- return TurnIdentity(message_id=message_id.strip())
105
+ chat_id = payload.get("chat_id")
106
+ parsed_chat_id = (
107
+ chat_id.strip() if isinstance(chat_id, str) and chat_id.strip() else None
108
+ )
109
+ return TurnIdentity(message_id=message_id.strip(), chat_id=parsed_chat_id)
92
110
 
93
111
 
94
112
  def resolve_message_id(explicit: str | None = None) -> str | None:
@@ -110,3 +128,38 @@ def resolve_message_id(explicit: str | None = None) -> str | None:
110
128
  if env_id is not None and env_id.strip():
111
129
  return env_id.strip()
112
130
  return None
131
+
132
+
133
+ def _usable_chat_id(value: str | None) -> str | None:
134
+ """A non-empty chat id that is not a preboot placeholder, else None."""
135
+ if value is None:
136
+ return None
137
+ stripped = str(value).strip()
138
+ if not stripped or stripped.startswith(PREBOOT_CHAT_ID_PREFIX):
139
+ return None
140
+ return stripped
141
+
142
+
143
+ def resolve_chat_id(explicit: str | None = None) -> str | None:
144
+ """Resolve the chat ID for a chat-bound CLI operation.
145
+
146
+ Same precedence as :func:`resolve_message_id` (explicit flag, then the
147
+ turn-identity file, then ``$UNIQUE_CHAT_ID``), except a ``chat_preboot*``
148
+ placeholder is skipped at every level — agents pass
149
+ ``--chat-id "$UNIQUE_CHAT_ID"`` from an environment that may have been
150
+ frozen before their chat existed. Returns ``None`` when no source yields
151
+ a real chat id (chat-optional callers then omit the id). Raises
152
+ ``TurnIdentityError`` when the turn-identity file is configured but
153
+ unusable.
154
+ """
155
+ explicit_id = _usable_chat_id(explicit)
156
+ if explicit_id is not None:
157
+ return explicit_id
158
+
159
+ identity = read_turn_identity()
160
+ if identity is not None:
161
+ file_id = _usable_chat_id(identity.chat_id)
162
+ if file_id is not None:
163
+ return file_id
164
+
165
+ return _usable_chat_id(os.environ.get(CHAT_ID_ENV_VAR))