345 tool_schemas: list |
None,
348 """Load a named session without entering the interactive picker."""
349 func =
"_load_session_by_name_for_startup"
350 meta = load_session_metadata(name, sessions_dir=sessions_dir)
351 session_name = meta.get(
"session_name")
or name
355 model = meta.get(
"model")
360 session.reasoning_effort = meta.get(
"reasoning_effort", session.reasoning_effort)
361 session.chat_logs = meta.get(
"chat_logs", [])
362 session.stats = TokenStats.from_dict(meta.get(
"stats", {}))
363 session.created = meta.get(
"created")
or (
364 session_name.split(
"_", 1)[1]
if "_" in session_name
else "unknown"
366 session.ui_prefs = dict(meta.get(
"ui", {})
or {})
367 apply_session_metadata_fields(session, meta)
368 session.dirty =
False
371 log_data = load_chat_log(session_name, sessions_dir=sessions_dir)
372 session.messages = log_data.get(
"messages", session.messages)
373 except FileNotFoundError
as exc:
374 _dbg(
"OC_CHAT.MAIN", func, f
"VALIDATION_ERROR: {session_name}: {exc}", 3)
376 f
"[Note: Session '{session_name}' has no chat log on disk. "
377 "Starting with fresh context.]"
379 except Exception
as exc:
383 f
"SYSTEM_ERROR: parse {session_name}: {type(exc).__name__}: {exc}",
387 session.load_failed =
True
388 presenter.show_error(
389 f
"[Warning: Could not parse chat log for session '{session_name}' "
390 f
"({exc}). On-disk chat log will NOT be overwritten on save until "
396 _migrate_legacy_system_prompt(session, tool_schemas)
397 ensure_system_includes_tool_reference(session, tool_schemas)
402 m.get(
"role") ==
"user"
403 and (m.get(
"content")
or "").startswith(VARIABLE_CONTEXT_SENTINEL)
404 for m
in session.messages
407 refresh_variable_context(session)
409 _inject_variable_context(session)
412 f
"Loaded session '{session_name}' — "
413 f
"{session.stats.message_count} messages, "
414 f
"{session.stats.tool_call_count} tool calls, "
415 f
"{session.stats.total_tokens:,} tokens "
416 f
"(model: {session.model})"
631def main(argv: list[str] |
None =
None) ->
None:
632 """Start the OC Chat REPL.
634 *argv* is exposed for testing; defaults to ``sys.argv[1:]`` when None.
643 _diagnostics_init(intercept_streams=
False, intercept_exceptions=
False)
644 _dbg(
"OC_CHAT.MAIN", func, f
"oc_chat v{_VERSION} starting", 2)
650 signal.signal(signal.SIGINT, _o3_sigint_handler)
652 args = build_arg_parser().parse_args(argv)
657 _cli_mode =
"option-c" if getattr(args,
"option_c",
False)
else getattr(args,
"mode",
None)
658 init_runtime_settings(_cli_mode)
666 _active_profile = resolve_risk_profile()
667 _raw_profile = os.environ.get(
"OC_CHAT_PROFILE",
"").strip()
668 if _raw_profile
and _raw_profile.lower() != _active_profile:
672 print(f
"\nWARNING: OC_CHAT_PROFILE={_raw_profile!r} is not a recognized risk "
673 f
"profile; running '{_active_profile}'. Valid: permissive, guarded, isolated.\n",
676 check_deployment_shape(_sandbox_root(), _active_profile)
677 except DeploymentShapeError
as _shape_exc:
678 print(f
"\nERROR: {_shape_exc}\n", file=sys.stderr)
686 if getattr(args,
"auto",
False):
688 if session_mode.auto_mode_available():
689 session_mode.set_mode(session_mode.AUTO)
692 print(f
"\nWARNING: --auto applies only under the permissive risk "
693 f
"profile; running '{_active_profile}', so every confirmable "
694 f
"call will still reach you.\n", file=sys.stderr)
701 if args.no_color
and os.getenv(
"NO_COLOR")
is None:
702 os.environ[
"NO_COLOR"] =
"1"
708 provider_name = os.environ.get(
"OC_CHAT_PROVIDER",
"deepseek")
710 if args.input
is not None or args.output
is not None:
713 raise SystemExit(run_batch_turn(
714 session_name=args.session,
715 input_path=args.input,
716 output_path=args.output,
717 provider_name=provider_name,
718 presenter=select_presenter(args),
731 provider = provider_plugin.registry.get(provider_name)
732 except KeyError
as e:
733 validation = validate_runtime_config(
734 provider_name=provider_name,
735 provider_names=provider_plugin.registry.names(),
736 sessions_dir=sessions_dir,
740 presenter.show_error(f
"ERROR: {e}")
743 validation = validate_runtime_config(
744 provider_name=provider_name,
745 provider_names=provider_plugin.registry.names(),
747 sessions_dir=sessions_dir,
754 mig = migrate_legacy_layout(sessions_dir)
755 if mig[
"migrated"]
or mig[
"recovered"]
or mig[
"orphans_moved"]:
758 parts.append(f
"{mig['migrated']} session(s) migrated")
760 parts.append(f
"{mig['recovered']} chat log(s) recovered from backup")
761 if mig[
"orphans_moved"]:
762 parts.append(f
"{mig['orphans_moved']} orphan log(s) moved to legacy/")
763 presenter.show_info(
"Storage upgrade: " +
", ".join(parts) +
".")
765 presenter.show_error(
766 "Migration warnings: " +
"; ".join(mig[
"errors"][:3])
767 + (f
" (+{len(mig['errors']) - 3} more)" if len(mig[
"errors"]) > 3
else "")
769 except Exception
as exc:
771 _dbg(
"OC_CHAT.MAIN", func, f
"SYSTEM_ERROR: migration: {type(exc).__name__}: {exc}", 1, override=
True)
772 presenter.show_error(f
"Storage migration failed: {exc}")
786 startup_driver =
PromptDriver(
None, turn_stats=turn_stats)
800 def _apply_lane_selection(selection):
801 """Plan 217 T4: apply a confirmed lane selection to the Agent.
803 ``main`` owns the Agent whose client and model manager the switch
804 rebuilds, so the opening menu delegates the apply step here rather
805 than importing the Agent into ``startup``. Returning the rebuilt
806 pair lets the menu rebind its own locals — otherwise the very next
807 new-session prompt would list the OLD lane's models.
811 switch_lane(agent, selection.lane_name, selection.model_id)
812 return agent.model_manager, agent.client
814 session = startup_session_prompt(
817 model_manager=agent.model_manager,
818 tool_schemas=agent.tool_schemas,
819 prompt_driver=startup_driver,
821 lane_switch=_apply_lane_selection,
824 agent.session = session
830 provider = agent.client.provider
837 attach_default_writer(session)
838 except Exception
as exc:
839 _dbg(
"OC_CHAT.MAIN", func, f
"SYSTEM_ERROR: event writer attach: {type(exc).__name__}: {exc}", 1, override=
True)
848 attach_presenter_bridge(presenter)
849 except Exception
as exc:
850 _dbg(
"OC_CHAT.MAIN", func,
851 f
"SYSTEM_ERROR: presenter bridge attach: {type(exc).__name__}: {exc}",
860 iteration_stats_sink =
None
864 args.stats_file, show=
not args.no_iteration_stats
866 except (OSError, TypeError)
as e:
867 presenter.show_error(f
"Cannot open stats file {args.stats_file}: {e}")
869 session.show_iteration_stats =
not args.no_iteration_stats
870 if iteration_stats_sink
is not None:
873 iteration_stats_sink.bind_to(session)
876 migrate_session_model(session, agent.model_manager.provider, presenter)
877 agent.model_manager.ensure(session.model)
881 model_manager=agent.model_manager,
882 tool_registry=agent.tool_registry,
885 tool_schemas=agent.tool_schemas,
886 turn_stats=turn_stats,
887 iteration_stats_sink=iteration_stats_sink,
899 refresh_presenter_bindings(ctx)
902 _gen_opts_dict = getattr(session,
"generation_options",
None)
or {}
905 ctx.generation_options = GenerationOptions(**_gen_opts_dict)
911 from pathlib
import Path
as _Path
913 _ep_registry = _EndpointRegistry(provider)
914 _ep_registry.load_env(os.environ)
915 _ep_registry.load_toml(_Path.home() /
".oc_chat" /
"endpoints.toml")
916 _saved_ep = getattr(session,
"endpoint_name",
"")
or ""
917 if _saved_ep
and _saved_ep
in _ep_registry.entries:
919 _ep_registry.switch(_saved_ep)
920 ctx.client.rebind_to(_ep_registry.active())
921 except PermissionError
as _ep_exc:
922 _dbg(
"OC_CHAT.MAIN", func, f
"saved endpoint blocked: {_ep_exc}", 1, override=
True)
923 ctx.endpoint_registry = _ep_registry
924 except Exception
as _ep_exc:
927 _dbg(
"OC_CHAT.MAIN", func, f
"SYSTEM_ERROR: endpoint registry init: {type(_ep_exc).__name__}: {_ep_exc}", 1, override=
True)
928 ctx.endpoint_registry =
None
931 agent.turn_stats = turn_stats
933 presenter.show_welcome(session)
940 _blast_banner = blast_radius_banner()
942 presenter.show_info(f
"[security] {_blast_banner}")
946 prompt_driver =
PromptDriver(ctx, turn_stats=turn_stats)
950 ctx.prompt_driver = prompt_driver
955 def _ask_user_read(text: str) -> str |
None:
957 return prompt_driver.read_user_input(
None, prompt_text=text)
958 except (EOFError, KeyboardInterrupt):
961 ask_user.bind_interaction(_ask_user_read)
963 if prompt_driver.using_prompt_toolkit:
965 "[prompt_toolkit enabled — Tab completes commands, \u2191/\u2193 cycles history, "
966 "Alt+Enter inserts newline, Ctrl+P opens command palette]"
973 presenter.show_session_start()
981 if consume_pending_sigint():
982 presenter.show_info(
"[Ctrl+C — the session change completed; nothing was interrupted]")
989 user_input = prompt_driver.read_user_input(ctx.session)
993 if user_input
is None:
997 user_input = user_input.strip()
998 if user_input.lower()
in (
"/exit",
"/quit",
"exit",
"quit"):
1003 result = dispatch(ctx, user_input)
1010 if result.kind == INTERNAL_FAILURE:
1011 if result.effect_disposition == EFFECT_UNKNOWN:
1015 _dbg(
"OC_CHAT.MAIN", func,
1016 "command failed with INDETERMINATE effects", 1, override=
True)
1018 if result.kind == HANDLED_NO_TURN:
1021 presenter.show_info(f
"[{result.warning}]")
1024 turn_input =
None if result.kind == HANDLED_TURN_QUEUED
else user_input
1026 agent.compress_if_needed()
1031 refresh_presenter_bindings(ctx)
1037 turn_session = ctx.session
1042 if consume_pending_sigint():
1043 presenter.show_info(
"[Ctrl+C — the queued turn will stop at its first checkpoint]")
1044 token = getattr(agent,
"cancellation",
None)
1045 if token
is not None:
1046 token.request_interrupt()
1050 if turn_input
is not None:
1051 agent.run_turn(turn_input)
1052 except KeyboardInterrupt:
1058 _dbg(
"OC_CHAT.MAIN", func,
"turn cancelled by user", 2, override=
True)
1059 if turn_session.messages
and turn_session.messages[-1][
"role"] ==
"user":
1060 turn_session.messages.pop()
1061 except Exception
as e:
1064 _dbg(
"OC_CHAT.MAIN", func,
1065 f
"SYSTEM_ERROR: turn: {type(e).__name__}: {e}", 1, override=
True)
1066 presenter.show_error(str(e))
1067 if turn_session.messages
and turn_session.messages[-1][
"role"] ==
"user":
1068 turn_session.messages.pop()
1072 mark_between_turns()
1082 if ctx.goal_files_changed:
1084 append_session_log(ctx)
1085 except Exception
as e:
1087 _dbg(
"OC_CHAT.MAIN", func,
1088 f
"SYSTEM_ERROR: session_log: {type(e).__name__}: {e}", 1, override=
True)
1089 presenter.show_error(f
"SESSION_LOG update failed: {e}")
1091 ctx.goal_description =
""
1092 ctx.goal_files_changed = []
1099 ctx.plan_description =
""
1100 ctx.plan_files_changed = []
1106 if iteration_stats_sink
is not None:
1107 iteration_stats_sink.close()
1109 if sys.exc_info()[0]
is None:
1111 write_clean_exit_marker(sessions_dir)
1112 except Exception
as exc:
1116 f
"SYSTEM_ERROR: clean-exit marker: {type(exc).__name__}: {exc}",
str _read_startup_input(prompt_driver, str prompt_text, str default, *, presenter)