Skip to content

emrg: a scheduled task has no owner role — it decides the direction and does the work in the same round #2467

Closed

Description

@argszero

What was observed

Each of the eight scheduled task templates in emrg/server/scheduler.py (TASK_TEMPLATES, from
evolution / paper / open-source / promote / journal / competition / ios-app /
feishu) is a single agent that both decides the direction of the round and then does the work
in that round.

There is no separate role that holds the whole picture and states the stance. Two routes exist
today and both were measured:

  1. Bake review semantics into the built-in subagent tool's schema. Rejected: subagent is
    a general tool, and giving it a fixed review meaning narrows what every other caller gets.
  2. Write "first call subagent with a brief like this" into the template prose. Measured
    execution rate across rounds: 16% → 56% → 31% — writing more detail made it worse, not
    better. And the brief is written by the parent agent, so it loses the position: it can reduce
    to review the previous rounds.

What is expected

A new tool named boss, whose caller is the worker and whose child session is the owner of
this task
.

  1. Three parameters only: focus (optional — the specific point the parent wants looked at),
    timeout_s (default the same 600 as subagent), intent (required, as on every other tool).
    The parent calls it, receives an answer, and treats that answer as this round's stance
    constraint.

  2. It reads the task itself, not a retelling. The child session's cwd is
    <parent session dir>/sub-agent/sub-boss, so the parent's directory is reachable from its own
    cwd and the child can read the parent's history.jsonl directly: the last user record is the
    whole task statement (measured on one schedule: 43,083 characters including extra_prompt),
    followed by the most recent rounds' tool calls and results. The parent supplies no part of
    the task statement.

  3. The child session persists across rounds — it is not cleared per round. The existing
    subagent slot mechanism clears a slot on selection (sub-agent/sub{slot} in
    emrg/server/daemon.py), which is wrong here: clearing per round pays to re-read the whole
    statement every round and throws away what it saw last round. Give it a fixed directory
    sub-boss that is independent of the nine LRU slots: created on the parent's first call,
    reused afterwards, removed together with sub-agent/ when the parent session is deleted. The
    depth limit stays SUBAGENT_MAX_DEPTH.

  4. Its prompt is its own template, not a string in code. Add
    emrg/server/prompts/boss.j2, rendered through the existing _get_jinja_env() so the host can
    reword it without a restart (the same convention memory_compaction.j2 follows). The template
    tells the child only: that it owns this task and the parent session is the worker, where the
    task statement is, that it should read the most recent rounds, and that it should state the
    stance and the bottom line rather than list problems. The point of the output is a position:
    how this stage should be done, what is unacceptable, and where the next step goes.

  5. One sentence on the parent side. Each of the eight template files in that directory (the
    evolution template and the seven beside it) gains the same sentence: the first step of every
    round is to call boss and treat its answer as the round's stance constraint, while anything
    it did not mention remains the parent's own judgement. Nothing else in those templates changes.
    Do not append that sentence from code — the templates are host-editable text, and an
    obligation hidden in code is invisible to the host.

  6. Reaching only scheduled tasks. Once emrg: a message cannot choose which tools the session gets — the table is fixed at daemon start #2465 lands (a message names the tools its turn may
    use, and a session gets the intersection), boss is written into the scheduled-task whitelist
    only, and a host session never sees it. If this lands separately and first, the implementer
    must build the equivalent gate itself — boss must not appear in a host session's tool table.
    The cheapest equivalent reuses the "input_history": False field the scheduler already sends
    (scheduler.py:2697): it already distinguishes "this message was not typed by the host", so
    recording it on the session answers the same question.

Cost (this must be stated in the PR description)

Keeping sub-boss across rounds means the task statement is read once: 43,083 characters is about
20k tokens, roughly ¥0.034 at an uncached price; each later round is a cache hit on its own
context plus the parent's few new rounds, roughly ¥0.001–0.003. At 40 cycles a day that is under
¥0.1 per cycle added. Clearing the child every round re-pays for the same statement every round,
which moves that by an order of magnitude — this is the reason item 3 exists.

Acceptance

Out of scope

Not changing the shape or meaning of the built-in subagent; no separate model or temperature for
boss; no "multiple owners" and no voting among them.

Activity

  1. how2how2how2-arch commented on Oct 22, 2026

    @how2how2how2-arch
    Collaborator

    Review of this proposal — anchors verified, four points to settle first

    Read against master 000a72a9, with every line this issue cites checked in the tree rather
    than taken on trust.

    The citations hold. scheduler.py:2697 does send "input_history": False; SUBAGENT_MAX_DEPTH = 9 is in the daemon; the child's directory is sub-agent/sub{slot} with the id
    sub-{parent_session_id}-{slot}; _get_jinja_env() and memory_compaction.j2 are the precedent
    point 4 relies on; TASK_TEMPLATES holds the eight entries point 5 counts. So the proposal is
    built on real code, not on an impression of it.

    1. The child should read the daily history file, not history.jsonl. A session is persisted
    twice (emrg/session.py): history.jsonl is documented as "Current conversation (compacted)"
    and is rewritten by compaction, while history_<date>.jsonl is appended and never compacted.
    "The last user record is the whole task statement" therefore holds today, and it holds for a
    reason the issue does not state: each round appends a fresh user record carrying the full
    statement, so the statement sits in the retained tail. That is a property of how the statement
    is re-sent, not of the file. A child that reads the compacted file inherits whatever compaction
    chose to keep. Point 2 should name the daily file, or better: have the daemon hand the statement
    to the child directly, which makes the read independent of compaction altogether.

    2. Persistence outside the nine slots has no lifetime and no bound. Point 3's reading of the
    existing mechanism is correct — the runner acquires a slot and calls
    Session.delete(child_id, slot_cwd) to empty it before the child is created. Nine slots bound
    anything precisely because they are reused. A fixed sub-boss outside them is unbounded by
    construction: it accumulates one round per round of the parent's life. Point 3 specifies removal
    together with the parent's sub-agent/ directory, but that is removal on delete, not on
    length. A persistent owner role needs a retention rule — what the child keeps and what it
    drops — or its cost grows linearly with the task's lifetime, which is the very cost point 3
    argues against.

    3. Requirement 5 changes the loop's own cost, and that is the host's decision. Adding "the
    first step of every round is to call boss" to all eight templates makes every round of every
    scheduled task — including the evolution template — pay for a child session before it does any
    work, against a 600 s default timeout. The measurement the issue reports (16% → 56% → 31%) is
    about prose a parent may ignore; a mandatory first step is a different regime, and a later round
    cannot choose to skip it. Deciding an installation's cost per round is a host decision, not a
    reviewer's, and the channel for it is the host's own, not a PR.

    4. Requirement 6's fallback should not be taken. #2466 implements exactly the gate this needs:
    a scheduled_task_tool_names() whitelist beside the registration, enforced at the serialised
    table, the execution-side lookup and the reflection loop, with a subagent child inheriting its
    parent's effective set. boss should wait for it rather than build the second gate on
    input_history. That flag is a per-message producer hint; recording it on the session creates a
    new per-session fact answering "was this typed by the host?" — which is the "ask inside the tool
    who called me" that this issue's own predecessor exists to remove, one layer down.

    This is a review, not a claim. Nothing here is scheduled as work.

  2. argszero commented on Oct 22, 2026

    @argszero
    OwnerAuthor

    Handled by #2470

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions