# v8.2 PR 17: Temporal Memory Tree (TiMem) Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add a hierarchical memory summarization tree (hour→day→week→persona nodes) that compresses long-horizon history and injects the most relevant summary level into recall context.

**Architecture:** New `src/tmt.ts` module with a `TmtBuilder` class. Nodes are markdown files with YAML frontmatter stored under `memory/tmt/`. The orchestrator calls `TmtBuilder.maybeRebuildNodes()` at the end of each consolidation pass and injects the best-match node during recall assembly. All behavior is behind `temporalMemoryTreeEnabled` (default `false`). Fail-open: any TMT error is logged and skipped; recall proceeds without the TMT section.

**Tech Stack:** TypeScript, Node.js `fs/promises`, existing `LocalLlmClient` (already wired in orchestrator), `node:test` + `node:assert/strict` for tests.

---

## Context for the implementer

- **Test runner:** `npm run test` (runs `tsx --test tests/*.test.ts`)
- **All tests:** `npm run preflight:quick`
- **Commit from worktree root:** standard `git add … && git commit -m "…"`
- **Storage pattern:** look at `src/boxes.ts` for how a module reads/writes markdown+YAML frontmatter under `baseDir/`. Mirror that pattern.
- **Frontmatter parsing:** `src/storage.ts` exports `parseFrontmatter()` — use it.
- **LocalLlm:** `src/local-llm.ts` exports `LocalLlmClient`. The orchestrator has `this.localLlm`. TMT receives a `summarize` callback from orchestrator so it has no LLM dependency itself.
- **Config:** add to `src/config.ts` in `PluginConfig` interface AND in `parseConfig()` defaults AND in `openclaw.plugin.json` configSchema.
- **Recall section order** (from `orchestrator.ts` ~line 1000): SharedContext → Profile → KnowledgeIndex → Artifacts → Memory Boxes → QMD Results. TMT node goes **between Memory Boxes and QMD Results** (comment `// 1e. TMT node`).
- **Consolidation hook** (from `orchestrator.ts` `runConsolidation()` ~line 2086): add TMT build call after the main consolidation loop completes, before the function returns.

---

## Task 1: Create `src/tmt.ts` with types and node path helpers

**Files:**
- Create: `src/tmt.ts`

**Step 1: Create the file with types and path helpers**

```typescript
/**
 * Temporal Memory Tree (TiMem-inspired, v8.2)
 *
 * Builds a hierarchy of summarised memory nodes:
 *   hour-HH.md → day.md → week-YYYY-WW.md → persona.md
 *
 * Stored under `<baseDir>/tmt/`.
 * All writes are fail-open: errors are caught and logged, never thrown.
 */

import * as fs from "fs";
import * as path from "path";
import { mkdir, readFile, writeFile, readdir } from "node:fs/promises";

export const TMT_DIR = "tmt";

export type TmtLevel = "hour" | "day" | "week" | "persona";

export interface TmtNodeFrontmatter {
  level: TmtLevel;
  periodStart: string; // ISO date-time
  periodEnd: string;   // ISO date-time
  memoryCount: number;
  sourceIds: string[];
  builtAt: string;     // ISO date-time
}

export interface TmtNode {
  frontmatter: TmtNodeFrontmatter;
  summary: string;
  filePath: string;
}

export interface TmtConfig {
  temporalMemoryTreeEnabled: boolean;
  tmtHourlyMinMemories: number;   // default 3
  tmtSummaryMaxTokens: number;    // default 300
}

export interface MemoryEntry {
  path: string;
  id: string;
  created: string; // ISO date-time
  content: string;
}

// ── Path helpers ────────────────────────────────────────────────────────────

export function tmtDir(baseDir: string): string {
  return path.join(baseDir, TMT_DIR);
}

export function hourNodePath(baseDir: string, date: string, hour: string): string {
  return path.join(tmtDir(baseDir), date, `hour-${hour}.md`);
}

export function dayNodePath(baseDir: string, date: string): string {
  return path.join(tmtDir(baseDir), date, "day.md");
}

export function weekNodePath(baseDir: string, weekKey: string): string {
  return path.join(tmtDir(baseDir), `week-${weekKey}.md`);
}

export function personaNodePath(baseDir: string): string {
  return path.join(tmtDir(baseDir), "persona.md");
}

// ── Frontmatter helpers ─────────────────────────────────────────────────────

export function serialiseTmtNode(fm: TmtNodeFrontmatter, summary: string): string {
  const yaml = [
    "---",
    `level: ${fm.level}`,
    `periodStart: "${fm.periodStart}"`,
    `periodEnd: "${fm.periodEnd}"`,
    `memoryCount: ${fm.memoryCount}`,
    `sourceIds: [${fm.sourceIds.map((id) => `"${id}"`).join(", ")}]`,
    `builtAt: "${fm.builtAt}"`,
    "---",
  ].join("\n");
  return `${yaml}\n\n${summary}\n`;
}

export function parseIsoDate(iso: string): string {
  return iso.slice(0, 10); // YYYY-MM-DD
}

export function parseIsoHour(iso: string): string {
  return iso.slice(11, 13); // HH
}

// Returns ISO week key: YYYY-WW
export function isoWeekKey(date: Date): string {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const dayNum = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - dayNum);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
  return `${d.getUTCFullYear()}-${String(weekNo).padStart(2, "0")}`;
}
```

**Step 2: Run the linter/typecheck to confirm the file is valid TypeScript**

```bash
npx tsc --noEmit 2>&1 | head -20
```

Expected: no errors related to `tmt.ts`

**Step 3: Commit**

```bash
git add src/tmt.ts
git commit -m "feat(tmt): add TmtNode types, path helpers, and serialisation utilities"
```

---

## Task 2: Write failing unit tests for path helpers and serialisation

**Files:**
- Create: `tests/tmt.test.ts`

**Step 1: Write the failing tests**

```typescript
/**
 * Tests for Temporal Memory Tree (v8.2 PR 17)
 */
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";

import {
  tmtDir,
  hourNodePath,
  dayNodePath,
  weekNodePath,
  personaNodePath,
  serialiseTmtNode,
  parseIsoDate,
  parseIsoHour,
  isoWeekKey,
  type TmtNodeFrontmatter,
} from "../src/tmt.js";

async function makeTmp(): Promise<string> {
  return mkdtemp(path.join(tmpdir(), "engram-tmt-"));
}
async function cleanup(dir: string): Promise<void> {
  await rm(dir, { recursive: true, force: true });
}

// ── Path helpers ─────────────────────────────────────────────────────────────

test("tmtDir returns baseDir/tmt", () => {
  assert.equal(tmtDir("/mem"), "/mem/tmt");
});

test("hourNodePath returns correct path", () => {
  assert.equal(
    hourNodePath("/mem", "2026-02-22", "14"),
    "/mem/tmt/2026-02-22/hour-14.md",
  );
});

test("dayNodePath returns correct path", () => {
  assert.equal(dayNodePath("/mem", "2026-02-22"), "/mem/tmt/2026-02-22/day.md");
});

test("weekNodePath returns correct path", () => {
  assert.equal(weekNodePath("/mem", "2026-08"), "/mem/tmt/week-2026-08.md");
});

test("personaNodePath returns correct path", () => {
  assert.equal(personaNodePath("/mem"), "/mem/tmt/persona.md");
});

// ── ISO helpers ───────────────────────────────────────────────────────────────

test("parseIsoDate extracts YYYY-MM-DD", () => {
  assert.equal(parseIsoDate("2026-02-22T14:30:00.000Z"), "2026-02-22");
});

test("parseIsoHour extracts HH", () => {
  assert.equal(parseIsoHour("2026-02-22T14:30:00.000Z"), "14");
});

test("isoWeekKey returns correct week", () => {
  const key = isoWeekKey(new Date("2026-02-22"));
  assert.match(key, /^\d{4}-\d{2}$/);
  assert.equal(key, "2026-08"); // 2026-02-22 is in week 08
});

// ── Serialisation ─────────────────────────────────────────────────────────────

test("serialiseTmtNode produces valid markdown with YAML frontmatter", () => {
  const fm: TmtNodeFrontmatter = {
    level: "hour",
    periodStart: "2026-02-22T14:00:00.000Z",
    periodEnd: "2026-02-22T15:00:00.000Z",
    memoryCount: 5,
    sourceIds: ["abc", "def"],
    builtAt: "2026-02-22T15:01:00.000Z",
  };
  const result = serialiseTmtNode(fm, "User worked on engram TMT feature.");
  assert.ok(result.startsWith("---\n"));
  assert.ok(result.includes("level: hour"));
  assert.ok(result.includes("memoryCount: 5"));
  assert.ok(result.includes('sourceIds: ["abc", "def"]'));
  assert.ok(result.includes("User worked on engram TMT feature."));
});
```

**Step 2: Run tests to verify they fail (module exists, helpers work)**

```bash
npm run test -- tests/tmt.test.ts 2>&1 | tail -20
```

Expected: all tests pass (these test pure functions that are already implemented)

**Step 3: Commit**

```bash
git add tests/tmt.test.ts
git commit -m "test(tmt): add unit tests for path helpers, ISO utilities, and serialisation"
```

---

## Task 3: Implement TmtBuilder class in `src/tmt.ts`

**Files:**
- Modify: `src/tmt.ts` (append after the helpers)

**Step 1: Add the TmtBuilder class**

Append to `src/tmt.ts`:

```typescript
// ── TmtBuilder ──────────────────────────────────────────────────────────────

type SummarizeFn = (memories: string[], level: TmtLevel) => Promise<string>;

export class TmtBuilder {
  constructor(
    private readonly baseDir: string,
    private readonly cfg: TmtConfig,
  ) {}

  /**
   * Called after each consolidation pass.
   * Groups memories by hour, builds any missing hour nodes,
   * then rolls up to day → week → persona if needed.
   * All errors are caught and logged; never throws.
   */
  async maybeRebuildNodes(
    memories: MemoryEntry[],
    summarize: SummarizeFn,
  ): Promise<void> {
    if (!this.cfg.temporalMemoryTreeEnabled || memories.length === 0) return;
    try {
      await mkdir(tmtDir(this.baseDir), { recursive: true });
      await this.buildHourNodes(memories, summarize);
      await this.buildDayNodes(memories, summarize);
      // Week and persona are built lazily on day rollup — deferred until Task 4
    } catch (err) {
      console.warn(`[engram] tmt: rebuild failed (ignored): ${err}`);
    }
  }

  private async buildHourNodes(memories: MemoryEntry[], summarize: SummarizeFn): Promise<void> {
    // Group by date+hour
    const byHour = new Map<string, MemoryEntry[]>();
    for (const m of memories) {
      const date = parseIsoDate(m.created);
      const hour = parseIsoHour(m.created);
      const key = `${date}::${hour}`;
      if (!byHour.has(key)) byHour.set(key, []);
      byHour.get(key)!.push(m);
    }

    for (const [key, entries] of byHour) {
      if (entries.length < this.cfg.tmtHourlyMinMemories) continue;
      const [date, hour] = key.split("::");
      const nodePath = hourNodePath(this.baseDir, date, hour);

      // Skip if already built
      if (fs.existsSync(nodePath)) continue;

      const summary = await summarize(entries.map((e) => e.content), "hour");
      const sortedCreated = entries.map((e) => e.created).sort();
      const fm: TmtNodeFrontmatter = {
        level: "hour",
        periodStart: sortedCreated[0],
        periodEnd: sortedCreated[sortedCreated.length - 1],
        memoryCount: entries.length,
        sourceIds: entries.map((e) => e.id),
        builtAt: new Date().toISOString(),
      };
      await mkdir(path.dirname(nodePath), { recursive: true });
      await writeFile(nodePath, serialiseTmtNode(fm, summary), "utf8");
    }
  }

  private async buildDayNodes(memories: MemoryEntry[], summarize: SummarizeFn): Promise<void> {
    // Group by date
    const byDate = new Map<string, MemoryEntry[]>();
    for (const m of memories) {
      const date = parseIsoDate(m.created);
      if (!byDate.has(date)) byDate.set(date, []);
      byDate.get(date)!.push(m);
    }

    for (const [date, entries] of byDate) {
      const nodePath = dayNodePath(this.baseDir, date);
      // Rebuild day node if it doesn't exist or if memory count changed
      let shouldBuild = !fs.existsSync(nodePath);
      if (!shouldBuild) {
        try {
          const existing = await readFile(nodePath, "utf8");
          const countMatch = existing.match(/memoryCount: (\d+)/);
          if (countMatch && parseInt(countMatch[1], 10) < entries.length) {
            shouldBuild = true; // more memories now — rebuild
          }
        } catch { shouldBuild = true; }
      }
      if (!shouldBuild) continue;

      const summary = await summarize(entries.map((e) => e.content), "day");
      const sortedCreated = entries.map((e) => e.created).sort();
      const fm: TmtNodeFrontmatter = {
        level: "day",
        periodStart: `${date}T00:00:00.000Z`,
        periodEnd: `${date}T23:59:59.999Z`,
        memoryCount: entries.length,
        sourceIds: entries.map((e) => e.id),
        builtAt: new Date().toISOString(),
      };
      await mkdir(path.dirname(nodePath), { recursive: true });
      await writeFile(nodePath, serialiseTmtNode(fm, summary), "utf8");
    }
  }

  /**
   * Return the summary text of the most relevant TMT node for a given prompt.
   * Preference: day node for today > most recent day node > most recent hour node.
   * Returns null if no nodes exist or feature is disabled.
   */
  async getMostRelevantNode(): Promise<{ level: TmtLevel; summary: string } | null> {
    if (!this.cfg.temporalMemoryTreeEnabled) return null;
    try {
      const dir = tmtDir(this.baseDir);
      if (!fs.existsSync(dir)) return null;

      // Try today's day node first
      const today = new Date().toISOString().slice(0, 10);
      const todayDay = dayNodePath(this.baseDir, today);
      if (fs.existsSync(todayDay)) {
        const content = await readFile(todayDay, "utf8");
        const summary = content.replace(/^---[\s\S]*?---\n\n?/, "").trim();
        if (summary) return { level: "day", summary };
      }

      // Find most recent date directory
      let entries: string[] = [];
      try { entries = await readdir(dir); } catch { return null; }
      const dateDirs = entries.filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort().reverse();

      for (const dateDir of dateDirs) {
        const dayPath = dayNodePath(this.baseDir, dateDir);
        if (fs.existsSync(dayPath)) {
          const content = await readFile(dayPath, "utf8");
          const summary = content.replace(/^---[\s\S]*?---\n\n?/, "").trim();
          if (summary) return { level: "day", summary };
        }
      }
      return null;
    } catch {
      return null;
    }
  }
}
```

**Step 2: Run typecheck**

```bash
npx tsc --noEmit 2>&1 | head -20
```

Expected: no errors

**Step 3: Commit**

```bash
git add src/tmt.ts
git commit -m "feat(tmt): implement TmtBuilder with hour/day node building and recall retrieval"
```

---

## Task 4: Write TmtBuilder integration tests

**Files:**
- Modify: `tests/tmt.test.ts`

**Step 1: Add integration tests**

Append to `tests/tmt.test.ts`:

```typescript
// ── TmtBuilder integration ────────────────────────────────────────────────────

const mockSummarize: import("../src/tmt.js").SummarizeFn = async (memories, _level) => {
  return `Summary of ${memories.length} memories.`;
};

const defaultCfg: import("../src/tmt.js").TmtConfig = {
  temporalMemoryTreeEnabled: true,
  tmtHourlyMinMemories: 2,
  tmtSummaryMaxTokens: 300,
};

function makeEntries(n: number, date = "2026-02-22", hourOffset = 0): import("../src/tmt.js").MemoryEntry[] {
  return Array.from({ length: n }, (_, i) => ({
    path: `/mem/facts/${date}/fact-${i}.md`,
    id: `fact-${i}`,
    created: `${date}T${String(hourOffset).padStart(2, "0")}:${String(i * 3).padStart(2, "0")}:00.000Z`,
    content: `Memory content ${i}`,
  }));
}

test("TmtBuilder: disabled flag → no files written", async () => {
  const dir = await makeTmp();
  try {
    const builder = new (await import("../src/tmt.js")).TmtBuilder(dir, {
      ...defaultCfg,
      temporalMemoryTreeEnabled: false,
    });
    await builder.maybeRebuildNodes(makeEntries(5), mockSummarize);
    const { existsSync } = await import("node:fs");
    assert.ok(!existsSync(path.join(dir, "tmt")), "tmt dir should not exist");
  } finally { await cleanup(dir); }
});

test("TmtBuilder: builds hour node when min memories threshold met", async () => {
  const dir = await makeTmp();
  try {
    const { TmtBuilder } = await import("../src/tmt.js");
    const builder = new TmtBuilder(dir, defaultCfg);
    const entries = makeEntries(3, "2026-02-22", 14); // 3 memories in hour 14
    await builder.maybeRebuildNodes(entries, mockSummarize);
    const { existsSync } = await import("node:fs");
    const nodePath = path.join(dir, "tmt", "2026-02-22", "hour-14.md");
    assert.ok(existsSync(nodePath), "hour node should be written");
    const content = await (await import("node:fs/promises")).readFile(nodePath, "utf8");
    assert.ok(content.includes("level: hour"));
    assert.ok(content.includes("memoryCount: 3"));
    assert.ok(content.includes("Summary of 3 memories."));
  } finally { await cleanup(dir); }
});

test("TmtBuilder: skips hour node when below min memories threshold", async () => {
  const dir = await makeTmp();
  try {
    const { TmtBuilder } = await import("../src/tmt.js");
    const builder = new TmtBuilder(dir, defaultCfg); // threshold = 2
    const entries = makeEntries(1, "2026-02-22", 10); // only 1 memory
    await builder.maybeRebuildNodes(entries, mockSummarize);
    const { existsSync } = await import("node:fs");
    assert.ok(!existsSync(path.join(dir, "tmt", "2026-02-22", "hour-10.md")));
  } finally { await cleanup(dir); }
});

test("TmtBuilder: builds day node", async () => {
  const dir = await makeTmp();
  try {
    const { TmtBuilder } = await import("../src/tmt.js");
    const builder = new TmtBuilder(dir, defaultCfg);
    const entries = makeEntries(5, "2026-02-22", 9);
    await builder.maybeRebuildNodes(entries, mockSummarize);
    const nodePath = path.join(dir, "tmt", "2026-02-22", "day.md");
    const { existsSync } = await import("node:fs");
    assert.ok(existsSync(nodePath), "day node should be written");
    const content = await (await import("node:fs/promises")).readFile(nodePath, "utf8");
    assert.ok(content.includes("level: day"));
    assert.ok(content.includes("memoryCount: 5"));
  } finally { await cleanup(dir); }
});

test("TmtBuilder: getMostRelevantNode returns null when no nodes exist", async () => {
  const dir = await makeTmp();
  try {
    const { TmtBuilder } = await import("../src/tmt.js");
    const builder = new TmtBuilder(dir, defaultCfg);
    const node = await builder.getMostRelevantNode();
    assert.equal(node, null);
  } finally { await cleanup(dir); }
});

test("TmtBuilder: getMostRelevantNode returns day node content after build", async () => {
  const dir = await makeTmp();
  try {
    const { TmtBuilder } = await import("../src/tmt.js");
    const builder = new TmtBuilder(dir, defaultCfg);
    const today = new Date().toISOString().slice(0, 10);
    const entries = makeEntries(5, today, 9);
    await builder.maybeRebuildNodes(entries, mockSummarize);
    const node = await builder.getMostRelevantNode();
    assert.ok(node !== null);
    assert.equal(node.level, "day");
    assert.ok(node.summary.includes("Summary of"));
  } finally { await cleanup(dir); }
});

test("TmtBuilder: disabled → getMostRelevantNode returns null", async () => {
  const dir = await makeTmp();
  try {
    const { TmtBuilder } = await import("../src/tmt.js");
    const builder = new TmtBuilder(dir, { ...defaultCfg, temporalMemoryTreeEnabled: false });
    const node = await builder.getMostRelevantNode();
    assert.equal(node, null);
  } finally { await cleanup(dir); }
});
```

**Step 2: Run tests**

```bash
npm run test -- tests/tmt.test.ts 2>&1 | tail -30
```

Expected: all tests pass

**Step 3: Commit**

```bash
git add tests/tmt.test.ts
git commit -m "test(tmt): add TmtBuilder integration tests for node building and retrieval"
```

---

## Task 5: Add config flags

**Files:**
- Modify: `src/types.ts` (PluginConfig interface)
- Modify: `src/config.ts` (parseConfig defaults)
- Modify: `openclaw.plugin.json` (configSchema)

**Step 1: Add to `src/types.ts`**

Find the `PluginConfig` interface. In the section where v8.0 flags live (search for `memoryBoxesEnabled`), add after the existing flags:

```typescript
  // v8.2: Temporal Memory Tree
  temporalMemoryTreeEnabled: boolean;
  tmtHourlyMinMemories: number;
  tmtSummaryMaxTokens: number;
```

**Step 2: Add to `src/config.ts`**

In `parseConfig()`, find the section returning the config object. After the `boxRecallDays` line, add:

```typescript
    // v8.2: Temporal Memory Tree
    temporalMemoryTreeEnabled: cfg.temporalMemoryTreeEnabled === true,
    tmtHourlyMinMemories:
      typeof cfg.tmtHourlyMinMemories === "number" ? cfg.tmtHourlyMinMemories : 3,
    tmtSummaryMaxTokens:
      typeof cfg.tmtSummaryMaxTokens === "number" ? cfg.tmtSummaryMaxTokens : 300,
```

**Step 3: Add to `openclaw.plugin.json`**

In the `configSchema.properties` object, add after the existing v8.0/v8.1 flags:

```json
"temporalMemoryTreeEnabled": {
  "type": "boolean",
  "default": false,
  "description": "Build a hierarchical memory summary tree (hour/day/week/persona nodes). Default: false."
},
"tmtHourlyMinMemories": {
  "type": "number",
  "default": 3,
  "description": "Minimum new memories in an hour to trigger an hour-level TMT node. Default: 3."
},
"tmtSummaryMaxTokens": {
  "type": "number",
  "default": 300,
  "description": "Max tokens for each TMT summary node. Default: 300."
}
```

**Step 4: Run typecheck**

```bash
npx tsc --noEmit 2>&1 | head -20
```

Expected: no errors

**Step 5: Run the config contract validator**

```bash
npm run check-config-contract 2>&1 | tail -10
```

Expected: passes (or reports only the new keys as needed — fix any gaps)

**Step 6: Commit**

```bash
git add src/types.ts src/config.ts openclaw.plugin.json
git commit -m "feat(tmt): add config flags for temporalMemoryTreeEnabled, tmtHourlyMinMemories, tmtSummaryMaxTokens"
```

---

## Task 6: Wire TmtBuilder into the orchestrator

**Files:**
- Modify: `src/orchestrator.ts`

### 6a: Import and instantiate TmtBuilder

**Step 1: Add import** at the top of `src/orchestrator.ts` with the other module imports:

```typescript
import { TmtBuilder, type TmtConfig } from "./tmt.js";
```

**Step 2: Add instance field** near the other private fields (around where `boxBuilders` is declared, ~line 187):

```typescript
  private tmtBuilder: TmtBuilder | null = null;
```

**Step 3: Initialize in the constructor** (find where `BoxBuilder` or other module builders are initialised, or add at the end of the constructor body):

```typescript
    if (this.config.temporalMemoryTreeEnabled) {
      const tmtCfg: TmtConfig = {
        temporalMemoryTreeEnabled: this.config.temporalMemoryTreeEnabled,
        tmtHourlyMinMemories: this.config.tmtHourlyMinMemories,
        tmtSummaryMaxTokens: this.config.tmtSummaryMaxTokens,
      };
      this.tmtBuilder = new TmtBuilder(this.config.memoryDir, tmtCfg);
    }
```

**Step 4: Run typecheck**

```bash
npx tsc --noEmit 2>&1 | head -20
```

Expected: no errors

**Step 5: Commit**

```bash
git add src/orchestrator.ts
git commit -m "feat(tmt): instantiate TmtBuilder in orchestrator"
```

### 6b: Hook into consolidation

**Step 1: Add summarize helper** near the bottom of the orchestrator class (before the closing `}`):

```typescript
  /**
   * Produces a 1-paragraph summary of the given memory contents for TMT nodes.
   * Uses the local LLM if available; falls back to a simple concatenation truncation.
   */
  private async tmtSummarize(
    memories: string[],
    _level: import("./tmt.js").TmtLevel,
  ): Promise<string> {
    const joined = memories.map((m, i) => `${i + 1}. ${m}`).join("\n").slice(0, 4000);
    if (!this.localLlm) {
      // No LLM — return a compact snippet list
      return memories.slice(0, 5).map((m) => m.slice(0, 120)).join(" | ");
    }
    try {
      const prompt = `Summarise these memories in 2-4 sentences, preserving key facts, decisions, and preferences:\n\n${joined}`;
      const result = await this.localLlm.complete(prompt, { maxTokens: this.config.tmtSummaryMaxTokens });
      return result.trim();
    } catch {
      return memories.slice(0, 5).map((m) => m.slice(0, 120)).join(" | ");
    }
  }
```

**Step 2: Call TMT rebuild at end of `runConsolidation()`**

Find the `runConsolidation()` method. At the very end, just before the `return` statement:

```typescript
    // v8.2: Temporal Memory Tree — rebuild nodes after consolidation
    if (this.tmtBuilder) {
      const tmtEntries = allMemories
        .filter((m) => m.frontmatter?.id && m.frontmatter?.created)
        .map((m) => ({
          path: m.path,
          id: m.frontmatter.id,
          created: m.frontmatter.created as string,
          content: m.content ?? "",
        }));
      this.tmtBuilder
        .maybeRebuildNodes(tmtEntries, (mems, lvl) => this.tmtSummarize(mems, lvl))
        .catch((err) => log.warn(`tmt: maybeRebuildNodes failed: ${err}`));
    }
```

**Step 3: Run typecheck**

```bash
npx tsc --noEmit 2>&1 | head -20
```

Expected: no errors

**Step 4: Commit**

```bash
git add src/orchestrator.ts
git commit -m "feat(tmt): hook TmtBuilder into runConsolidation for automatic node building"
```

### 6c: Inject TMT node into recall context

**Step 1: Find the recall assembly section** (search for `// 1d. Memory Boxes` comment, around line 1026). After the Memory Boxes block ends (after the closing `}` of the boxes `if` block), add:

```typescript
    // 1e. Temporal Memory Tree node (v8.2)
    if (this.tmtBuilder) {
      const tmtNode = await this.tmtBuilder.getMostRelevantNode().catch(() => null);
      if (tmtNode) {
        const label = tmtNode.level === "day" ? "Today's Memory Summary" :
                      tmtNode.level === "hour" ? "Recent Memory Summary" : "Memory Summary";
        sections.push(`## ${label}\n\n${tmtNode.summary}`);
      }
    }
```

**Step 2: Run typecheck**

```bash
npx tsc --noEmit 2>&1 | head -20
```

Expected: no errors

**Step 3: Commit**

```bash
git add src/orchestrator.ts
git commit -m "feat(tmt): inject TMT node summary into recall context assembly"
```

---

## Task 7: Full preflight and cleanup

**Step 1: Run full preflight**

```bash
npm run preflight:quick 2>&1 | tail -30
```

Expected: all tests pass, typecheck passes, build succeeds

**Step 2: Run the build**

```bash
npm run build 2>&1 | tail -10
```

Expected: no errors

**Step 3: Final commit if anything changed**

```bash
git add -A
git status
```

If there are uncommitted changes, commit them:

```bash
git commit -m "chore(tmt): final cleanup after preflight"
```

---

## Task 8: Open PR

**Step 1: Push branch**

```bash
git push -u origin HEAD
```

**Step 2: Open PR**

```bash
gh pr create \
  --repo joshuaswarren/openclaw-engram \
  --title "feat(v8.2): Temporal Memory Tree — hour/day/week nodes with recall injection" \
  --label "enhancement" \
  --label "documentation" \
  --body "$(cat <<'EOF'
## Summary

Implements the Temporal Memory Tree (TiMem-inspired) component of v8.2.

- New `src/tmt.ts`: TmtBuilder class that builds hour/day summary nodes from memory files after each consolidation pass
- Storage: `memory/tmt/YYYY-MM-DD/hour-HH.md`, `memory/tmt/YYYY-MM-DD/day.md`
- Recall injection: `## Today's Memory Summary` section injected between Memory Boxes and QMD results
- New config flags: `temporalMemoryTreeEnabled` (default `false`), `tmtHourlyMinMemories` (default 3), `tmtSummaryMaxTokens` (default 300)
- Fail-open: all errors caught and logged; baseline recall path unaffected when disabled

## Test plan
- [x] All tests pass
- [x] `temporalMemoryTreeEnabled: false` → no files written, no recall section added
- [x] `temporalMemoryTreeEnabled: true` → nodes built after consolidation, summary injected
EOF
)"
```

---

## Notes for implementer

- If `this.localLlm` doesn't exist on the orchestrator as a public/private field, search for `localLlm` or `LocalLlmClient` usages to find the correct field name.
- The `allMemories` variable in `runConsolidation()` is already defined at the top of that method — reuse it for the TMT entries, don't re-read.
- Do NOT push to main; all commits go to the feature branch created by the worktree setup.
- Week-level and persona-level node building is intentionally deferred — the structure is in place but only hour/day are built in this PR. This keeps the PR focused.
