Skip to content

symfonic.capabilities.memory.phases.nightly

nightly

The NIGHTLY cadence: the eleven phases quick does not run, and the factory.

PHASE_ROSTER[NIGHTLY] is quick's four plus eleven more, and it is nested rather than merely different: every quick phase is a nightly phase, so a deployment that runs both never has to ask which cadence maintains what. :func:nightly_phases returns all fifteen or raises, the same contract quick_phases holds.

What the extra eleven need that the four did not. Quick works over the recent window; most of these work over everything, because a node that must be pruned, decayed or expired is by definition one nothing has touched. Two of them -- synthetic_links and procedural_promotion -- learn from a scope's turns, which is a third read again. :mod:.evidence holds all three, each read once per cycle.

Three phases decline rather than run. pending_edges materialises edges a caller inferred and has nothing to do when no caller offered any; meta_nodes and the two learning phases need collaborators a deployment may not have composed. Declining is what skipped is for, and it is a different sentence from the roster being incomplete.

MetaNodesPhase

MetaNodesPhase(graph: Any, nodes: AllNodes, *, llm_summarise: Any = None)

Phase 4. Clusters summarised into a node that stands for them.

Source code in src/symfonic/capabilities/memory/phases/nightly.py
def __init__(self, graph: Any, nodes: AllNodes, *, llm_summarise: Any = None) -> None:
    self._graph = graph
    self._nodes = nodes
    self._summarise = llm_summarise

PendingEdgesPhase

PendingEdgesPhase(graph: Any, nodes: AllNodes, *, connections: Any = ())

Phase 2.5. Edges a caller inferred, written as real ones.

connections is the deployment's, per cycle: legacy took it as an argument to run() and did nothing when it was empty. Kept that way, so a scope with no inferred edges reports a skip rather than a zero.

Source code in src/symfonic/capabilities/memory/phases/nightly.py
def __init__(
    self, graph: Any, nodes: AllNodes, *, connections: Any = ()
) -> None:
    self._graph = graph
    self._nodes = nodes
    self._connections = list(connections or ())

ProceduralPromotionPhase

ProceduralPromotionPhase(procedural: Any, evidence: EpisodicEvidence | None, *, llm_extractor: Any = None, llm_model_name: str = '', **settings: Any)

Phase 12. Something a scope does repeatedly becomes a draft skill.

Draft, always. This is the offline learning path: it runs unattended and must never activate a skill on its own, which is why the counter it feeds is promoted and not published. A human approves.

Source code in src/symfonic/capabilities/memory/phases/nightly.py
def __init__(
    self,
    procedural: Any,
    evidence: EpisodicEvidence | None,
    *,
    llm_extractor: Any = None,
    llm_model_name: str = "",
    **settings: Any,
) -> None:
    self._procedural = procedural
    self._evidence = evidence
    self._extractor = llm_extractor
    self._model_name = llm_model_name
    self._settings = settings

SyntheticLinksPhase

SyntheticLinksPhase(graph: Any, evidence: EpisodicEvidence | None)

Phase 11. Nodes a scope's turns keep mentioning together get an edge.

Source code in src/symfonic/capabilities/memory/phases/nightly.py
def __init__(self, graph: Any, evidence: EpisodicEvidence | None) -> None:
    self._graph = graph
    self._evidence = evidence

nightly_phases

nightly_phases(*, graph: Any, store: Any = None, procedural: Any = None, pending_connections: Any = (), llm_summarise: Any = None, procedural_extractor: Any = None, procedural_model_name: str = '', stale_days: int | None = None, **quick: Any) -> tuple[ConsolidationPhase, ...]

Build the complete NIGHTLY roster, in roster order.

Parameters:

Name Type Description Default
graph Any

the GraphBackend the store reads through, or a GraphMemoryStore if the deployment has one.

required
store Any

the memory store, for the two phases that learn from a scope's turns. Without it they decline: reading episodic evidence through anything but the retrieval port would mean a second answer to "what has this scope done", and the wrong one.

None
procedural Any

where a draft skill is written. Without it phase 12 declines rather than extracting patterns it cannot store.

None
pending_connections Any

inferred edges to materialise this cycle.

()
llm_summarise Any

what names a cluster, for phase 4.

None
procedural_extractor Any

a model-backed :class:~.drafts.ProceduralExtractor. When given it runs instead of the regex extractor -- the eval that added it rejected merged streams.

None
stale_days int | None

the decay horizon, when a deployment tunes it.

None
**quick Any

forwarded to :func:~.quick.quick_phases.

{}
Source code in src/symfonic/capabilities/memory/phases/nightly.py
def nightly_phases(
    *,
    graph: Any,
    store: Any = None,
    procedural: Any = None,
    pending_connections: Any = (),
    llm_summarise: Any = None,
    procedural_extractor: Any = None,
    procedural_model_name: str = "",
    stale_days: int | None = None,
    **quick: Any,
) -> tuple[ConsolidationPhase, ...]:
    """Build the complete NIGHTLY roster, in roster order.

    Args:
        graph: the ``GraphBackend`` the store reads through, or a
            ``GraphMemoryStore`` if the deployment has one.
        store: the memory store, for the two phases that learn from a scope's
            turns. Without it they decline: reading episodic evidence through
            anything but the retrieval port would mean a second answer to
            "what has this scope done", and the wrong one.
        procedural: where a draft skill is written. Without it phase 12
            declines rather than extracting patterns it cannot store.
        pending_connections: inferred edges to materialise this cycle.
        llm_summarise: what names a cluster, for phase 4.
        procedural_extractor: a model-backed
            :class:`~.drafts.ProceduralExtractor`. When given it runs
            *instead of* the regex extractor -- the eval that added it
            rejected merged streams.
        stale_days: the decay horizon, when a deployment tunes it.
        **quick: forwarded to :func:`~.quick.quick_phases`.
    """
    resolved = phase_graph(graph)
    everything = AllNodes(resolved)
    evidence = EpisodicEvidence(store) if store is not None else None
    if procedural is not None and store is None:
        raise MemoryContractError(
            "nightly_phases was given a procedural layer and no store, so the "
            "phase that writes drafts has nowhere to read the turns it learns "
            "from. Pass the memory store, or neither."
        )

    decay = {"stale_days": stale_days} if stale_days is not None else {}
    by_name: dict[str, Any] = {
        "cooccurrence": _OverAllNodes(
            "cooccurrence", create_cooccurrence_edges, resolved, everything
        ),
        "tag_risk": _OverAllNodes("tag_risk", tag_risk_nodes, resolved, everything),
        "prune_orphans": _OverAllNodes(
            "prune_orphans", prune_orphans, resolved, everything
        ),
        "meta_nodes": MetaNodesPhase(resolved, everything, llm_summarise=llm_summarise),
        "working_ttl": _OverAllNodes(
            "working_ttl", cleanup_working_ttl, resolved, everything
        ),
        "decay_importance": _OverAllNodes(
            "decay_importance", decay_importance, resolved, everything, **decay
        ),
        "expire_semantic_durability": _OverAllNodes(
            "expire_semantic_durability",
            expire_semantic_durability,
            resolved,
            everything,
        ),
        "prune_retracted": _OverScope("prune_retracted", prune_retracted, resolved),
        "pending_edges": PendingEdgesPhase(
            resolved, everything, connections=pending_connections
        ),
        "synthetic_links": SyntheticLinksPhase(resolved, evidence),
        "procedural_promotion": ProceduralPromotionPhase(
            procedural,
            evidence,
            llm_extractor=procedural_extractor,
            llm_model_name=procedural_model_name,
        ),
    }
    # Quick's four are nightly's too, built by the factory that owns them --
    # one implementation of "what is a strengthen phase", not two.
    for phase in quick_phases(graph=resolved, **quick):
        by_name[phase.name] = phase

    from symfonic.capabilities.memory.rosters import PHASE_ROSTER
    from symfonic.capabilities.memory.schedule import ConsolidationCycle

    order = PHASE_ROSTER[ConsolidationCycle.NIGHTLY]
    return PhaseRoster((by_name[name] for name in order), resolved)