Skip to content

Latest commit

 

History

History
68 lines (66 loc) · 39.7 KB

decisions.md

File metadata and controls

68 lines (66 loc) · 39.7 KB

Decisions

One line per ruling. Each names its source, its status, and the condition under which it stops applying. A reason marked unverified is stated in the source but has no retained failure or measurement behind it; treat it as a claim, not a fact. Dates are when the ruling entered this repository (from git log), not necessarily when it was made.

Date Decision Source and reason Status Retires when
2026-10-17 Imp.Run.start/3 request_retry: [waits:, reserve:] (opt-in) sends a model request that failed with an error Imp.Errors.retryable?/1 accepts again, inside Imp.LM.request/2, after each wait; under a deadline the last wait repeats while it leaves reserve of the deadline, without one the list is the bound. Each attempt is its own :model_request/:model_response, the failed response carrying retry_in_ms. A provider's retry-after longer than the wait is waited instead. Only the outermost request in a process retries (an LM wrapping an LM would otherwise turn k retries into (k+1)² attempts). A streamed request is not sent again: its chunks may already be with the caller. A provider down for minutes fails every request a host's turn makes, and the retries Imp already makes (ReqLLM's, ReActV2's one more step) are within seconds of the failure. The wait is the run's, so a host decides it per run, and the record shows each attempt and wait. In force. The host stops needing it.
2026-10-17 A ReActV2 forced submit call whose arguments fail validation takes the path of a forced request with no submit call: the tools-disabled extractor derives the outputs (:extracted), and the invalid call and its error stay in the history. A forced submit the tool policy refuses (a denial, a policy error, an operational safety error) is not extracted: the extractor is checked by no policy, and the turn ends as before. Before, the first ended :extracted and the second :incomplete with no outputs. lib/imp/predict/react_v2.ex finish_forced_submit/9, test/react_v2_test.exs. Some models, when tool_choice names submit, write the call's arguments in their reasoning and emit no call or an empty one. Replaying one real request against one model through several providers: one provider gave no call 4 of 4 times, with the arguments in the reasoning; an empty submit({}) with finish reason tool_calls was also seen. Both are the same failure, and only one of them kept the answer. In force. Forced calls stop arriving empty, or the extractor is found to lose more answers than it recovers.
2026-10-16 ReActV2 history_replay: :actions (opt-in) shows earlier turns without the text and reasoning their steps wrote beside their calls, and without steps that only wrote text. Default :trajectory replays whole, as DSPy's ReActV2 does (its history events carry next_thought and the calls with results, and the adapter renders every event). A host whose loop does not speak for itself (its text is read by nobody) found the model rebuilding a persona from its own earlier narration on every turn. The divergence is opt-in; the record keeps every step, only the prompt changes. settled DSPy offers a comparable history view.
2026-10-16 ReActV2 finish: :tools ends a turn only through finish_on and never forces: past the one continued step after a thought, the step limit, or a second failed request (a failed request gets one more step, once per turn, not on the last allowed step), the turn ends :incomplete. Forcing a call from a loop whose ending tools act in the world would make it act without having decided to; the host knows what an unfinished turn means. settled A host needs a forced ending tool.
2026-10-16 Under finish: :submit, every thought-only ReActV2 step is followed, in every request and on replay, by one user message: "Your message above was not sent. To reply, call submit with ; to keep working, call a tool." The thought stays the model's own (assistant) message. Two exceptions: the forced submit's own last thought, when that request has nothing new to ask, is shown as the request's last user message (the row on the forced request), with no note after it; and a thought that the turn's extracted outputs directly follow was answered and gets no note. This is a sentence Imp writes in a request, where otherwise it adds nothing but a host's last_request_note. lib/imp/predict/react_v2.ex unsent_note/2, lib/imp/adapter/chat.ex unsent_note_messages/3, test/react_v2_unsent_note_test.exs. With the thought shown back as a user message under [[ ## next_thought ## ]], some models read it as an echo or a pipeline artifact, or as their own message already sent, and ended with an empty submit: 4 of 74 owed replies lost in a rehearsal. Measured 2026-10-16 on 21 real final steps, 2 samples each, across 11 model families, after a plain-text step: thought as a user message, 5 samples wrote text again; thought kept with this note, none did, replies were submitted intact (similarity about 1.0), Meta's models accepted it with tool_choice: auto, and one family once submitted a closing note as its answer; with only the note's first sentence, models probed their status and took extra steps. The note is a sentence Imp writes, which the last request does not; it states a fact about the step (only submit is sent) and the two ways on. In force. A measurement finds a wording or shape that loses fewer replies.
2026-10-16 Under finish: :submit, a ReActV2 step that writes a thought and calls no tool is followed by an ordinary step (every tool, tool_choice: "auto"; amended the same day: the thought stays the model's own message, followed by the unsent note, the row on that note), not at once by the forced submit; a second thought-only step in a row, or a step that says nothing or only whitespace, is the :empty_tool_calls interruption. max_iters ends the turn with the forced submit as before; once the deadline has passed no further request is sent and the turn is :incomplete (:deadline_exceeded). With written tool calls the request ends on the tool listing, after the thought. Signatures with submit under finish: :auto keep DSPy's ending. lib/imp/predict/react_v2.ex continue_after_thought?/3, test/react_v2_thought_continues_test.exs. Forcing at once took every other tool away from a model that had just announced its next action, and added a request whose failure lost the reply the thought held. Measured 2026-10-16 on 42 real final steps from 11 models through OpenRouter, sending that ordinary step: most samples called submit with the thought's reply, copied or paraphrased (Meta's models 6 of 6, unforced; Kimi, GPT and Gemini models likewise); a thought that was a closing note was submitted as an empty answer; a DeepSeek model went on to fetch what it had said it would read; 5 of 42 wrote text again. DSPy's ReActV2 forces submit after any step that calls no tool. In force. A measurement shows a different bound, or forcing at once, loses fewer answers.
2026-10-16 ReActV2's forced submit request, when the step before it wrote a thought and called nothing, shows that thought as the request's last message, a user message under its field's marker ([[ ## next_thought ## ]]), not as the assistant turn the history records it as. DSPy's ReActV2 leaves the thought out of the forced request. lib/imp/adapter/chat.ex thought_as_request/4, test/react_v2_finish_submit_test.exs. As a trailing assistant turn the request is refused by providers that do not take an assistant prefix: Mistral ("Cannot set an assistant prefix with function calling") and Gemini ("Requests ending with a model turn are not supported"), probed through OpenRouter 2026-10-16. Dropping it, as DSPy does, made models regenerate finished answers with reasoning off, 2 of 16 to the output limit (#336). Measured 2026-10-16 on real final steps re-sent through OpenRouter: with the thought as this user message, the submitted answer kept the plain-text reply (text similarity 0.8 to 1.0 across Kimi, GPT, DeepSeek and Muse models), a model whose plain text was a closing note submitted an empty answer as it intended, Meta's models (which accept only tool_choice: auto) copied the reply into submit 3 of 3, and Gemini accepted every request; with the thought dropped, the models wrote a different answer (similarity about 0.05), one shrank to 30 characters, and the closing-note model submitted a 170-character answer. In force. A measurement shows the thought is better dropped, or DSPy keeps it in a form providers accept.
2026-10-12 In that plain-text step (row of 2026-10-07; a program with finish: :auto, the default), the text output is not presented as a field: the system message lists no output fields, the default agent line names no output ("You are an Agent. Use the supplied tools to gather information and take actions."), there is no "The outputs to produce are:" list, and the finish line is "When the final answer is ready, write it as plain text without calling a tool." followed by the field's description, once. Amended 2026-10-15: the finish line was "... reply without calling a tool."; "reply" is a common tool name, so for a program holding a reply tool it read as an instruction about that tool, and it now uses the sentence the other tool-less text answer already used. A host's agent_line still replaces the agent line. lib/imp/adapter/chat.ex default_system/2, with_guidance/5; test/react_v2_plain_text_answer_test.exs. The answer is the whole message, so a named, typed output field described a place in the message that does not exist; models on several providers wrote the name into the message as a label (answer:, **answer**, [[ ## answer ## ]]), after notes about the turn, and the whole message, label and notes included, was the answer. The description was listed twice. DSPy has no plain-text finish to follow (row of 2026-10-07). In force. ReActV2 gives that signature a submit tool, or DSPy defines a plain-text finish whose prompt differs.
2026-10-08 A stored history turn shows the outputs it recorded and nothing for the rest: no filler, and not DSPy 3.2.1's None. A turn with no recorded output is its user message alone. A predictor that names a tool-calls output and is not a tool loop keeps its recorded outputs as the assistant text on native replay. The system message of every format leaves out a history input, since history arrives as turns: a field typed history, or one the call supplies as an Imp.History. #261, #258. lib/imp/adapter/chat.ex recorded_reply/3, system_signature/2, test/history_test.exs. A filler or None puts words in the model's mouth that it never wrote, and a [[ ## history ## ]] section in the structure is one no message in the request contains. DSPy 3.2.1 does both (ChatAdapter.format_assistant_message_content, Adapter.format). In force. DSPy stops writing missing history outputs or listing the history input, and Imp follows it.
2026-10-07 A ReActV2 step with one unconstrained text output and natively held tools, in a program with finish: :auto (the default; finish: :submit gives it submit and the marker layout of every other signature), is rendered by the Chat adapter with no output markers: the system message lays out the inputs alone (no history placeholder, since history is turns), says the message without a tool call is the answer, and shows demos and stored turns as plain text. Parsing still reads marker replies. Tool names stay listed, as DSPy lists them. Amended 2026-10-12: it lists no output field either (row of that date). lib/imp/adapter/chat.ex plain_text_answer?/2, test/react_v2_plain_text_answer_test.exs. The answer is the plain text of the last message (Imp.Predict.ReActV2, no submit), and history replays turns as plain text, so a [[ ## answer ## ]]/[[ ## completed ## ]] template gave the model a second, different way to finish. DSPy 3.3.1's ReActV2 has no such path: it always finishes through submit, and its markers frame only next_thought. In force. ReActV2 gives that signature a submit tool, or DSPy defines a plain-text finish whose prompt differs.
2026-09-28 A turn ReActV2 records with only a user side (its :last_request_note, inputs no step spent) is a step event that called nothing, and the chat adapter renders a step with no calls and no other output as its user message alone, in native and written tool modes. A stored step that recorded any other output keeps its assistant message. A History entry with inputs and no outputs keeps Imp's rendering, a finished exchange whose missing outputs read "Not supplied for this conversation history message." (DSPy 3.2.1 renders them as None; that divergence is separate.) #259. lib/imp/predict/react_v2.ex append_user_turn/2, lib/imp/adapter/chat.ex render_written_tool_history_turn/3, test/react_v2_last_request_note_test.exs, test/adapter_chat_written_history_test.exs. Recorded as inputs alone, the note rendered as a user message followed by an assistant reply the model never gave, in the request and on replay. Amended 2026-10-08: a History entry's missing outputs are left out, and an entry with none is its user message alone (row of that date). Does not retire.
2026-09-28 A model call's cost (Imp.Core.LMResponse, the :model_response event) is only a charge the provider reported, and nil when it reported none; ReqLLM's catalog price is estimated_cost, never cost. #252. lib/imp/core.ex LMResponse moduledoc, test/model_response_cost_test.exs. Hosts sum cost as money spent (Dwell's daily cap); the catalog price differs from the charge when prices change, when routing picks another endpoint, and by ReqLLM rounding each line item to a millionth of a dollar. In force. Does not retire.
2026-09-28 Imp follows semantic versioning. Before 1.0, a release that changes what a caller receives or can rely on bumps the minor version (0.5 to 0.6), and a release of fixes that change nothing a caller relies on bumps the patch. The install lines recommend {:imp, "~> 0.x.0"}, which takes later patch releases of 0.x only, so a caller never takes a breaking release unasked. A two-part requirement such as ~> 0.x means >= 0.x.0 and < 1.0.0 in Mix and takes every later 0.x release. Owner, 2026-09-28. CHANGELOG.md marks each breaking change "Breaking:" with its migration, and RELEASE_NOTES.md names them. test/package_contract_test.exs checks the install lines. In force. Amended 2026-10-08: the row said ~> 0.x stops at the next minor; it does not (#305). Does not retire.
2026-09-26 Imp.Optimizer.GEPA with no :execution_profile runs DSPy's GEPA (:gepa_v0_1_4_merge, or :gepa_v0_1_4 with use_merge: false), and its reflection records are DSPy's; Imp's own search is execution_profile: :beam_native, chosen explicitly. Owner, 2026-09-26. An upstream name promises upstream semantics; the published GEPA results (research/RESULTS.md R3, R5) used the pinned profile, as :gepa_v0_1_4; and no reason for the BEAM-native default was ever recorded. lib/imp/optimizer/gepa.ex moduledoc, test/gepa_agent_reflection_test.exs. In force. Does not retire.
2026-09-25 Prompts and parsed values use neutral spellings: types in words (string, integer, true or false, one of: a, b, list of strings, object, declared once in Imp.Adapter.FieldType), values in JSON (null, true, ["x", "y"]), in every adapter, ReAct, and the MIPROv2, GEPA and SIMBA prompts. A non-string answer for a string field is kept as its JSON text; a null is no value. Parity with DSPy means the same fields, order and constraints in the prompt, the same fields and types accepted, and the same errors raised, not DSPy's text or Python's str() spelling. The owner's intention, 2026-09-25: Python spellings (Literal[...], True, None, repr(Example)) read as foreign in an Elixir library and teach the model nothing the neutral words do not. test/adapter_type_wording_test.exs, test/upstream_exam/adapters_test.exs; the golden trace and the MIPROv2 proposer differentials compare prompts after putting DSPy's spellings into Imp's words (Imp.DSPyWording). In force. Does not retire.
2026-09-25 A tool call's value alone never reads as :refused: Imp.Tool.outcome/1 says :refused only for an Imp.MCP.CallFailure or an MCP error result that declares it in structuredContent.outcome ("refused", "auth_refused", "unknown"), and ReActV2 and RLM decide their own refusals where they refuse the call and record them as metadata.outcome. lib/imp/tool.ex outcome/1, test/mcp_call_outcome_test.exs. A tool can return any term, including one shaped like Imp's refusals, and :refused tells a caller that repeating the call is safe; reading it from a term a tool that ran can produce would retry a write that landed. The declared outcome is how a server that knows more (Kite, for a write that may have been applied) says so. In force. Does not retire.
2026-09-25 A ReAct or ReActV2 prediction's fields are the signature's outputs only; how the turn ended (history, termination_reason, termination_cause, termination_error, finished_by_tool, unexecuted_tool_calls, context_projection) is metadata. termination_reason says how the turn ended, :incomplete when it has no answer, and termination_cause is set whenever it was interrupted. lib/imp/predict/react_v2.ex moduledoc, Imp.Prediction.complete?/1, test/react_v2_test.exs. An output named history collided with the loop's own, and a reason that mixed how a turn ended with why it failed needed a private list to say whether it answered. In force. Does not retire.
2026-09-25 Every public options list in Imp.Run and Imp.ACP is declared once with NimbleOptions, its documentation is generated from the declaration, and an unknown key raises. lib/imp/run.ex, lib/imp/acp.ex, lib/imp/acp/options.ex, test/run_test.exs, test/acp_options_tool_kinds_test.exs. A misspelled :authorize ran tools unauthorised, and a misspelled ACP key reached the transport and was ignored. In force; other modules taking options are not yet all converted. Does not retire.
2026-09-25 A failed LM request from Imp.Clients.ReqLLM is one %Imp.LMError{} with status, retryable and context_window_exceeded, and ReqLLM's own error under reason; Imp.ContextWindowExceededError is gone into it. A client that raises is {:lm_failed, client, exception}, with the exception, not its message. Imp.Errors.retryable?/1 and context_window_exceeded?/1 read both. lib/imp/exceptions.ex, lib/imp/clients/req_llm.ex, test/error_shapes_test.exs. Before, provider errors reached callers as ReqLLM's structs, a raising client as text, and retryable?/1 knew only an Imp.LMError nothing built; three private copies of the context-window test each unwrapped a different set of shapes. In force. Does not retire.
2026-09-25 A completion that cannot be read as a signature's outputs is %Imp.AdapterParseError{} with a :kind (:malformed, :missing_fields, :invalid_fields, :unsupported_output, :other), from every built-in adapter and from Imp.Predict, which fills :trace and, for n > 1, :completion_index. An LM failure during the JSON fallback is returned as the LM's error. lib/imp/exceptions.ex, lib/imp/predict.ex, test/error_shapes_test.exs. Predict returned a map around a tuple, so ReActV2's :parse_error could never match and a parse failure there read as :prediction_error; a fallback's LM failure read as a parse failure. DSPy lets the fallback's LM exception through. In force. Does not retire.
2026-09-25 An error reason is a term: a raise keeps the exception struct and a throw or exit is {kind, value}, for every *_failed / *_error tag Imp builds at a public boundary (:lm_failed, :module_call_failed, :retriever_failed, :tool_error, :tool_policy_error, MCP and ACP callbacks). One tag has one shape. Imp.Redaction keeps an exception's type while redacting its fields. test/error_shapes_test.exs. A reason that is only text can be acted on only by matching it, and one tag with two shapes breaks every exhaustive match. In force. Does not retire.
2026-09-25 Imp.Example and Imp.Prediction keep each key as the atom or string it was given, and every key lookup compares by text. lib/imp/field_map.ex, test/field_key_type_test.exs. Same reason as the tool-name ruling of 2026-09-24: turning a string into an atom when one happened to be loaded made a key's type depend on what else ran in the VM. In force. Does not retire.
2026-09-24 A run event sink that raises, throws or exits is reported to the run's owner as {:imp_run_event_sink_failed, run_id, %{sequence, kind, reason}}. Imp marks no gap itself, because a sink that failed may still have stored the event; the host decides what its record says. Stopping or cancelling a run reports every event the sink had not finished with, the one in the sink as :in_sink_when_stopped and the rest as :never_handed_to_sink. lib/imp/run.ex moduledoc and Imp.Run.EventDelivery, test/run_event_sink_failure_test.exs; workshop docs/constellation-work.md W3.2. A host that stores events as the run's record lost events silently when its store call timed out, and the record read as complete. In force. Does not retire.
2026-09-24 How many runs a host may have at once is the host's setting: Imp.Run.start/3 with admission: {pool, limit} counts the run in the host's named pool, not the machine-wide :async_max_workers pool, and a full pool returns {:error, :busy} at once rather than waiting. Without it a run waits for the machine-wide pool as before. lib/imp/run.ex start/3, Imp.Tasks.Admission, test/run_admission_test.exs; workshop docs/constellation-work.md W3.3. One machine-wide 8 with an unbounded wait queued every agent's runs behind each other's. A wait inside start/3 blocks the caller's mailbox and hides the queue where the host cannot order it; a host that owns its runs knows when one ends and can start the next. In force. Does not retire.
2026-09-24 An MCP tool call that got no answer from its tool returns %Imp.MCP.CallFailure{} with an outcome of :refused, :not_sent or :unknown, decided in Imp where ExMCP's error arrives, with ExMCP's error kept unchanged in reason. A timeout is always :unknown, and so is JSON-RPC invalid params (-32602). Imp.Tool.outcome/1 reads any tool call, and ReActV2 and RLM record it on :tool_result. lib/imp/mcp/call_failure.ex, test/mcp_call_outcome_test.exs. A host that has to match ExMCP's terms to decide a retry re-derives the rule and misses cases; ExMCP's client sends one HTTP request at a time from its own process, so a call that timed out waiting is still sent later (true of ExMCP 1.5; see Status); and ExMCP's server sends a tool handler's returned ProtocolError (invalid params among them) as the JSON-RPC error after the handler ran (method_handlers.ex handle_tool_reply/3; test/mcp_call_outcome_test.exs shows the handler ran). In force. Amended 2026-10-25: since ExMCP 1.6.0 the client drops a request whose caller's deadline passed while it waited, so a call that timed out waiting is not sent later (test/mcp_call_outcome_test.exs). The timeout stays :unknown: its caller gets the same :timeout whether the request waited or went out and is still running. Does not retire. The text prefixes in CallFailure retire when ExMCP keeps a failed send's reason as a term.
2026-09-24 Under on_failure: :drop, an "sse" descriptor refused for carrying "headers" or "auth" (:mcp_sse_credentials_refused), or for a URL with a query string (:mcp_sse_url_refused), is left out with a warning, as an unreachable server is, rather than refusing the import; under the default :refuse it still refuses the import. Owner ruling, 2026-09-24. lib/imp/mcp/connections.ex connect_all/5, test/mcp_legacy_sse_test.exs "under on_failure: :drop leave out only that server". An ACP client such as Zed that offers one credentialed sse server must not lose every other server of its session. In force. Does not retire.
2026-09-16 Imp.MCP.connect/2 drops a server it cannot connect only when the caller asks for it (on_failure: :drop), and even then only for a failed dial and a failed tools/list: an unauthorized descriptor, a declared auth that cannot produce a header, a malformed descriptor and anything the caller's :tool_filter raises still refuse the whole import. An absence carries the index of its descriptor, because a name is not an identity. lib/imp/mcp/connections.ex moduledoc, test/mcp_connection_test.exs. A caller that needs every tool must not silently get a smaller catalog; the refusals are answers the caller gave before anything was dialed, or the caller's own code failing; and a caller told only a name cannot tell which of two descriptors under it was left out. In force. Does not retire.
2026-09-16 Each MCP dial is bounded by :timeout on its own, and a dial abandoned at that deadline is killed together with the half-open client it is linked to. lib/imp/mcp/connections.ex dial/2 and abandon/1, test/mcp_connection_test.exs. A host that accepts the connection and answers nothing returns within neither :handshake_timeout nor :era_probe_timeout, so one budget for the whole list refused imports that :drop exists to let through; and ExMCP.Client traps exits and ignores the EXIT from the process that started it, so a killed dial left a live client holding a socket. In force. Amended 2026-10-25: ExMCP 1.6.0 bounds the probe by :era_probe_timeout and initialize by :handshake_timeout, and falls back from a probe refused with HTTP 400, 404 or 405, or one that timed out, to initialize within the same dial, so a silent host would hold a dial for both; the probe and initialize share the dial's :timeout. ExMCP bounds its own handshake and closes a client whose starter dies.
2026-08-23 Imp is not published to Hex; each release is a private immutable Git tag (v0.3.x) installed with {:imp, github: "deepfates/imp", tag: ...}. README.md Install. mix hex.build refused the Git ExMCP dependency. Retired 2026-09-23 by the row below. Retired.
2026-09-13 An ACP tool kind is derived from the MCP ToolAnnotations the server declares, not from a host table keyed by tool name; a tool that declares no hint gets nil rather than a guess, and an explicit :tool_kinds entry still wins. lib/imp/acp/tool_kind.ex moduledoc, test/acp_options_tool_kinds_test.exs. A name table goes stale the moment a server publishes a tool it does not name, and that is exactly when a permission mode that would have asked does not ask. In force. Does not retire.
2026-09-13 On session/load and session/resume the session is installed with the _meta it was created with, not the _meta the request carried; both are passed to the program factory as :meta and :requested_meta. lib/imp/acp.ex moduledoc, test/acp_imp_acp_test.exs. A session's history and transcript belong to the configuration that produced them, so a resume that silently adopts a different one replays one configuration's transcript as another's; only the factory knows which of its own _meta keys are identity-bearing, so it is given both and decides whether to refuse. In force. Does not retire.
2026-09-13 Imp.ACP (the ACP session/program adapter) is part of Imp; the separate imp_acp package is retired with no compatibility shim and no second implementation. Retired imp_acp README, AGENTS.md. One protocol stack, one owner of the ExMCP dependency. In force, and released in v0.4.0. The "unreleased" caveat retired at v0.4.0. The absorption itself does not retire.
2026-09-13 Absorption renames: IMP_ACP_PATH becomes IMP_PATH (copied workspace example), IMP_ACP_ROOT becomes IMP_ROOT (host application preset), launcher is examples/workspace_agent/scripts/workspace-agent-acp. Module names Imp.ACP, Imp.ACP.Host and the deepfates.com/imp-acp wire metadata namespace keep their names. Retired imp_acp README. Saved executable and environment references are updated explicitly, not by rewriting contact history. Amended 2026-09-25: Imp.ACP.MCP is gone before 0.5.0, the first Hex release; it copied Imp.MCP.Import to add one field, which Imp.ACP.ToolKind.derive_all(import.annotations) computes. Does not retire.
2026-09-13 The workspace agent's session store keeps the imp_acp/workspace_agent/sessions directory name. examples/workspace_agent/README.md. Preserves saved sessions; it is a storage location, not a dependency on the retired package. In force. Saved sessions are migrated, or the owner accepts losing restart continuity for them.
2026-09-13 Ordinary Imp boot starts no protocol listener, subprocess or remote connection and does not start the ExMCP application. Protocol entry points (Imp.ACP.*, non-empty Imp.MCP.connect/2) start it explicitly. Releases using them declare applications: [ex_mcp: :load, erlexec: :load]; erlexec starts on the first stdio MCP connection. mix.exs dependency comment, docs/production.md "Protocol runtime in releases", commit 728f8c77. Prediction and optimizer processes must not open listeners or acquire protocol boot output. In force. Does not retire while Imp is a library inside a host application.
2026-09-23 Imp is a Hex package, installed with {:imp, "~> 0.5"}, and every dependency is a Hex package. ExMCP is ~> 1.5, unpatched. What Imp needed from the deepfates/ex_mcp fork lives in Imp, each piece with its reason and the condition that retires it beside it: Imp.MCP.OwnedStdio (server process groups, the child PATH), Imp.MCP.Trust (trust for authorized remote origins), the HTTP client options in Imp.MCP.Connections, and Imp.MCP.OAuth.Flow. Publishing a version is the owner's action. Owner ruling, 2026-09-24: ExMCP from Hex, with what can live in our code moved there rather than carried as patches. mix hex.build and mix package.check build and consume the package; test/mcp_stdio_lifecycle_test.exs, test/mcp_stdio_environment_test.exs, test/mcp_trust_test.exs, test/mcp_http_public_server_test.exs and test/mcp_oauth_test.exs fail without each piece. In force; 0.5.0 is published on Hex. Does not retire.
2026-09-24 An Imp.Tool keeps the type of the name it was given, and a tool imported from an MCP server is named by the server's string. No string is turned into an atom. lib/imp/tool.ex new/4, test/tool_schema_runtime_test.exs. Converting a string to an atom when one happened to be loaded made a name's type depend on what else ran in the VM, so the same server's tools came back as a mix of atoms and strings; lookups and tool policies already compare names by text. In force. Does not retire.
2026-09-13 mix.exs resolves ExMCP three ways: a bundled vendor/ex_mcp if present, else EX_MCP_PATH, else the GitHub pin. mix.exs. No reason was recorded beside the conditional. Retired 2026-09-23: ExMCP comes from Hex, one way. Retired.
2026-09-13 Known interoperability limit, not a feature: when an HTTP MCP server selects protocol version 2025-03-26, ExMCP's notifications/initialized can still carry the client's 2025-11-25 header, and a strict older server may reject the session. Compatibility with strict older HTTP servers is not claimed. docs/production.md, commit 902a5546. ExMCP 1.5.0's connection manager sends that notification before it settles the transport's version (establish_legacy_protocol/4). Open defect, documented. ExMCP settles the HTTP version before sending that notification and a test against a strict older server passes.
2026-09-13 Imported MCP tool calls disable generic transport retries, use ExMCP's :safe_only broken-stream policy, and never repeat an ambiguous write. The retired Imp-specific retry, backoff and Retry-After options are rejected, not ignored, and are not reimplemented above ExMCP. docs/production.md. A timeout does not say whether the call ran; replay needs a real server idempotency contract, which is the application's to establish. In force. Does not retire.
2026-09-13 Tool errors return {:error, {:mcp_tool_error, envelope}} with the original content, codes and operation identifiers, so refusal, authorization refusal and indeterminate effect stay distinguishable. The old text-only error tuple is gone. docs/production.md. A boundary declares its failure classes. In force. Does not retire.
2026-09-13 Imp.MCP.connect/2 imports only explicitly authorized server descriptors; host-supplied command, URL, headers, environment and working directory are untrusted input. The workspace agent denies host-supplied MCP servers unless WORKSPACE_AGENT_MCP_AUTHORITY=host. docs/production.md, examples/workspace_agent/README.md. In force. Does not retire.
2026-09-09 Imp.ExternalCommand keeps its own process-group lifecycle (TERM, grace period, KILL, confirm gone) on raw ports. AGENTS.md "Process ownership". The recorded reason was that the fork's OwnedProcess stopped the root before signalling, so a child could never handle TERM; a trainer killed mid-checkpoint loses work (unverified: stated, no retained incident). That reason no longer applies to Imp's own code: Imp.MCP.OwnedStdio runs MCP servers through erlexec, which sends the group TERM and waits its kill_timeout before KILL. Documented divergence. Moving ExternalCommand onto erlexec with its grace period is a candidate, not done. ExternalCommand runs on erlexec with its grace period kept, or a reason to keep raw ports is recorded here.
2026-09-09 Behaviour boundaries that return bare {:error, term()} or error strings are half-declared. New failure classification extends the classes that exist (Imp.LMError's retryable, status and context_window_exceeded, Imp.AdapterParseError's and Imp.OperationalSafetyError's :kind, Imp.MCP.CallFailure's outcome) rather than adding a new scheme. AGENTS.md "Where the boundaries are half-declared". A caller that must match error text to decide a retry or a spend will retry something that already happened. Direction, not finished; which behaviours still return bare strings is not inventoried (unverified). Every @callback whose failure feeds a retry or spend decision names its failure classes.
2026-09-13 (runs dated 2026-08 in the retired imp_acp repository) For the workspace agent, persistent RLM is the default local-model program; ReActV2 is the explicit restart-durable option (WORKSPACE_AGENT_PROGRAM=react). Former exercise notes in examples/workspace_agent/README.md (deleted 2026-09-15): the local Qwen model through Toad 0.6.20 did not reliably honor ReActV2's declared JSON object arguments for commands; the retained failure ended in refusal before any effect. In force. A local model honors ReActV2 tool arguments reliably enough that restart continuity can be the default too.
2026-07-20 Imp.Adapter.Chat.parse/3 fails loudly when the completion's [[ ## field ## ]] sections do not cover every output field: no single-output leniency, no in-parse JSON decode. lib/imp/adapter/chat.ex do_parse/2, test/silent_failure_regressions_test.exs. DSPy __call__ answers a failed ChatAdapter parse with a second call through the JSON adapter; a lenient parse returns the whole completion as the answer and that fallback never fires. In force. Does not retire.
2026-07-18 mix check is the merge signal. Provider-backed (:live), integration, protocol, package, differential and evidence-infrastructure runs are separate gates because they need credentials, datasets, external services or spend. A green default run is not evidence for a fidelity claim. .github/workflows/ci.yml header (commit dc9d95f7), restated in AGENTS.md "Checks" and CONTRIBUTING.md (2026-08-22). In force. Does not retire.
2026-08-22 Research campaigns and evidence receipts are not release gates. .github/workflows/ci.yml header. In force. Does not retire.
2026-08-07 The pinned-DSPy differential lane (mix differential.check) runs per PR with known-red residue kept visible: the sealed v1 contract detects mix.lock drift after the bandit CVE bump, and the MuSiQue receipts read a machine-local /tmp data directory. .github/workflows/ci.yml differential job comment. Visible red beats hidden red. Retired 2026-09-17: re-run on a clean tracked tree, neither residue is red any more. Retired.
2026-07-17 CI dependency cache: exact mix.lock key with no prefix fallback; package.check is the only writer; every job warms ~/.hex to the full lock closure. .github/actions/elixir-setup/action.yml. Four cache-poisoning incidents from partial ancestor caches saved under new keys. In force. package.check's offline clean-room no longer needs a complete ~/.hex, or actions/cache starts re-saving on exact hits.
2026-07-17 Evidence-infrastructure tests (:evidence_infrastructure) are excluded from the default mix test. CONTRIBUTING.md. They need full git history, pinned DSPy Python environments and sometimes provider credentials, none of which a fresh clone has. In force. Does not retire.
2026-07-10 ReqLLM is the provider transport boundary; upstream algorithm names keep upstream semantics; deliberate Elixir-native alternatives get a distinct contract and rationale; fixtures and symbol presence never become parity claims. CONTRIBUTING.md "Design Standard". In force. Does not retire.
2026-07-07 mix aliases (check, package.check, ...) exist only in a source checkout (detected by test/package_contract_test.exs being present); the shipped package carries runtime sources only. mix.exs aliases/0 and its comment (detection entered in commit c193433f). Local benchmark and evidence control files stay out of the consumer's dependency tree. In force. Does not retire.
2026-08-22 No ticket file, log or diary belongs in this repository; unfinished work is a pull request on a topic branch. CONTRIBUTING.md "Unfinished work is a pull request on a topic branch; there is no ticket file in this repository." An earlier .tickets/ directory and the CONTRIBUTING.md sentence that justified it were both removed. Resolved: git ls-files .tickets is empty. The owner reopens it.
2026-09-11 No OAuth for now, neither MCP's nor ATProto's. Owner ruling, 2026-09-11. Imp carries Imp.ACP.DemoMCPOAuthPlug and a demo MCP HTTP server mix task used only by its own tests to exercise ExMCP's PKCE client; ordinary use has no OAuth path. In force. A multi-tenant reason appears; that is a host application's question, not Imp's.
2026-09-11 Outside contributions are welcome through GitHub issues and pull requests on this repository. Imp carries no ExMCP patches; a change ExMCP needs goes upstream to azmaveth/ex_mcp only when it cannot be made from Imp, and only after the owner has seen it. Owner ruling, 2026-09-11, amended 2026-09-17 when the repository went public and 2026-09-24 when ExMCP moved to Hex. In force. The owner says otherwise.
2026-09-11 Tests falsify: revert the change and watch the named test fail before believing it. The owner's machine is a consumer, not a privileged insider: whatever a stranger must do to run this, the owner does the same. Owner ruling, 2026-09-11. The stranger.check CI job in .github/workflows/ci.yml runs deps.get and mix check from a cold checkout for the same reason. In force. Does not retire.
2026-09-11 Work is a pull request on a topic branch; working notes go in the pull request or nowhere. Adopting a new path deletes the old one in the same change. Owner ruling, 2026-09-11; CONTRIBUTING.md "Unfinished work is a pull request on a topic branch". In force. Does not retire.