All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
Worker isolation mode:
CONDUCTOR_WORKER_ISOLATION=threadruns every worker as a thread instead of amultiprocessing.Process(defaultprocessis unchanged —spawnremains the start method). For environments where multiprocessing's fork+exec bootstraps (spawn children,resource_tracker) fail, e.g. Firecracker microVM guests. Thread-mode tradeoffs: no per-worker force-kill (shutdown is cooperative), CPU-bound workers share the GIL, andsignal.signalbecomes a no-op off the main thread. Implementation: the Windows-only Process→Thread shim moved toconductor.client.automator.worker_isolation(the privateworker_manager._patch_conductor_use_threads_on_windowshelper is removed — the Windows gate callsapply_thread_isolation()directly) and now also swaps the logging-relayQueuefor a plainqueue.Queue -
Canonical metrics mode: opt-in harmonized metric surface via
WORKER_CANONICAL_METRICS=true-- details -
MetricsSettingsgainsclean_directoryandclean_dead_pidsfor opt-in stale.dbfile cleanup (both default toFalse) -
SchedulerClientnow carries the schedule lifecycle operations itself:pause(reason=),resume,delete,run_now,preview_next,reconcile(declarative tri-state sync) — with typed errors (ScheduleNotFound,InvalidCronExpression, ...).pause_schedulegains an optionalreason=(stored by OSS Conductor servers; ignored by Orkes servers)
- Multiprocessing start method now defaults to
spawnon all platforms (wasforkeverywhere except Windows).forkcaused silently-restartingSIGSEGVworker subprocesses on macOS (exitcode-11) and fork-with-held-lock deadlocks on POSIX. - Legacy metrics emit unchanged by default; no env var required
metrics_collector.pyis now a compatibility shim;from conductor.client.telemetry.metrics_collector import MetricsCollectorcontinues to workget_schedulereturns a typedWorkflowSchedule(orNonefor missing schedules) instead of a raw camelCase dict, matching its declared annotation and docs/SCHEDULE.md; dict-consumers should switch to attribute access orto_dict()
@worker_taskworkers are now picklable, making the decorator path work with thespawn/forkserverstart methods (fixesTypeError: cannot pickle '_thread.lock' objectandPicklingError: it's not the same object as ...; issues #264, #271):Worker.api_clientis created lazily in the worker process, runtime state (locks, pending async tasks, background loop) is excluded from pickling and rebuilt in the child, and decorated functions are pickled as importable references resolved in the childTaskHandler.start_processes()no longer hangs the interpreter when a worker fails to start (e.g., unpicklable state underspawn); it now cleans up already-started subprocesses and raises with actionable guidance- Worker processes killed by a signal now log a diagnostic hint (signal number,
PYTHONFAULTHANDLER=1guidance) instead of restarting silently - Per-schedule pause/resume now work on both Conductor server families: the client sends
PUT(the OSS Conductor dialect — the spec-generatedGETfailed there) and transparently falls back toGETon a 405 for Orkes servers Agent(model="claude-code/...")registration no longer crashes withSpawnSafetyError/PicklingErrorunder thespawnstart method (top-level or nested as a sub-agent); the tool worker is now built through the same picklable passthrough path already used by other frameworksAgentResult.tool_callsno longer drops or misnames tool calls. A tool task was identified by acall_reference-name prefix, which is the LLM provider's tool-call id format — so an Anthropic-backed agent (toolu_…) recorded no tool calls at all. Tools are now identified by the task type they compiled to, and named from_agent_tool_name(set by the server on every tool kind),method, or the task definition. Consequences:http,api,mcp,agent_tool,humanandpull_workflow_messagestools now appear at all; media and RAG tools report their own name instead ofgenerate_image/llm_index_text; and a camelCase worker name is no longer lowercased, soassert_tool_used(result, "getWeather")matchesAgentResult.eventsis now populated byrun()andrun_async()for natively-compiled agents, derived from the same execution already fetched fortool_callsrather than from an extra call. It was previously only filled in when anon_eventcallback orstream()was used, so the eightconductor.ai.agents.testingassertions that read events —assert_no_errors,assert_handoff_to,assert_events_contain,assert_max_turnsand the rest — saw an empty list. Four of them check for absence and so passed vacuously,assert_no_errorson every eval case (expect_no_errorsdefaults toTrue);expect_handoff_to, used by the eval runner's own documented example, failed every time. Framework agents (LangChain, LangGraph, OpenAI Agents, Claude Agent SDK) also derive both fields from their execution's task list now: the whole agent runs inside one passthrough task, but the Claude Agent SDK injects a task per tool call, so those tool calls appear. LangChain and LangGraph publish their steps to the event stream rather than as tasks, so that detail still reacheson_eventandstream()consumers only- Tool arguments reported on
AgentEvent.argsand inAgentResult.tool_callsnow agree, and both strip every key Conductor injects — previously_agent_tool_name,_allowed_commands,__humanTaskDefinitionand__conductor_agent_ctx__leaked into one surface or the other