Skip to content

chore: sync current Daimon v0.1.2 state - #1

Merged
apresmoi merged 56 commits into
mainfrom
agent/publish-current-state
Aug 13, 2026
Merged

chore: sync current Daimon v0.1.2 state#1
apresmoi merged 56 commits into
mainfrom
agent/publish-current-state

Conversation

@apresmoi

Copy link
Copy Markdown
Collaborator

What changed

  • Synchronizes the complete local Daimon history onto a reviewable branch.
  • Advances the v0.1.2 per-agent runtime, Pi integration, world authority, causal turn evidence, memory integration, and wake acceptance.
  • Retains authenticated teammate message text alongside world-capable wake context while excluding private transport bytes.

Why

The GitHub default branch is behind the runtime used by the current Spawnfile and Simfile integration. This draft publishes the current Daimon runtime contract for coordinated review.

Impact

  • World-capable message wakes now preserve useful authenticated team communication in the model-visible prompt.
  • Organizational ownership remains outside Daimon.
  • Package version remains 0.1.2.

Validation

  • npm run typecheck — pass
  • npm test — 156/156 pass
  • npm run build — pass

Review notes

  • Full tests require a stable built sibling Mneme package; they pass when dependency builds are not deleted concurrently.
  • The historical diff contains one pre-existing blank-line-at-EOF warning.

apresmoi added 30 commits July 9, 2026 19:19
# Conflicts:
#	src/pi/piAgentHandle.ts
#	src/pi/piHarness.ts
Commit 02d4a1d made the Pi harness memory runtime opt-in and updated
every other memory-dependent test and call site, but missed this file.
Without `memory` the adapter hands Pi an empty `customTools`, so
`memory_search` did not exist and, worse, the honesty assertion above it
was passing against an empty memory runtime — it could pass having
examined nothing. Restoring the opt-in re-arms the guard.
…olling it

daimon imported mneme's tool descriptors for names and prompts, then
hand-rolled the argument contract twice: a TypeBox schema duplicating
mneme's exported zod schema, and a field allowlist duplicating mneme's
kernel validator. Both still carried evidence_event_ids, which mneme
removed when it started deriving provenance from the authenticated
envelope.

So daimon advertised to the model a field mneme rejects, accepted it
locally, and mneme returned malformed() — a result, not a throw — which
means every memory.register silently wrote nothing. Two tests failed for
want of bank rows and a recallable prior turn; the cause was that daimon's
memory has been writing nothing since 2026-07-29.

The allowlist and tool-name set are now derived from mneme, and a new
contract test polices daimon's advertised schema against it. schemaFor
throws on an unknown tool instead of silently returning the forget
schema. The derivation is representation-independent, because daimon's
peer range legally admits an older mneme whose exports were plain maps.
apresmoi added 26 commits August 3, 2026 21:25
…bounds

Widen the session seam from Pi's concrete AgentSession to a three-method
PiSessionLike so a CLI engine can supply a session through the existing
sessionFactory option, while runWake keeps minting the wake id and
stamping turn.input.submitted/turn.output.completed unchanged. Mount the
existing createPiWorldTools/createPiMemoryTools ToolDefinitions on an MCP
server that advertises each tool's own parameters verbatim and validates
against that same object, bounded per wake by maxToolTurns and a deadline.
Raw Pi training capture is now unrepresentable alongside a supplied
session rather than merely discouraged.
…the tool call

The deadline was checked only at entry, so a call already running when it
elapsed ran to completion and returned success: a 300ms tool under a 100ms
deadline returned text "done" with no abort observed. Pass the tool a
deadline-driven signal combined with the MCP request signal, so worldTools'
existing callerSignal handling cancels the in-flight request, and race the
call against the deadline so an overrun reports McpWakeDeadlineError.

Replace Reflect.apply, which type-checked none of the five positional
arguments to the one call the whole mount exists to make, with a direct
tool.execute call and a single narrowing at the Ajv validation boundary.
Swapping the signal and onUpdate arguments is now a compile error.
daimon's production code imports @noopolis/mneme, so a devDependency was
already a boundary leak, but round 2's file:../mneme made daimon's runtime
install require a sibling checkout at a relative path, which goal.md
guardrail 1 forbids by name. Declare the real requirement instead.

^0.1.1 is deliberately not satisfiable from the registry today: only 0.1.0
is published, and 0.1.1 is the version carrying the schemaForModelToolName
fix. That is the honest state - daimon is not independently installable
until mneme 0.1.1 is published, and this specifier says so loudly instead
of resolving to a mneme without the fix.

package-lock.json is deliberately left at its linked entry
(resolved ../mneme, link true), which is true of this checkout. npm
regenerates it as a registry tarball URL for 0.1.1 that returns HTTP 404;
that artifact does not exist and is not committed.
…ce as the operator

A missing NOOPOLIS_RUN_ID resolved to the placeholder "unset-run", so
multi-agent evidence could share one namespace and be contaminated before
it was collected, with no repair available after the fact. Remove the
fallback entirely; a blank or missing value now throws naming the variable.

formatWakePrompt rendered an unattributed wake as "from: operator", the
highest-trust principal, so any participant able to deliver an
unattributed wake had its message presented with the operator's authority.
Render absence as absence. An explicit operator or agent attribution still
renders unchanged.
One CLI session adapter satisfies PiSessionLike and is supplied through the
existing sessionFactory seam, so runWake keeps minting the wake id and
stamping turn.input.submitted/turn.output.completed on the CLI path exactly
as on Pi's. Each wake starts an ephemeral loopback MCP server over the tool
objects piHarness already built, injects it per invocation (codex via -c,
grok via project-scoped config), and tears it down on success, failure and
deadline abort. The bearer is stripped from the child environment and never
reaches argv, a config file, or an error message.

agy has no MCP client, so it is a typed variant requiring an explicit
toolAccess: "none" rather than a faked MCP path or a silent omission.

A failing engine now surfaces bounded, redacted stderr instead of only
"CLI engine exited 1", which hid the cause of the one failure that mattered.

Deletes mixedEngineCli.ts and mixed-engine-org.ts; the engine-spawn
knowledge lives in the adapter and no second path survives.
…ools

The CLI-over-MCP adapter had never been run against a live engine, and it
could not have worked. Two defects, both invisible to the stub.

First, the codex branch of spawnEngine took a prompt parameter and never used
it: it passed `-` to read from stdin, opened the pipe, and never wrote or ended
it. Codex blocked on empty stdin until the timeout. Measured against
codex-cli 0.146.0: the current argv produced zero bytes of stdout and stderr in
20s, while the same prompt piped in returned normally.

Second, the argv omitted the sandbox setting that this invocation's other copy
in the spawnfile compiler has always passed, so codex auto-cancelled every MCP
tool call the adapter mounted. Same tool, same server, one flag apart:

  bare argv       -> mcp: daimon/live_lookup (failed)
                     user cancelled MCP tool call, toolInvoked=false
  --sandbox flag  -> mcp: daimon/live_lookup (completed)
                     PINEAPPLE, toolInvoked=true

PINEAPPLE is only obtainable by executing the tool, so the positive is not a
model guess. The stub could catch neither defect: it never reads stdin and it
is not codex, so it approves nothing.

Also guards the stdin write against EPIPE, so a child that dies before reading
surfaces through the existing exit diagnostic instead of crashing the process.

scripts/liveCodexSession.mjs drives the real binary through
createCliSessionFactory with no command override. It is not in `npm test`
because it needs network and real auth:

  mounted tools: live_lookup
  final text: PINEAPPLE
  tool invoked: true

daimon 142 pass / 0 fail / 0 skipped, verified outside the sandbox. Inside a
sandbox the MCP test reports as skipped, which is how both defects shipped
green before.
…bsence

formatWakePrompt interpolated `event.from` verbatim, so a participant naming
itself the absence sentinel produced a prompt BYTE-IDENTICAL to genuine
absence, and a newline in the value injected extra header lines into the
trusted region above the message body. Measured against a real Moltnet node:
a message posted with from.name "[no attribution supplied] (absence)" rendered
exactly as an unattributed wake, and from.name "blue\n- kind: operator.command"
added two forged header lines.

A supplied value is now JSON-encoded, so it is quoted (distinct from the bare
absence marker) and its newlines are escaped (the header stays four lines).
Absence is unforgeable by construction rather than by blocklist.

The pre-existing guard could not fire: it compared against the near-miss string
"[no attribution supplied]" rather than the exact sentinel, so it passed against
the vulnerable code. Verified by reconstructing both and watching it go green.
Assertions updated to the quoted format, which is a deliberate format change.

daimon 144/144, 0 skipped (run outside the sandbox; inside it, one loopback
test skips).
…uite

node_modules/@noopolis/mneme is a symlink to the sibling checkout and resolves to
ecosystem/mneme/dist/index.js. Ten suites load mneme VALUES — the memory kernel,
the stores, the tool descriptors — and daimon's test script never builds it. A
five-day-stale mneme/dist hid two real daimon failures behind a green suite: the
honest verdict was 119/121 and the main checkout reported 121/121.

Compares the newest source against the OLDEST emitted output so a partial rebuild
cannot launder the rest, and reports the file counts it actually compared. A
package with no src/ beside its dist/ is an ordinary published package and passes,
so this suite still passes from a clean clone.

Proven both directions: with ecosystem/mneme/src/index.ts touched the full suite is
147 pass / 1 fail; after `node scripts/build-closure.mjs --for daimon` it is
148 pass / 0 fail / 0 skipped.
The world trajectory exonerates the agent completely: it woke, called
world_observe with the exact sense URI from its character card, failed in 5ms,
called world_affordances to ask what it was permitted to do, failed in 6ms,
reported the problem in prose and stopped. terminal_status completed, eight
seconds of real model time on gpt-5.4-mini. The mind was never the problem.

But every failed call recorded `result: {}`. On an error event the payload is
not under `details`, so `details ?? result` collapsed to nothing -- while
`status` was being computed from `record.isError` on the same line. The
recorder could already see it was an error and still kept nothing about it.

Failed calls now retain the full result, which carries the Pi error shape
{ content: [{ type: "text", text }], details: {} }, through the existing
redaction and bounds rather than a second hand-rolled path. The success path
is unchanged and pinned by a test so this stays additive.

That is the ninth layer of this class repaired in this item, and the last
silent one. 5 and 6 milliseconds is far too fast for a round trip, and both
remaining candidates were eliminated in the same probe: world resolves to
172.27.0.3 and the endpoint answers HTTP, not 401. The tools fail locally,
before any request leaves the container. The next run should name the cause.

daimon 144 pass / 0 fail / 0 skipped, measured outside the sandbox. A single
failure of "MCP deadline aborts an in-flight tool" appeared once under full
suite load; it passes 9/9 in isolation three times and the full suite passes
144/144 twice, so it is a load-induced flake in a timing-sensitive deadline
test, confirmed by repetition rather than assumed. Reverting this change by
hand fails two assertions; restoring passes 5/5.

Reported as "1 skipped" from inside a sandbox that denies loopback binds. Run
outside it, that test executes -- the second time in this program a skip has
concealed a real result.
A deployed organization ran published @noopolis/daimon@0.1.2 and
@noopolis/mneme@0.1.1, never this worktree, so no daimon or mneme change could
be tested live. runtimes.yaml pins daimon to install.kind: container_image and
container.ts returns a COPY of that prebuilt image with commands: [] and no
npm, so runtimePackageOverrides -- which only applies to the npm branch --
could not have changed a single byte. The lever that does work already existed
and nothing set it: SPAWNFILE_DAIMON_RUNTIME_IMAGE.

What was missing was a way to build that image from source. Dockerfile.runtime
gains an additive local target that installs Daimon and Mneme from tarballs
staged into a minimal build context; the registry path stays the Dockerfile's
default target and is unchanged. The build context is assembled in os.tmpdir()
and holds only the two tarballs and the Dockerfile, because the docker context
here is an SSH context to a remote host and the whole context is uploaded.

The acceptance is that the image contains OUR code, not 0.1.2, checked with two
discriminators that a published build fails:

  world-trajectory failed-result capture: PASS (local discriminator present)
  mneme causal.js unset-run: PASS (absent)

The second also settles the risk that npm would resolve daimon's
@noopolis/mneme@^0.1.1 dependency from the registry despite the local tarball.
It does not.

Verified end to end: after a live run with SPAWNFILE_DAIMON_RUNTIME_IMAGE set,
the organization image itself carries the local packages -- unset-run count 0
where published 0.1.1 has 1, and the error-capture discriminator present.

This satisfies the standing rule rather than bending it: nothing is published,
and a deployed organization can now be run against a local build before any
publish decision is made.

daimon 144 pass / 0 fail / 0 skipped outside the sandbox. The Docker build and
in-image acceptance were run by the handler; the worker correctly reported it
had no Docker and did not simulate them.
@apresmoi
apresmoi marked this pull request as ready for review August 13, 2026 20:11
@apresmoi
apresmoi merged commit 52cc09d into main Aug 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant