Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ coverage/
graph.html
/output/
/design/
/.temp/
evals/topology/results/
evals/scale/.tmp/
evals/hierarchy-scale/.tmp/
Expand Down Expand Up @@ -76,3 +77,7 @@ results/

# pnpm content-addressable store cache (never commit)
.pnpm-store/

# Local goal/planning state (pi goal mode); a scratch plan is not repository content.
.pi/goals/
.pi/.goals-pool-snapshot.json
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ Use current code and owning documents to establish facts; experiments and past
decisions supply evidence and rationale. Update the existing owner with a behavior
change rather than adding another summary. Preserve unrelated working-tree edits.

Do not place necessary files in cache folders, including TEMP and tmp. Please
follow Git best practices when using Git.

It is essential to write code that is readable, maintainable, and extensible. You
can employ appropriate design patterns to achieve this, but avoid using patterns
simply for the sake of using them. If a problem can be solved with a simple
`if-else` statement, there is no need to implement a combination of Strategy,
Factory, and Chain of Responsibility patterns. Cramming too many patterns into a
single class often results in over-engineering.

Required knowledge must be recoverable from the shared repository or an explicit
shared task reference. Local memory, indexes, and prior Agent sessions are optional
accelerators, never the only source needed to continue work.
99 changes: 99 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# syntax=docker/dockerfile:1.7

ARG NODE_VERSION=22.19.0

FROM node:${NODE_VERSION}-bookworm-slim AS build
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY tsconfig.json tsconfig.build.json ./
COPY src ./src
RUN npm run build


FROM node:${NODE_VERSION}-bookworm-slim AS nmg-runtime
ENV DEBIAN_FRONTEND=noninteractive \
NMG_DATA_DIR=/data \
NMG_DB_PATH=/data/nmg.sqlite \
NMG_DAEMON_IDLE_TIMEOUT_MS=0 \
NMG_EMBED_AUTO_SYNC=1 \
NMG_EMBED_LOCAL_SERVER=0

RUN apt-get -o Acquire::Retries=5 update \
&& apt-get -o Acquire::Retries=5 install -y --no-install-recommends \
ca-certificates \
tini \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --omit=optional \
&& npm cache clean --force

COPY --from=build /app/dist ./dist
COPY bin ./bin
COPY docker/entrypoint.sh ./docker/entrypoint.sh

RUN chmod +x ./docker/entrypoint.sh \
&& mkdir -p /data

VOLUME ["/data"]

HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD node bin/nmg.mjs daemon status --db "$NMG_DB_PATH" --json \
| grep -q '"running": true'

ENTRYPOINT ["/usr/bin/tini", "--", "/app/docker/entrypoint.sh"]


# Lightweight runtime for FTS-only use or an external OpenAI-compatible
# embedding provider. It intentionally contains no Python, PyTorch, CUDA, or
# model weights.
FROM nmg-runtime AS external


# Self-contained convenience image. This remains the final/default target so
# existing `docker build .` commands keep producing the local-BGE image.
FROM nmg-runtime AS bge

ARG TORCH_INDEX_URL=https://download.pytorch.org/whl/cu126
ARG BGE_SOURCE_MODEL=BAAI/bge-small-en-v1.5
ARG BGE_REVISION=5c38ec7c405ec4b44b94cc5a9bb96e735b38267a

ENV NMG_EMBED_BASE_URL=http://127.0.0.1:8000/v1 \
NMG_EMBED_MODEL=BAAI/bge-small-en-v1.5 \
NMG_EMBED_PROFILE=bge-en \
NMG_EMBED_LOCAL_SERVER=1 \
BGE_MODEL=/opt/models/bge-small-en-v1.5 \
BGE_PORT=8000 \
VIRTUAL_ENV=/opt/nmg-embed \
PATH=/opt/nmg-embed/bin:$PATH

RUN apt-get -o Acquire::Retries=5 update \
&& apt-get -o Acquire::Retries=5 install -y --no-install-recommends \
python3 \
python3-pip \
python3-venv \
&& rm -rf /var/lib/apt/lists/*

RUN python3 -m venv "$VIRTUAL_ENV" \
&& pip install --no-cache-dir --upgrade pip setuptools wheel \
&& pip install --no-cache-dir --index-url "$TORCH_INDEX_URL" torch

RUN pip install --no-cache-dir sentence-transformers fastapi "uvicorn[standard]"

RUN BGE_SOURCE_MODEL="$BGE_SOURCE_MODEL" BGE_SOURCE_REVISION="$BGE_REVISION" \
python -c 'import os; from sentence_transformers import SentenceTransformer; model = SentenceTransformer(os.environ["BGE_SOURCE_MODEL"], revision=os.environ["BGE_SOURCE_REVISION"]); model.save_pretrained("/opt/models/bge-small-en-v1.5")'

COPY evals/omnimemeval/bge_server.py ./evals/omnimemeval/bge_server.py
COPY evals/omnimemeval/embedding_batcher.py ./evals/omnimemeval/embedding_batcher.py

ENV HF_HUB_OFFLINE=1 \
TRANSFORMERS_OFFLINE=1

HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)" \
&& node bin/nmg.mjs daemon status --db "$NMG_DB_PATH" --json \
| grep -q '"running": true'
7 changes: 5 additions & 2 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ A failure that escaped and meets the [post-mortem triage
rule](postmortem/README.md#when-to-write-one) gets a numbered record under
`postmortem/`, with its index row.

Do not add conventional root community files such as `CODE_OF_CONDUCT.md` or
`CONTRIBUTING.md` by default. NMG's repository collaboration surface is Agent-first:
A failure that escaped and meets the [post-mortem triage
rule](postmortem/README.md#when-to-write-one) gets a numbered record under
`postmortem/`, with its index row.

NMG's repository collaboration surface is Agent-first:
the root `AGENTS.md` is the stable bootstrap, this file routes documentation work,
and `docs/README.md` owns document authority. Add another root entry only when it
prevents a named collaboration failure that these routes cannot cover; assign its
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Archive: the OoO continuation samples (2026-09-14)

**Why this is here.** The runs behind ledger row G7 were driven from a git worktree under `%TEMP%`
(`C:/Users/LEGION/AppData/Local/Temp/nmg-board-verbs`). Committed content survives a temp cleanup -
the branch is pushed and the objects live in the repository - but the _measurements_ would not: the
run logs, the candidate modules the model wrote and the artifacts it submitted were all untracked
scratch under `.nmg/`. This directory puts every sample a claim depends on inside the repository.

**What was run.** Provider `deepseek`, model `deepseek-v4-flash`, driven by
`evals/ooo-execution/live-continuation.ts`; each stage one process, one model call, with the frozen
envelope `turns 10`, `reads 5`, `timeoutMs 300 000`. `wallMs` measured; wall time and token counts come
from the adapter's own accounting, not from a clock outside it.

## Run logs (`run-logs/`)

| file | lines | what it is |
| ---------------------- | ----- | --------------------------------------------------------------------- |
| `g7-run.jsonl` | 25 | 干净的一遍:五个任务,每个 part1 → 边界 → part2,同一 store |
| `m-rep6.jsonl` | 43 | 早期那批因认领被前任占住而连续失败的重复(保留为缺陷证据) |
| `m6.jsonl` | 198 | 配对的重复测量:merge 的 part1/part2 各多次;本文档引用的主要数据集 |
| `merge-repeats6.jsonl` | 44 | 第一次尝试的重复测量(当时失败记录还不完整) |
| `merge-retry.jsonl` | 12 | merge 在独立 store 里的重试(含释放死认领后的一次成功) |
| `run-1.jsonl` | 8 | chunk 的首次全链路(含一条 part2 判为 unchanged 的旧记录) |
| `run-2.jsonl` | 7 | chunk 的 part2 崩在 patchCandidate(invalid patch structure)的那一轮 |
| `run-3.jsonl` | 7 | chunk 在失败记录缺失期的样本 |
| `run-4.jsonl` | 8 | chunk 改为记录失败后的样本 |
| `run-5.jsonl` | 9 | chunk 第一个完整成功的五任务循环样本 |

Each line is one role's record: `role`, `task`, `stage`, `conclusion`, `turns`, `reads`,
`tokens`, `wallMs`, `passed`/`cases`, `failed`, `digest`, `continuedFrom`. A `part2` record's
`continuedFrom` is the digest of the artifact it continued from, which is how a continuation is shown
to start from the delivered bytes rather than from the stub.

## Rejected artifacts (`rejected-artifacts/`)

| file | bytes | the model submitted | why it was not accepted |
| ------------------------- | ----- | ------------------------------------------------- | -------------------------------------------------------- |
| `m6--merge-part2-r14.txt` | 1598 | `kind=conclusion`, `conclusion=no-change-needed` | 合法结论被误判为畸形补丁(当时运行器只认提升文件的补丁) |
| `m6--merge-part2-r19.txt` | 1981 | `kind=conclusion`, `conclusion=no-change-needed` | 同上(修复前留存) |
| `m6--merge-part2-r22.txt` | 1663 | `kind=conclusion`, `conclusion=cannot-complete` | 合法结论,但为 cannot-complete:模型宣告未完成 |
| `m6--merge-part2.txt` | 1482 | `kind=conclusion`, `conclusion=promote-candidate` | 早期一条被拒产物 |

These are the bytes behind a failure claim. They are kept because "invalid patch structure" is a
statement about bytes, and reading them is what showed the string-matching misclassification: a
`no-change-needed` conclusion is a legal answer, not a malformed patch.

## Candidates (`candidates/`)

The modules the model actually produced, `35` of them, named `<run>--<task>--<stage>.ts` (a `-r<N>`
suffix marks the repetition). They are the artifacts the checks passed or rejected, and the evidence for
the cost comparison in the sibling documents.

## Related documents

- `../ooo-real-continuation-2026-09-14.md` - the G7 check itself: five tasks, one boundary each.
- `../ooo-real-continuation-comparison-2026-09-14.md` - the comparison of the stable first stage against
the continuations, and the failure classification this archive supports.
- `../../../../experiments/execution/ooo-contract-obligations.md` - the ledger; row G7.

## What this archive does not hold

- The `.sqlite` board stores (7 MB, binary, reproducible from the logs and the roles).
- Token counts for calls that failed before the usage record existed; those rows show `null` and the
documents say so rather than estimating.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// movingAverage(values, window) returns the mean of each run of `window` consecutive values.
// This stage covers an input longer than a window of 2 or more. The remaining cases of the
// contract arrive with the next handoff.
export function movingAverage(values: readonly number[], window: number): number[] {
const result: number[] = [];
for (let i = 0; i + window <= values.length; i++) {
let sum = 0;
for (let j = i; j < i + window; j++) {
sum += values[j];
}
result.push(sum / window);
}
return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// movingAverage(values, window) returns the mean of each run of `window` consecutive values.
// Contract:
// - One output per full window, so an input shorter than the window yields [] and an
// empty input yields [].
// - A window of 1 returns the values themselves.
// - A window that is not a positive integer (0 or fractional) is refused with an Error.
export function movingAverage(values: readonly number[], window: number): number[] {
if (!Number.isInteger(window) || window < 1) {
throw new Error('window must be a positive integer');
}
const result: number[] = [];
for (let i = 0; i + window <= values.length; i++) {
let sum = 0;
for (let j = i; j < i + window; j++) {
sum += values[j];
}
result.push(sum / window);
}
return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// chunk(items, size) splits items into consecutive groups of at most `size` items.
// For this stage the contract is the ordinary case: a non-empty list whose length is not a
// multiple of `size`, where the last group may be short. The remaining cases of the contract
// arrive with the next handoff. The input array is never modified.
export function chunk(items: readonly unknown[], size: number): unknown[][] {
const result: unknown[][] = [];
for (let i = 0; i < items.length; i += size) {
result.push(items.slice(i, i + size));
}
return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// chunk(items, size) splits items into consecutive groups of at most `size` items.
// Contract:
// - a non-empty list whose length is not a multiple of `size`: the last group may be short
// - an empty list yields no groups at all: chunk([], 3) is []
// - a length that is an exact multiple of `size` yields full groups only
// - size 1 yields one group per item
// - `size` must be a positive integer; 0, negative or fractional sizes are refused with an Error
// The input array is never modified.
export function chunk(items: readonly unknown[], size: number): unknown[][] {
if (!Number.isInteger(size) || size <= 0) {
throw new Error(`chunk: size must be a positive integer, received ${size}`);
}

const result: unknown[][] = [];
if (items.length === 0) {
return result;
}

for (let i = 0; i < items.length; i += size) {
result.push(items.slice(i, i + size));
}
return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// parseDuration(text) reads a duration such as "2s" and returns milliseconds, or null when the
// text is not a duration. This stage covers the single-unit cases: milliseconds and seconds.
// The remaining units and the refusal cases arrive with the next handoff.
export function parseDuration(text: string): number | null {
if (typeof text !== "string") return null;
const match = /^([0-9]+(?:\.[0-9]+)?)(ms|s)$/.exec(text);
if (match === null) return null;
const magnitude = Number(match[1]);
const unit = match[2];
if (unit === "ms") return magnitude;
return magnitude * 1000;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// parseDuration(text) reads a duration such as "2s" and returns milliseconds, or null when the
// text is not a duration. This covers the single-unit cases (ms, s) and the sequential h/m/s
// forms such as "1h2m3s", plus the refusals: empty text, a bare number, or an unknown unit
// such as "1x". The result is always a number or null, never NaN.
export function parseDuration(text: string): number | null {
if (typeof text !== "string") return null;

// Sticky matcher: every token must begin exactly where the previous one ended, so the only
// accepted shape is a contiguous run of number+unit tokens that consumes the whole string.
const token = /([0-9]+(?:\.[0-9]+)?)(ms|h|m|s)/y;

let total = 0;
let index = 0;
let previousRank = Number.POSITIVE_INFINITY;

while (index < text.length) {
token.lastIndex = index;
const match = token.exec(text);
if (match === null) return null;

const rank = unitRank(match[2]);
// Units must walk outward -> inward (h, m, s, ms); a repeat or an out-of-order unit is
// refused, which keeps them in h, m, s order.
if (rank >= previousRank) return null;
previousRank = rank;

total += Number(match[1]) * unitMilliseconds(match[2]);
index = token.lastIndex;
}

// No token at all (empty text) or leftover text that was not a number+unit segment.
if (index === 0 || index !== text.length) return null;
return total;
}

function unitRank(unit: string): number {
if (unit === "h") return 3;
if (unit === "m") return 2;
if (unit === "s") return 1;
return 0; // "ms"
}

function unitMilliseconds(unit: string): number {
if (unit === "h") return 3600000;
if (unit === "m") return 60000;
if (unit === "s") return 1000;
return 1; // "ms"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// mergeSorted(a, b) merges two arrays that are already sorted ascending into one sorted array.
// This stage covers two non-empty arrays of equal length. The remaining cases of the contract
// arrive with the next handoff. The result is a new array.
export function mergeSorted(a: readonly number[], b: readonly number[]): number[] {
const result: number[] = [];
let i = 0;
let j = 0;

// Two-pointer merge: repeatedly take the smaller current head. Using <= from `a`
// keeps the merge stable with respect to `a` for equal elements.
while (i < a.length && j < b.length) {
if (a[i] <= b[j]) {
result.push(a[i]);
i++;
} else {
result.push(b[j]);
j++;
}
}

// Append any remaining elements (no-op for the equal-length case this stage covers).
while (i < a.length) {
result.push(a[i]);
i++;
}
while (j < b.length) {
result.push(b[j]);
j++;
}

return result;
}
Loading
Loading