backend/app/faros/memory/research_memory.py:20-22:
def merge(self, payload: Dict[str, Any]) -> None:
self._data.update(payload)
self.state_store.save_memory(self.run_id, self._data)
The orchestrator at backend/app/faros/runtime/orchestrator.py:103 calls memory.merge(result.outputs) after every step. Because dict.update silently overwrites, later capability outputs clobber earlier ones with the same key — for example:
idea_refinement produces paperType (via the user's input passed to its inputs dict in idea_refinement.py:24).
experiment produces paperType again (line 20 of experiment.py: paper_type = inputs.get("paperType", "algorithm") — but then it's not in outputs).
paper_drafting reads paperType from inputs (line 34 of paper_drafting.py).
The keys should be namespaced per capability, especially since the orchestrator at line 73-74 does:
node_inputs = {}
node_inputs.update(memory.data)
node_inputs.update(node.inputs)
…i.e., memory leaks straight into the next node's inputs. This is convenient for "selectedCandidate" (it's the whole point of the workflow), but dangerous for anything that two adjacent capabilities both compute.
Two options
-
Namespace per node: memory.merge({f"{node.id}.{k}": v for k, v in result.outputs.items()}). Then downstream capabilities have to opt-in by explicitly referencing memory["idea.selectedCandidate"].
-
Conflict-warn merge: log a warning when merge overwrites a key that another capability set. The first option is structurally cleaner; the second is back-compat and adds an observability signal.
I'd lean toward (2) for the v1.1 release since (1) is a contract change for capabilities and the README's "Stable release baseline" claim covers FAROS metadata.
backend/app/faros/memory/research_memory.py:20-22:The orchestrator at
backend/app/faros/runtime/orchestrator.py:103callsmemory.merge(result.outputs)after every step. Becausedict.updatesilently overwrites, later capability outputs clobber earlier ones with the same key — for example:idea_refinementproducespaperType(via the user's input passed to its inputs dict inidea_refinement.py:24).experimentproducespaperTypeagain (line 20 ofexperiment.py:paper_type = inputs.get("paperType", "algorithm")— but then it's not inoutputs).paper_draftingreadspaperTypefrominputs(line 34 ofpaper_drafting.py).The keys should be namespaced per capability, especially since the orchestrator at line 73-74 does:
…i.e., memory leaks straight into the next node's inputs. This is convenient for "selectedCandidate" (it's the whole point of the workflow), but dangerous for anything that two adjacent capabilities both compute.
Two options
Namespace per node:
memory.merge({f"{node.id}.{k}": v for k, v in result.outputs.items()}). Then downstream capabilities have to opt-in by explicitly referencingmemory["idea.selectedCandidate"].Conflict-warn merge: log a warning when
mergeoverwrites a key that another capability set. The first option is structurally cleaner; the second is back-compat and adds an observability signal.I'd lean toward (2) for the v1.1 release since (1) is a contract change for capabilities and the README's "Stable release baseline" claim covers FAROS metadata.