Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
024e102
feat: give each Grok worker a private TMPDIR under its registered home
apresmoi Sep 17, 2026
aa1aef0
feat: refuse Grok turns unless shared temp is closed to workers and t…
apresmoi Sep 17, 2026
faae4cd
build: rebuild native engine broker artifacts with the worker TMPDIR
apresmoi Sep 17, 2026
8d24c50
fix: write tool-output spills group-readable so the agent's own Grok …
apresmoi Sep 17, 2026
45d9e41
docs: document the Grok worker temp and spill provisioning contract
apresmoi Sep 17, 2026
7439edb
fix: attest every registered Grok worker's private temp and close the…
apresmoi Sep 17, 2026
a2ba04b
fix: pin the tool-output spill directory and publish spills by rename…
apresmoi Sep 17, 2026
4aef65c
fix: refuse non-canonical Grok worker registration paths and truncate…
apresmoi Sep 17, 2026
4c761c8
build: rebuild native engine broker artifacts with canonical registra…
apresmoi Sep 17, 2026
4e874ad
docs: record sibling temp attestation, spill directory pinning and ca…
apresmoi Sep 17, 2026
92fb1ca
fix: refuse Grok worker deny paths bubblewrap cannot materialize as t…
apresmoi Sep 17, 2026
8c7e309
fix: let the Grok broker projection mask the wake-acceptance store th…
apresmoi Sep 17, 2026
089c3f8
fix: accept the contracted traverse-only runtime home for brokered Gr…
apresmoi Sep 17, 2026
277ea26
fix: create every runtime-home directory private so a traversable Gro…
apresmoi Sep 17, 2026
bcd2f8e
docs: state the traverse-only Grok runtime home rule and the private …
apresmoi Sep 17, 2026
d485784
fix: name the mounted tools in the agent identity envelope
apresmoi Sep 17, 2026
0c16fc5
build: stage the packaged Linux engine broker on every packing host
apresmoi Sep 17, 2026
1af01fd
fix: refuse broker proxy policy misses non-retryably with a named reason
apresmoi Sep 17, 2026
7b9a92c
fix: keep the Grok session-title sink on its transient refusal shape
apresmoi Sep 17, 2026
1a6ee5d
test: keep the grant-path title sink refusal transient
apresmoi Sep 17, 2026
b54f66f
fix: lead an inbox turn with each delivery's own text and keep the ac…
apresmoi Sep 17, 2026
8496d44
fix: name a failed brokered worker's own reason instead of its exit code
apresmoi Sep 17, 2026
bac2d12
fix: stop naming a healthy turn's own two proxy requests as refusals
apresmoi Sep 17, 2026
081b3ce
test: pin the no-active-turn refusal to its non-retryable shape
apresmoi Sep 17, 2026
37363c3
fix: state the Grok use_tool prefix rule once, from the worker contra…
apresmoi Sep 17, 2026
cb7a745
fix: name the prefixed Grok tool form as the only valid one and add n…
apresmoi Sep 17, 2026
1c74e71
feat: record each brokered request's tool-call names in the per-reque…
apresmoi Sep 17, 2026
d8a67dc
test: split the identity-envelope tests into their own file under the…
apresmoi Sep 17, 2026
964dda3
fix: prefix the inbox prompt's oversized-payload branch for Grok too
apresmoi Sep 17, 2026
23b28e9
fix: forward the MCP session and protocol headers and the GET/DELETE …
apresmoi Sep 17, 2026
8d5c26c
docs: record the broker MCP facade's closed header allowlist and supp…
apresmoi Sep 17, 2026
a43d62c
fix: name the transport send tool the way Grok can call it
apresmoi Sep 17, 2026
9d773ae
test: share one facade across the broker MCP facade tests and cover r…
apresmoi Sep 17, 2026
73cc6b6
fix: name the underlying fault beside the proxy's broker_unavailable …
apresmoi Sep 17, 2026
16c1224
fix: name one level of a broker fault's own cause so a failed provide…
apresmoi Sep 17, 2026
f3d4b2b
fix: escape the flatten ranges and the test's control byte so no sour…
apresmoi Sep 17, 2026
6a4fa63
fix: wake the MCP facade's backpressure await on a hang-up or abort s…
apresmoi Sep 17, 2026
64a30bb
fix: refuse a fenced credential realm as a named non-retryable auth_s…
apresmoi Sep 17, 2026
8ac45f4
docs: keep the session-title sink's own paragraph intact in the runti…
apresmoi Sep 17, 2026
a509277
fix: let a usage decoder fault fall through to the estimate instead o…
apresmoi Sep 17, 2026
20ae57b
fix: decode a failed worker's last words as text and keep both ends o…
apresmoi Sep 17, 2026
3f063c6
fix: keep both ends of the launcher's diagnostic window and scrub cre…
apresmoi Sep 17, 2026
389d7a5
fix: give the brokered worker a blocking stdout pipe so a large write…
apresmoi Sep 18, 2026
4ae129f
test: cover a worker write four times its own pipe buffer in the nati…
apresmoi Sep 18, 2026
cadc637
test: seal the spend the proxy measured when a worker's output never …
apresmoi Sep 18, 2026
734056b
fix: fail a brokered worker's model request fast instead of retrying …
apresmoi Sep 18, 2026
d7e36db
fix: trip the launcher's total-output bound from the post-exit drain too
apresmoi Sep 18, 2026
128dd56
test: take the facade before any mount listens so a contended fixed p…
apresmoi Sep 18, 2026
2ef4589
fix: end a per-wake MCP mount's leftover connections so a finished tu…
apresmoi Sep 18, 2026
25f84ef
fix: end the broker provider proxy's leftover sockets on shutdown ins…
apresmoi Sep 18, 2026
6c33505
test: split the MCP tunnel drain test out of the facade suite
apresmoi Sep 18, 2026
c05306e
feat: seal the brokered worker's in-flight MCP tool calls so a hung c…
apresmoi Sep 18, 2026
1ec76f2
feat: seal a brokered turn's terminal evidence into the broker's ledg…
apresmoi Sep 18, 2026
0fe368a
test: prove a worker still blocked in write past the launcher's outpu…
apresmoi Sep 18, 2026
b096da6
fix: raise the launcher's whole-turn output bound to the control prot…
apresmoi Sep 18, 2026
942df75
feat: observe the brokered worker's standalone MCP GET tunnel
apresmoi Sep 18, 2026
edbbb99
docs: record the MCP GET tunnel observation and what the real CLI doe…
apresmoi Sep 18, 2026
1a5ba8d
docs: name the tunnel timings in the seal line's own bound
apresmoi Sep 18, 2026
130b548
test: split the facade's observation suite and its rig out of the fac…
apresmoi Sep 18, 2026
4f4922a
feat: count and seal the MCP facade's refused requests by reason class
apresmoi Sep 18, 2026
35c9bd5
test: pin the seal row's exact field set for a completed turn
apresmoi Sep 18, 2026
3c7e74f
test: type the sealed completed turn's native result frame
apresmoi Sep 18, 2026
074c75c
fix: assert and correct the mode of existing runtime-home subdirectories
apresmoi Sep 18, 2026
f81c55e
fix: ensure the agent tool-state directory through the runtime-home l…
apresmoi Sep 18, 2026
734372a
fix: derive the MCP capability budget from the compiled worker turn b…
apresmoi Sep 18, 2026
36ce9f0
fix: keep the v2 activity closure query answerable after a control ho…
apresmoi Sep 18, 2026
194580f
fix: record the wake outcome that reclaimed an undisposed delivery
apresmoi Sep 18, 2026
8e0cb1a
fix: decide a reclaimed delivery on the wake outcome alone
apresmoi Sep 18, 2026
cff564a
test: await the published offline-reconciliation lease instead of a 5…
apresmoi Sep 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion src/contracts/grokWorkerContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,68 @@
* run directly and saves one `search_tool` round trip per tool (P0: 3 → 2
* requests).
*/

/**
* The atoms of that route, and its single definition.
*
* Both texts a Grok worker receives are rendered from them: the pinned system
* prompt below, and the caller's identity envelope
* ({@link grokMountedToolNamingRule}, used by `src/runtime/engineDispatcher.ts`).
* They were worded independently once, and the envelope told the model to call
* the tools by their bare names - which Grok 1.0.34 refuses outright, before
* any HTTP: `'moltnet_read' is not a valid MCP tool name. Tool names must be
* qualified as \`server__tool\`` (local rig, real CLI, real rendered config).
* There is exactly one valid spelling, so two independently worded naming rules
* are one rule too many; this is the single definition both render from.
*/
export const DAIMON_GROK_MCP_SERVER = "daimon" as const;
/** Grok's own name for an MCP tool of that server: exactly what `tool_name` must carry. */
export const DAIMON_GROK_TOOL_PREFIX = `${DAIMON_GROK_MCP_SERVER}__` as const;
export const grokDaimonToolName = (tool: string): string => `${DAIMON_GROK_TOOL_PREFIX}${tool}`;
/** Grok's two MCP meta-tools, and the argument that names a tool for the first. */
export const GROK_MCP_INVOKE_TOOL = "use_tool" as const;
export const GROK_MCP_SEARCH_TOOL = "search_tool" as const;
export const GROK_MCP_TOOL_NAME_ARGUMENT = "tool_name" as const;
/** Illustrative Daimon tools for the system prompt, which cannot know a wake's real mount. */
const DAIMON_GROK_EXAMPLE_TOOLS = Object.freeze(["moltnet_read", "moltnet_send", "memory_search", "memory_register"] as const);

export const DAIMON_GROK_SYSTEM_PROMPT = [
"You are a headless Daimon agent; no human is present.",
"Your identity, instructions and wake event are in the user prompt.",
"Daimon tools are MCP tools on server daimon: call a known one directly with use_tool (tool_name daimon__moltnet_read, daimon__moltnet_send, daimon__memory_search, daimon__memory_register, or another daimon__ name you were given); use search_tool only for a name you do not know.",
`Daimon tools are MCP tools on server ${DAIMON_GROK_MCP_SERVER}: call a known one directly with ${GROK_MCP_INVOKE_TOOL} (${GROK_MCP_TOOL_NAME_ARGUMENT} ${DAIMON_GROK_EXAMPLE_TOOLS.map(grokDaimonToolName).join(", ")}, or another ${DAIMON_GROK_TOOL_PREFIX} name you were given); use ${GROK_MCP_SEARCH_TOOL} only for a name you do not know.`,
"If a tool result says output was saved to a file, read that path with read_file.",
"If a tool fails, do not retry it in a loop: stop and report the failure.",
"Your final answer is a private note to the runtime: one line, or empty."
].join(" ");

/**
* The same route, stated once for the caller's identity envelope, where a
* wake's real mounted tools are known.
*
* It contributes exactly what the pinned prompt cannot know - the wake's real
* mounted names - and the one rule that makes them callable. It asserts rather
* than corrects: one bare catalogue, one prefix rule, one example, and the
* prefixed form named as the *only* valid form, because that is the CLI's own
* verdict on a bare name rather than a preference.
*
* What it deliberately leaves out is as load bearing. It never claims the
* agent's own instructions spell a tool wrongly, never offers a shell or CLI
* route, never repeats the catalogue in prefixed form - and never restates the
* `search_tool` rule. A Grok worker already reads two authoritative sentences
* about `search_tool`: the pinned prompt's ("only for a name you do not know")
* and Grok's own injected notice, which says the model MUST call it before any
* MCP tool. Observed on the rig: that contradiction is not enforced, and
* `use_tool` works with no prior `search_tool`. A third wording would only add
* a voice, so this sentence stays out of that argument entirely.
*/
export const grokMountedToolNamingRule = (mountedToolNames: readonly string[]): string => {
const example = grokDaimonToolName(mountedToolNames[0] ?? DAIMON_GROK_EXAMPLE_TOOLS[0]);
return `Your mounted tools are exactly: ${mountedToolNames.join(", ")}. `
+ `On this engine each is an MCP tool on server ${DAIMON_GROK_MCP_SERVER}, and its only valid tool name is `
+ `${DAIMON_GROK_TOOL_PREFIX}<name>: invoke it with ${GROK_MCP_INVOKE_TOOL}, ${GROK_MCP_TOOL_NAME_ARGUMENT} = ${example}. `
+ "A bare name is not a valid MCP tool name and reaches nothing.";
};

/** Closed declared-model vocabulary; defaults are `grok-4.6` at `low`. */
export const GROK_BROKER_MODELS = Object.freeze(["grok-4.6", "grok-4.5", "grok-build"] as const);
export const GROK_BROKER_REASONING_EFFORTS = Object.freeze(["low", "medium", "high"] as const);
Expand Down
31 changes: 22 additions & 9 deletions src/contracts/runtimeContractManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ export const GROK_ENGINE_BROKER = {
systemPromptSha256: "2c31c0085a54a4efbf9c0cf0b8124c56e47f38691b7f0c7fa233a74abaa8ddf8",
// sha256 of `renderGrokBrokerWorkerConfig({ model, reasoningEffort })`, the only accepted config.toml bytes.
configSha256: {
"grok-4.6": { low: "eed6a451150a72b2cb528b30c23b3d51c7d3bc38c67a8985d4dcdf956ff214d3", medium: "8850502dbebf8918c5161c63efcc4ccf18719488300f4cec1deceb2c112b451f", high: "3ce44ace503362326b47149b528b942ce638fe146313d62502f248acf9c7333d" },
"grok-4.5": { low: "7aa13e90b9bc08d1a018f48b7a84de1dab41db586627ee2d5a25f69011ba7e25", medium: "218ba37e57a6f02fa36b265b4e154e68e30bd2d4794feb130cc226fdda7732a9", high: "0bb4ad8bfa5062169b28422d1d534b45420d4e46b1e546bda1c578eb34303646" },
"grok-build": { low: "83ac7202442286a65c359cc596b0b8db7bc4529ee70e98224f6cd6f66deb6878", medium: "0146313f28739888eb4e861f1bfb285f7ee4e0a9164669256ebdf6492a2790ce", high: "bbe72aaf70c417dc7007823a7e9e1a7d1fa8d57e50bde6f24036083b32bcc859" }
"grok-4.6": { low: "ab58499ac32678097c146479896f2b8a8e2b0e39aea22dc0a60b6227e370538e", medium: "df1a5cc84346e7f6bf6090492fbd19faaefb42953e3bb2e8c6cbc0572242403f", high: "65b0212564fb74042b1503d293fb8d3620276033264c0efade2a539ca09218e3" },
"grok-4.5": { low: "8247127c3625ff7c5d8d527a53596b89ec6557a821ac46cfd00bd122b90daff6", medium: "59288cee61297bb8c002097061a48f77b09d310754187a253ee089f7172a9155", high: "c63c3387ce92d94ec3f690abfe98942afcd7c9e17ff84816bbe751f340ab251f" },
"grok-build": { low: "fb343f2809903f26d21681470943235031f946e99085542fd89555eb7782cbb5", medium: "8a587ef75c90eab70d19b24583e60051d6fba9d90c558839fdbb15588b4cc656", high: "a23724e00d670caee185ba7690d2daa868173e53905cf446f5329666f01ab4e3" }
},
// Worker `GROK_HOME` layout the broker attests before every turn. The home and
// its `sessions/` directory are root-owned, worker-group writable and sticky so
Expand All @@ -64,10 +64,23 @@ export const GROK_ENGINE_BROKER = {
directory: { uid: 0, group: "worker", mode: 0o1771 },
sessionsDirectory: { relativePath: "sessions", uid: 0, group: "worker", mode: 0o1771 },
readOnlyFiles: { names: ["config.toml", "managed_config.toml", "requirements.toml", "sandbox.toml", "trusted_folders.toml"], uid: 0, gid: 0, mode: 0o444 },
sandboxEvents: { relativePath: "sessions/sandbox-events.jsonl", owner: "worker", group: "broker", mode: 0o640 }
sandboxEvents: { relativePath: "sessions/sandbox-events.jsonl", owner: "worker", group: "broker", mode: 0o640 },
// The launcher exports TMPDIR=<worker home>/tmp; Grok's strict profile grants TMPDIR read-write.
privateTmp: { relativeToWorkerHome: "tmp", owner: "worker", mode: 0o700 },
// Strict also grants shared /tmp and /var/tmp read-write and refuses to start if either is
// denied, so the deployment keeps them from every worker by mode: root-owned, a non-worker
// group (< 2200), others read-only (Grok needs to open the directory) and no search/write.
sharedTmp: { paths: ["/tmp", "/var/tmp"], uid: 0, maxGroupExclusive: 2_200, otherMode: 0o4, mode: 0o1774 },
// The organization runtime home of a brokered Grok agent: traverse-only for the
// worker group so the worker can reach `tool-output/` and nothing else (no group
// read, no group write, no world bits; `physicalReadiness.ts` refuses anything else).
organizationRuntimeHome: { owner: "organization", group: "worker", mode: 0o710 },
// Spilled tool output the worker reads with read_file: setgid directory in the worker's group,
// files written 0640 by the runtime, never other-readable.
spillDirectory: { relativeToRuntimeHome: "tool-output", owner: "organization", group: "worker", mode: 0o2750, fileMode: 0o640 }
}
},
bounds: { promptBytes: 65_536, capabilityBytes: 4_096, capabilityBundleBytes: 8_196, outputBytes: 65_536 },
bounds: { promptBytes: 65_536, capabilityBytes: 4_096, capabilityBundleBytes: 8_196, outputBytes: 262_144 },
// Accounting and limits (P2). The broker is the single sealed usage writer.
controlProtocolVersion: "noopolis.daimon.engine-broker.v2",
turnRecordVersions: ["noopolis.daimon.engine-broker-turn.v1", "noopolis.daimon.engine-broker-turn.v2"],
Expand Down Expand Up @@ -120,9 +133,9 @@ export const GROK_ENGINE_BROKER = {
projectionVersion: "noopolis.daimon.grok-broker-projection.v1",
slotPreflightVersion: "noopolis.daimon.grok-slot-preflight.v2",
artifacts: {
sourceSha256: "36f60689f0a8af0e3108f5f53d78ed52b7d4b6f934c75b6184606dfa82bc741e",
x64Sha256: "36dc76b134eb59cf5a6720b6f94228eb279108e20ea3343fa6efd9ffcb60a4d3",
arm64Sha256: "c93216cc6fa4ca50dc404fe41e68da9150a869b14f46eb42484ae77c3aa400a9"
sourceSha256: "dd39aacfece496cc6528f6acdb4f1066a848a0fb5b0961f5c70b0ba00440dc24",
x64Sha256: "67e3624d3198e9c59e1ffafa4eca7c895dfe265d5b8bb0614cb547b68b8b93a7",
arm64Sha256: "c07d22225ff968bc289e5ddf0981cdd5d64040eee6e3ea45da7d03e1439dea98"
}
} as const;
export const AGY_SUBSCRIPTION_REALM = {
Expand Down Expand Up @@ -172,5 +185,5 @@ export const RUNTIME_CONTRACT_MANIFEST = {
] },
healthResponseSchema: { type: "object", additionalProperties: false, required: ["version", "state", "agents"], properties: { version: { const: "noopolis.daimon.organization-runtime-health.v1" }, state: { enum: ["starting", "running", "stopping", "stopped"] }, agents: { type: "array", maxItems: ORGANIZATION_RUNTIME_MAX_AGENTS, items: { type: "object", additionalProperties: false, required: ["agentId", "state"], properties: { agentId: text, state: { enum: ["starting", "running", "stopping", "stopped", "idle", "failed"] } } } } } },
activityResponseSchema: { type: "object", additionalProperties: false, required: ["version", "items"], properties: { version: { const: "noopolis.daimon.organization-runtime-activity.v1" }, items: { type: "array", maxItems: 100, items: activityItem }, nextCursor: { type: "string", minLength: 1, maxLength: 16, pattern: "^(0|[1-9][0-9]{0,15})$" } } },
activityV2ResponseSchema: { type: "object", additionalProperties: false, required: ["version", "items"], properties: { version: { const: ORGANIZATION_RUNTIME_ACTIVITY_V2_VERSION }, executions: { type: "array", maxItems: ORGANIZATION_RUNTIME_MAX_AGENTS, items: { type: "object", additionalProperties: false, required: ["agent_id", "execution_id", "state", "delivery_ids"], properties: { agent_id: text, execution_id: { type: "string" }, state: { const: "running" }, delivery_ids: { type: "array", maxItems: 32, items: text } } } }, items: { type: "array", maxItems: 2_112, items: { type: "object", additionalProperties: false, required: ["version", "acceptance_id", "agent_id", "delivery_id", "request_digest", "state", "accepted_at", "updated_at", "active"], properties: { version: { const: "noopolis.daimon.wake-receipt-status.v2" }, acceptance_id: { type: "string" }, agent_id: text, delivery_id: text, request_digest: { type: "string" }, state: { enum: ["accepted", "running", "completed", "failed", "stopped"] }, accepted_at: timestamp, updated_at: timestamp, active: { type: "boolean" }, execution_id: { type: "string" }, deferred: { type: "boolean" }, text: { type: "string", maxLength: 16384 }, queue_position: { type: "integer", minimum: 1 }, code: { enum: ["engine_failed", "host_stopped", "host_stopping", "queue_full", "unknown_agent"] } } } } } }
activityV2ResponseSchema: { type: "object", additionalProperties: false, required: ["version", "items"], properties: { version: { const: ORGANIZATION_RUNTIME_ACTIVITY_V2_VERSION }, state: { enum: ["running", "stopped"] }, executions: { type: "array", maxItems: ORGANIZATION_RUNTIME_MAX_AGENTS, items: { type: "object", additionalProperties: false, required: ["agent_id", "execution_id", "state", "delivery_ids"], properties: { agent_id: text, execution_id: { type: "string" }, state: { const: "running" }, delivery_ids: { type: "array", maxItems: 32, items: text } } } }, items: { type: "array", maxItems: 2_112, items: { type: "object", additionalProperties: false, required: ["version", "acceptance_id", "agent_id", "delivery_id", "request_digest", "state", "accepted_at", "updated_at", "active"], properties: { version: { const: "noopolis.daimon.wake-receipt-status.v2" }, acceptance_id: { type: "string" }, agent_id: text, delivery_id: text, request_digest: { type: "string" }, state: { enum: ["accepted", "running", "completed", "failed", "stopped"] }, accepted_at: timestamp, updated_at: timestamp, active: { type: "boolean" }, execution_id: { type: "string" }, deferred: { type: "boolean" }, text: { type: "string", maxLength: 16384 }, queue_position: { type: "integer", minimum: 1 }, code: { enum: ["engine_failed", "host_stopped", "host_stopping", "queued_wake_stopped", "active_wake_aborted", "queue_full", "unknown_agent"] } } } } } }
} as const;
10 changes: 5 additions & 5 deletions src/observability/causalEvents.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createHash, randomUUID } from "node:crypto";
import { constants } from "node:fs";
import { appendFile, mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
import { appendFile, open, readFile, rename, stat, unlink } from "node:fs/promises";
import path from "node:path";
import { ensureRuntimeHomeDirectory } from "../runtime/runtimeHomeLayout.js";

/**
* Daimon's own copy of the `noopolis.causal-event.v1` wire envelope. Field-
Expand Down Expand Up @@ -106,8 +107,7 @@ const readSeqStore = async (runtimeHomePath: string): Promise<CausalSeqStore> =>
};

const writeSeqStore = async (runtimeHomePath: string, store: CausalSeqStore): Promise<void> => {
const directory = telemetryDir(runtimeHomePath);
await mkdir(directory, { recursive: true });
const directory = await ensureRuntimeHomeDirectory(runtimeHomePath, "telemetry");
const file = seqFilePath(runtimeHomePath);
const temporary = `${file}.${randomUUID()}.tmp`;
const handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
Expand Down Expand Up @@ -182,7 +182,7 @@ export const nextCausalSeq = async (input: {
const lockPath = path.resolve(telemetryDir(input.runtimeHomePath), "causal.seq.lock");
const previous = seqAllocationQueues.get(lockPath) ?? Promise.resolve();
const allocation = previous.catch(() => undefined).then(async () => {
await mkdir(telemetryDir(input.runtimeHomePath), { recursive: true });
await ensureRuntimeHomeDirectory(input.runtimeHomePath, "telemetry");
await acquireSeqLock(lockPath);
try {
const store = await readSeqStore(input.runtimeHomePath);
Expand All @@ -207,7 +207,7 @@ export const nextCausalSeq = async (input: {

/** Appends one CausalEvent record as a line of `runtimeHome/telemetry/causal.jsonl`. */
export const appendCausalEvent = async (runtimeHomePath: string, event: CausalEvent): Promise<void> => {
await mkdir(telemetryDir(runtimeHomePath), { recursive: true });
await ensureRuntimeHomeDirectory(runtimeHomePath, "telemetry");
await appendFile(jsonlFilePath(runtimeHomePath), `${JSON.stringify(event)}\n`, "utf8");
};

Expand Down
6 changes: 3 additions & 3 deletions src/observability/orgObserver.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { mkdir, writeFile } from "node:fs/promises";
import { writeFile } from "node:fs/promises";
import path from "node:path";

import type { MemoryRecallAudit } from "@noopolis/mneme";
import { ensureRuntimeHomeDirectory } from "../runtime/runtimeHomeLayout.js";

export interface WakeBenchRow {
agent: string;
Expand Down Expand Up @@ -206,8 +207,7 @@ export class OrgObserver {
}

async write(runtimeRoot: string): Promise<void> {
const telemetryDir = path.join(runtimeRoot, "telemetry");
await mkdir(telemetryDir, { recursive: true });
const telemetryDir = await ensureRuntimeHomeDirectory(runtimeRoot, "telemetry");
const summaryRecord = {
assertions: this.assertions,
behavior: this.behaviorSummary(),
Expand Down
10 changes: 10 additions & 0 deletions src/pi/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ removing redundant nested denies that cannot be mounted by its Linux sandbox.
An intervening readable path or workspace root makes a deeper deny necessary.
Verify changes with generated production arguments and real local sandbox
commands; a model call is neither required nor permitted for this check.

The per-wake MCP mount is torn down on the wake's own completion path, so that
teardown must be bounded. `Server.close()` waits for every open connection, and
a connection the MCP transport has no record of — a socket opened before
`initialize`, or an idle keep-alive socket a client's pool still holds, which is
what relaying a turn through the broker MCP facade leaves behind — is not the
transport's to end. Close the transport first, then end the remaining
connections; never wait for the client to release them. A finished turn that
parks here publishes nothing and dies to an outer deadline, which loses exactly
the terminal evidence the turn existed to produce.
Loading
Loading