Page actions

Memory

Deep Agents maintains /memories/AGENTS.md itself through write_file and edit_file. The shared Python graph uses StoreBackend, so a fresh conversation can recall the file from the store rather than from an earlier transcript. Angular remains the default on this guide. The experimental React preview uses native React chat components and the same graph and prompt.

Teach and recall

In Angular Run, Tell it about your operation sends the suggested prompt immediately. Reload before asking what the agent remembers to start a new thread under the same browser identity. The Angular panel renders trimmed nonblank lines. In React, Teach and Recall only fill a draft; explicit Send or Enter authors a turn. React preserves literal blank lines, Unicode and whitespace, displays full tool arguments and results, and renders assistant Markdown.

Opening either example does not create a thread. React also keeps mode changes, history navigation, unsent drafts, New and saved-history selection free of authoring. New clears the React draft and local view; reload clears the retained local catalog. A successful followup reuses the owned thread; New or reload creates a fresh thread lazily on the next Send.

Capture the browser scope

The storage key threadplane.deep-agents-memory.scope.v1 stores a canonical UUIDv4. Each app generation captures the persisted readback winner and supplies exactly context: { memory_scope }. The namespace is ('cockpit', 'deep-agents-memory', 'browser-v1', scope). New conversations retain the same scope while storage is available.

Storage is partitioned by origin and browser storage partition. Angular and React on the same origin share the key; local development ports have different origins. Unavailable or corrupt storage blocks authoring before SDK thread creation. Replacement or deletion later blocks future turns. An already active turn may settle under its captured scope with a warning. There is no read, migration or reset of the historical global namespace.

Warning: Logical partition, not authentication

A browser UUID is not authorization. Public state APIs fall outside this scope guarantee. Derive an authenticated namespace and enforce server authorization when your application requires it.

memory-scope.ts
import {
  DestroyRef,
  Injectable,
  InjectionToken,
  inject,
  signal,
} from '@angular/core';
 
export const SCOPE_KEY = 'threadplane.deep-agents-memory.scope.v1';
export const validScope = (value: unknown): value is string =>
  typeof value === 'string' &&
  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(
    value
  );
export interface ScopeStorage {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
}
export interface ScopeEvent {
  readonly key: string | null;
  readonly newValue: string | null;
  readonly storageArea: unknown;
}
export interface ScopeEvents {
  addEventListener(
    type: 'storage',
    listener: (event: ScopeEvent) => void
  ): void;
  removeEventListener(
    type: 'storage',
    listener: (event: ScopeEvent) => void
  ): void;
}
export interface ScopeOptions {
  readonly storage: () => ScopeStorage;
  readonly uuid: () => string;
  readonly events?: ScopeEvents;
}
export type ScopeSnapshot = Readonly<
  | { status: 'ready'; scope: string }
  | { status: 'blocked'; scope?: string; warning: string }
>;
export interface BrowserScope {
  getSnapshot(): ScopeSnapshot;
  subscribe(listener: () => void): () => void;
  /** Call immediately before EVERY author action, including creation/queued runs. */
  admit(): Readonly<{ memory_scope: string }> | undefined;
  dispose(): void;
}
/** One immutable identity per app generation. Storage failure permanently blocks
 * future authoring; a previously captured context can still settle under its owner. */
export function createBrowserScope(options: ScopeOptions): BrowserScope {
  let storage: ScopeStorage | undefined,
    scope: string | undefined,
    disposed = false;
  let snapshot: ScopeSnapshot = Object.freeze({
    status: 'blocked',
    warning:
      'Browser memory setup is unavailable. Reload after checking browser storage.',
  });
  const listeners = new Set<() => void>();
  const block = () => {
    if (snapshot.status === 'blocked') return;
    snapshot = Object.freeze({
      status: 'blocked',
      scope,
      warning:
        'Browser memory identity changed or storage is unavailable. Active work keeps its original scope. Reload before sending again.',
    });
    for (const listener of [...listeners]) listener();
  };
  const onStorage = (event: ScopeEvent) => {
    if (
      disposed ||
      (event.storageArea !== null && event.storageArea !== storage) ||
      (event.key !== null && event.key !== SCOPE_KEY)
    )
      return;
    if (event.key === null || event.newValue !== scope) block();
  };
  let listening = false;
  try {
    storage = options.storage();
    let value = storage.getItem(SCOPE_KEY);
    if (value === null) {
      const generated = options.uuid();
      if (!validScope(generated)) throw Error('Invalid generated identity');
      storage.setItem(SCOPE_KEY, generated);
      value = storage.getItem(SCOPE_KEY);
    }
    if (!validScope(value)) throw Error('Invalid stored identity');
    scope = value;
    snapshot = Object.freeze({ status: 'ready', scope });
    if (options.events) {
      options.events.addEventListener('storage', onStorage);
      listening = true;
    }
  } catch {
    block();
  }
  return Object.freeze({
    getSnapshot: () => snapshot,
    subscribe(listener: () => void) {
      if (!disposed) listeners.add(listener);
      return () => {
        listeners.delete(listener);
      };
    },
    admit() {
      if (disposed || snapshot.status !== 'ready') return undefined;
      try {
        if (storage?.getItem(SCOPE_KEY) !== scope) block();
      } catch {
        block();
      }
      return snapshot.status === 'ready'
        ? Object.freeze({ memory_scope: scope! })
        : undefined;
    },
    dispose() {
      if (disposed) return;
      disposed = true;
      if (listening) options.events?.removeEventListener('storage', onStorage);
      block();
      listeners.clear();
    },
  });
}
 
/** Local Angular injection seams keep storage failures testable without a shared runtime. */
export const MEMORY_SCOPE_OPTIONS = new InjectionToken<ScopeOptions>(
  'MEMORY_SCOPE_OPTIONS',
  {
    providedIn: 'root',
    factory: () => ({
      storage: () => window.localStorage,
      uuid: () => window.crypto.randomUUID(),
      events: window,
    }),
  }
);
@Injectable({ providedIn: 'root' })
export class MemoryScope {
  private readonly controller = createBrowserScope(
    inject(MEMORY_SCOPE_OPTIONS)
  );
  private readonly state = signal(this.controller.getSnapshot());
  readonly snapshot = this.state.asReadonly();
  readonly scope = this.controller.getSnapshot().scope;
  constructor() {
    const unsubscribe = this.controller.subscribe(() =>
      this.state.set(this.controller.getSnapshot())
    );
    inject(DestroyRef).onDestroy(() => {
      unsubscribe();
      this.controller.dispose();
    });
  }
  admit(): Readonly<{ memory_scope: string }> | undefined {
    return this.controller.admit();
  }
}
memory-transport.ts
import {
  FetchStreamTransport,
  type LangGraphSubmitOptions,
  type StreamEvent,
} from '@threadplane/langgraph';
import type { MemoryScope } from './memory-scope';
/** Admission precedes even thread creation. The inherited adapter owns all SDK
 * operation boundaries and read/join/cancel behavior. Context extras are retained,
 * but the Memory graph intentionally accepts exactly memory_scope. */
export class MemoryTransport extends FetchStreamTransport {
  constructor(
    apiUrl: string,
    private readonly scope: Pick<MemoryScope, 'admit'>,
    onThreadId?: ConstructorParameters<typeof FetchStreamTransport>[1],
    clientOptions?: ConstructorParameters<typeof FetchStreamTransport>[2],
    reportOperationFailure?: ConstructorParameters<
      typeof FetchStreamTransport
    >[3]
  ) {
    super(
      apiUrl,
      onThreadId,
      { ...clientOptions, maxRetries: 0 },
      reportOperationFailure
    );
  }
  private scoped(options?: LangGraphSubmitOptions): LangGraphSubmitOptions {
    const context = this.scope.admit();
    if (!context)
      throw new Error(
        'Browser memory identity is unavailable. Reload before sending again.'
      );
    return {
      ...options,
      context: { ...(options?.context as Record<string, unknown>), ...context },
    };
  }
  override async *stream(
    assistantId: string,
    threadId: string | null,
    payload: unknown,
    signal: AbortSignal,
    options?: LangGraphSubmitOptions
  ): AsyncIterable<StreamEvent> {
    const admitted = this.scoped(options);
    yield* super.stream(assistantId, threadId, payload, signal, admitted);
  }
  override createQueuedRun(
    assistantId: string,
    threadId: string,
    payload: unknown,
    signal: AbortSignal,
    options?: LangGraphSubmitOptions
  ) {
    return super.createQueuedRun(
      assistantId,
      threadId,
      payload,
      signal,
      this.scoped(options)
    );
  }
}
memory-agent-gate.ts
import { computed } from '@angular/core';
import type { LangGraphAgent } from '@threadplane/langgraph';
import { MemoryScope } from './memory-scope';
/** One gate drives both the ordinary Chat composer and older automatic suggestions.
 * A denied submission resolves locally so void composer handlers cannot reject. */
export function guardMemoryAgent<T>(
  agent: LangGraphAgent<T>,
  scope: MemoryScope
): LangGraphAgent<T> {
  return {
    ...agent,
    isInputBlocked: computed(
      () => scope.snapshot().status !== 'ready' || !!agent.isInputBlocked?.()
    ),
    submit: async (input, options) => {
      if (!scope.admit()) return;
      await agent.submit(input, options);
    },
  };
}
app.config.ts
import { injectCockpitRuntimeConnection } from '@threadplane/cockpit-telemetry';
import { ApplicationConfig, inject, signal } from '@angular/core';
import { MemoryScope } from './memory-scope';
import { MemoryTransport } from './memory-transport';
import {
  provideAgent,
  ɵLANGGRAPH_RUNTIME_OPERATION_REPORTER,
} from '@threadplane/langgraph';
 
export const appConfig: ApplicationConfig = {
  providers: [
    provideAgent(() => {
      const connection = injectCockpitRuntimeConnection();
      if (connection.adapter !== 'langgraph') {
        throw new Error('incompatible runtime');
      }
      const scope = inject(MemoryScope);
      const threadId = signal<string | null>(null);
      const onThreadId = (id: string) => threadId.set(id);
      const clientOptions = { ...connection.clientOptions, maxRetries: 0 };
      const transport = new MemoryTransport(
        connection.apiUrl,
        scope,
        onThreadId,
        clientOptions,
        inject(ɵLANGGRAPH_RUNTIME_OPERATION_REPORTER, { optional: true }) ??
          undefined
      );
      return {
        apiUrl: connection.apiUrl,
        assistantId: connection.assistantId,
        clientOptions,
        threadId,
        onThreadId,
        transport,
      };
    }),
  ],
};

Fresh observations

The graph reads the actual store initially, before every model call and at terminal completion. A missing file is {}; a present empty file is ''. Reads validate strict UTF-8 and at most 65,536 bytes after downloading and materializing the file. The bound does not limit store allocation. The pinned middleware strips HTML comments and trailing whitespace from the model prompt; custom events, raw checkpoints and the React panel retain literal contents.

graph.py
"""Agent-authored cross-thread Memory with validated browser scope and fresh reads.
 
Custom events announce new reads. API values filtered through output_channels
omit private Memory keys; custom events and raw checkpoints retain them.
Private I/O schema attributes are not authentication or a secrecy guarantee.
"""
from pathlib import Path
from deepagents import create_deep_agent
from deepagents.backends import StoreBackend
from langchain_openai import ChatOpenAI
from src.memory_observation import (
    MEMORY_EVENT, MEMORY_FILE, MEMORY_NAMESPACE, MemoryContext,
    MemoryObservationMiddleware, memory_namespace,
)
 
PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
 
def build_memory_agent():
    """Bind a thread once and refresh actual store contents at each phase."""
    backend = StoreBackend(namespace=memory_namespace)
    return create_deep_agent(
        model=ChatOpenAI(model="gpt-4.1", temperature=0),
        system_prompt=(PROMPTS_DIR / "memory.md").read_text(),
        backend=backend,
        memory=[MEMORY_FILE],
        context_schema=MemoryContext,
        middleware=[MemoryObservationMiddleware(backend)],
    )
 
graph = build_memory_agent()
memory_observation.py
"""Fresh browser-scoped store observations; scope partitioning is not authentication.
 
The byte bound applies after backend materialization, not to store allocation.
Private attributes omit I/O schemas and API values filtered by output_channels.
Custom events and raw checkpoints retain them; this is not a secrecy guarantee.
"""
from typing import Annotated, NotRequired, TypedDict
from uuid import UUID
from langchain.agents.middleware import AgentMiddleware, AgentState
from langchain.agents.middleware.types import PrivateStateAttr
from langgraph.config import get_stream_writer
 
MEMORY_FILE = '/memories/AGENTS.md'
MEMORY_EVENT = 'deep_agents.memory'
MEMORY_NAMESPACE = ('cockpit', 'deep-agents-memory', 'browser-v1')
MAX_MEMORY_BYTES = 65_536
 
class MemoryContext(TypedDict):
    memory_scope: str
 
class MemoryObservationState(AgentState):
    memory_scope_owner: NotRequired[Annotated[str, PrivateStateAttr]]
    memory_contents: NotRequired[Annotated[dict[str, str], PrivateStateAttr]]
 
def validate_scope(context):
    """Require an exact context and canonical lowercase UUID v4."""
    if not isinstance(context, dict) or set(context) != {'memory_scope'}:
        raise ValueError('Invalid memory scope context')
    scope = context['memory_scope']
    if not isinstance(scope, str) or len(scope) != 36:
        raise ValueError('Invalid memory scope context')
    try:
        parsed = UUID(scope)
    except ValueError:
        raise ValueError('Invalid memory scope context') from None
    if parsed.version != 4 or str(parsed) != scope:
        raise ValueError('Invalid memory scope context')
    return scope
 
def memory_namespace(runtime):
    return (*MEMORY_NAMESPACE, validate_scope(getattr(runtime, 'context', None)))
 
class MemoryObservationError(RuntimeError):
    """An actual read could not establish a fresh observation."""
 
class MemoryObservationMiddleware(AgentMiddleware):
    state_schema = MemoryObservationState
    def __init__(self, backend):
        self.backend = backend
    def _admit(self, state, runtime):
        scope = validate_scope(runtime.context)
        if 'memory_scope_owner' in state and state['memory_scope_owner'] != scope:
            raise ValueError('Memory scope differs from thread owner')
        return scope
    def _snapshot(self, responses):
        try:
            if len(responses) != 1 or responses[0].path != MEMORY_FILE:
                raise ValueError()
            response = responses[0]
            if response.error == 'file_not_found':
                return {}
            if response.error is not None or not isinstance(response.content, bytes):
                raise ValueError()
            if len(response.content) > MAX_MEMORY_BYTES:
                raise ValueError()
            return {MEMORY_FILE: response.content.decode('utf-8', errors='strict')}
        except Exception:
            raise MemoryObservationError('Memory observation unavailable') from None
    def _publish(self, contents, scope, phase):
        get_stream_writer()({'name': MEMORY_EVENT, 'data': {
            'memory_contents': contents, 'memory_scope_owner': scope, 'phase': phase,
        }})
        return {'memory_contents': contents, 'memory_scope_owner': scope}
    def _refresh(self, state, runtime, phase):
        scope = self._admit(state, runtime)
        try:
            contents = self._snapshot(self.backend.download_files([MEMORY_FILE]))
        except Exception:
            raise MemoryObservationError('Memory observation unavailable') from None
        return self._publish(contents, scope, phase)
    async def _arefresh(self, state, runtime, phase):
        scope = self._admit(state, runtime)
        try:
            contents = self._snapshot(await self.backend.adownload_files([MEMORY_FILE]))
        except Exception:
            raise MemoryObservationError('Memory observation unavailable') from None
        return self._publish(contents, scope, phase)
    def before_agent(self, state, runtime):
        return self._refresh(state, runtime, 'initial')
    async def abefore_agent(self, state, runtime):
        return await self._arefresh(state, runtime, 'initial')
    def before_model(self, state, runtime):
        return self._refresh(state, runtime, 'model')
    async def abefore_model(self, state, runtime):
        return await self._arefresh(state, runtime, 'model')
    def after_agent(self, state, runtime):
        return self._refresh(state, runtime, 'terminal')
    async def aafter_agent(self, state, runtime):
        return await self._arefresh(state, runtime, 'terminal')

The named deep_agents.memory custom event contains memory_contents, memory_scope_owner and phase. Angular's store observed during this run comes from this live event. Historical state/checkpoint reads are a distinct fallback. A live observation is read-time evidence, not durability, and checkpoints do not promise that the store remains current.

The graph's private input/output schema annotations omit Memory keys from API values filtered through graph.output_channels. Custom events, checkpoint events and raw checkpoints retain those keys. This is not a secrecy guarantee; unfiltered local graph streams can include private values.

memory.component.ts
import { Component, computed, inject } from '@angular/core';
import {
  ChatComponent,
  ChatWelcomeSuggestionComponent,
} from '@threadplane/chat';
import { ExampleChatLayoutComponent } from '@threadplane/example-layouts';
import { injectAgent } from '@threadplane/langgraph';
import { MemoryScope } from './memory-scope';
import { guardMemoryAgent } from './memory-agent-gate';
 
/** Name of the custom stream event the graph's visibility middleware emits. */
const MEMORY_EVENT = 'deep_agents.memory';
 
interface MemoryFile {
  path: string;
  contents: string;
  lines: string[];
}
 
const SUGGESTIONS = [
  // values match cockpit/deep-agents/memory/angular/e2e/da-memory.spec.ts prompts.
  {
    label: 'Tell it about your operation',
    value:
      'I fly a Citation CJ3 out of KASE, and I always want briefings in bullet points. Please remember that.',
    description: 'The agent edits its own memory file.',
  },
  {
    label: 'Start over and ask what it knows',
    value: 'What do you already know about my operation?',
    description: 'Reload first: new threads use the saved browser identity.',
  },
] as const;
 
/** The graph reads a browser-scoped StoreBackend and publishes scoped custom
 * observations before model work and at terminal read time. PrivateStateAttr
 * hides schema fields; compiled values can still include them, while the installed
 * API may omit them via output_channels. State/checkpoint observations are historical
 * read-time evidence, not a guarantee that the store remains current or durable. */
@Component({
  selector: 'app-da-memory',
  standalone: true,
  imports: [
    ChatComponent,
    ChatWelcomeSuggestionComponent,
    ExampleChatLayoutComponent,
  ],
  template: `
    <example-chat-layout sidebarWidth="22rem">
      <chat main [agent]="agent" class="flex-1 min-w-0">
        <div chatWelcomeSuggestions>
          @for (s of suggestions; track s.value) {
            <chat-welcome-suggestion
              [label]="s.label"
              [value]="s.value"
              [description]="s.description"
              (selected)="send($event)"
            />
          }
        </div>
      </chat>
      <!-- #region memory-panel -->
      <div sidebar class="panel">
        <h3 class="cap">Agent Memory</h3>
        <p class="source" data-testid="memory-source" [attr.data-source]="memorySource()">
          {{
            memorySource() === 'live'
              ? 'store observed during this run'
              : memorySource() === 'checkpoint'
                ? 'historical state/checkpoint read'
                : memorySource() === 'unavailable'
                  ? 'memory observation unavailable'
                  : 'no source yet'
          }}
        </p>
        @if (scopeWarning()) {
          <p role="alert" data-testid="memory-scope-warning">{{ scopeWarning() }}</p>
        }
        @if (memoryFiles().length === 0 && (memorySource() === 'live' || memorySource() === 'checkpoint')) {
          <p class="empty">No memory file at this observation's read time</p>
        }
        @for (file of memoryFiles(); track file.path) {
          <div class="mem" data-testid="memory-file" [attr.data-path]="file.path">
            <span class="mem__path">{{ file.path }}</span>
            @for (line of file.lines; track $index) {
              <p class="mem__line" data-testid="memory-line">{{ line }}</p>
            }
          </div>
        }
        <p class="hint">
          This panel shows the scoped store at an observed read time. A new conversation uses
          the same browser identity while its storage is available. Observations do not promise durability.
        </p>
      </div>
      <!-- #endregion -->
    </example-chat-layout>
  `,
  styles: [
    `
      .panel {
        display: flex;
        flex-direction: column;
        gap: 0.75rem;
        padding: 1rem;
        background: var(--tplane-chat-bg);
        color: var(--tplane-chat-text);
      }
 
      .cap {
        margin: 0;
        color: var(--tplane-chat-text-muted);
        font-size: var(--tplane-chat-font-size-xs);
        font-weight: 700;
        letter-spacing: 0.12em;
        text-transform: uppercase;
      }
 
      .source {
        margin: 0;
        color: var(--tplane-chat-text-muted);
        font-family: var(--tplane-chat-font-mono);
        font-size: var(--tplane-chat-font-size-xs);
      }
 
      .empty {
        margin: 0;
        color: var(--tplane-chat-text-muted);
        font-size: var(--tplane-chat-font-size-sm);
        font-style: italic;
      }
 
      .mem {
        display: flex;
        flex-direction: column;
        gap: 0.125rem;
        padding: 0.5rem;
        border-radius: var(--tplane-chat-radius-card);
        background: var(--tplane-chat-surface-alt);
      }
 
      .mem__path {
        color: var(--tplane-chat-text-muted);
        font-family: var(--tplane-chat-font-mono);
        font-size: var(--tplane-chat-font-size-xs);
      }
 
      .mem__line {
        margin: 0;
        color: var(--tplane-chat-text);
        font-size: var(--tplane-chat-font-size-sm);
      }
 
      .hint {
        margin: 0;
        color: var(--tplane-chat-text-muted);
        font-size: var(--tplane-chat-font-size-xs);
        line-height: var(--tplane-chat-line-height);
      }
    `,
  ],
})
export class MemoryComponent {
  private readonly scope = inject(MemoryScope);
  protected readonly agent = guardMemoryAgent(injectAgent(), this.scope);
  protected readonly scopeWarning = computed(() => {
    const snapshot = this.scope.snapshot();
    return snapshot.status === 'blocked' ? snapshot.warning : null;
  });
  protected readonly suggestions = SUGGESTIONS;
 
  // #region memory-sources
  // The adapter clears custom events at run/thread boundaries. While loading,
  // absence of a new custom event must not promote old checkpoint/state data.
  private readonly observation = computed<{
    source: 'live' | 'checkpoint' | 'none' | 'unavailable';
    contents?: Record<string, string>;
  }>(() => {
    if (this.agent.error()) return { source: 'unavailable' };
    const owner = this.scope.scope;
    if (!owner) return { source: 'unavailable' };
    for (const event of [...this.agent.customEvents()].reverse()) {
      if (event.name !== MEMORY_EVENT) continue;
      const data = plainRecord(event.data);
      if (
        !data ||
        Object.keys(data).length !== 3 ||
        !['memory_contents', 'memory_scope_owner', 'phase'].every((k) =>
          Object.hasOwn(data, k)
        ) ||
        data.memory_scope_owner !== owner ||
        !['initial', 'model', 'terminal'].includes(data.phase as string)
      )
        return { source: 'unavailable' };
      const contents = captureMemoryMap(data.memory_contents);
      return contents
        ? { source: 'live', contents }
        : { source: 'unavailable' };
    }
    if (this.agent.isLoading()) return { source: 'none' };
    const value = plainRecord(this.agent.value());
    if (!value || !Object.hasOwn(value, 'memory_contents'))
      return { source: 'none' };
    if (value.memory_scope_owner !== owner) return { source: 'unavailable' };
    const contents = captureMemoryMap(value.memory_contents);
    return contents
      ? { source: 'checkpoint', contents }
      : { source: 'unavailable' };
  });
  // #endregion
 
  // #region memory-source-label
  protected readonly memorySource = computed(() => this.observation().source);
  // #endregion
  protected readonly memoryFiles = computed<MemoryFile[]>(() =>
    Object.entries(this.observation().contents ?? {}).map(
      ([path, contents]) => ({
        path,
        contents,
        // Retain the older Angular line presentation; backing text stays literal.
        lines: contents
          .split('\n')
          .map((line) => line.trim())
          .filter((line) => line.length > 0),
      })
    )
  );
  protected send(text: string): void {
    void this.agent.submit({ message: text });
  }
}
 
function plainRecord(value: unknown): Record<string, unknown> | undefined {
  try {
    if (!value || typeof value !== 'object' || Array.isArray(value))
      return undefined;
    const prototype = Object.getPrototypeOf(value);
    if (prototype !== null && prototype !== Object.prototype) return undefined;
    const descriptors = Object.getOwnPropertyDescriptors(value);
    if (
      Reflect.ownKeys(value).some(
        (key) =>
          typeof key !== 'string' ||
          !descriptors[key].enumerable ||
          !Object.hasOwn(descriptors[key], 'value')
      )
    )
      return undefined;
    return value as Record<string, unknown>;
  } catch {
    return undefined;
  }
}
/** Exact one-path, bounded Unicode scalar data, copied without trimming. */
function captureMemoryMap(input: unknown): Record<string, string> | undefined {
  try {
    const record = plainRecord(input);
    if (!record) return undefined;
    const keys = Object.keys(record);
    if (keys.length > 1 || keys.some((key) => key !== '/memories/AGENTS.md'))
      return undefined;
    if (!keys.length) return Object.freeze({});
    const contents = record['/memories/AGENTS.md'];
    if (typeof contents !== 'string' || contents.length > 65536)
      return undefined;
    for (let i = 0; i < contents.length; i++) {
      const unit = contents.charCodeAt(i);
      if (unit >= 0xd800 && unit <= 0xdbff) {
        const next = contents.charCodeAt(++i);
        if (!(next >= 0xdc00 && next <= 0xdfff)) return undefined;
      } else if (unit >= 0xdc00 && unit <= 0xdfff) return undefined;
    }
    if (new TextEncoder().encode(contents).length > 65536) return undefined;
    return Object.freeze({ '/memories/AGENTS.md': contents });
  } catch {
    return undefined;
  }
}

React confirmation and restoration

React labels custom evidence store observed during this run. confirmed checkpoint at read time and Response saved. require native success plus matching current root state, a fresh newest history head, an exact checkpoint and actual native transcript/load. Only absent private Memory keys may be omitted from live values; ordinary values and the complete tool transcript must match. Loaded values must match fully.

Saved conversations restore an exact retained historical checkpoint through reads only. A failed initial, model or terminal read, partial failed observation, oversized file or failed checkpoint read cannot confirm Saved. A terminal read failure after a successful write can leave the write persisted: confirmation is cleared and there is no rollback. Stop cancels local delivery while backend work may continue. New invalidates stale results. Conversations can update the same file at the same time; concurrent conversations are not serialized.

React uses ifExists: 'raise' and maxRetries: 0, with no automatic Retry, replay or Resume. Its package remains a private source-build preview.

Source and verification

Code includes the complete Angular scope, transport, authoring gate and UI, or the actual native React application and authority sources, together with both shared Python sources and the canonical Memory prompt. Browser fixtures and verification controls are excluded from public Code and production builds.

npx nx build cockpit-deep-agents-memory-angular
npx nx e2e cockpit-deep-agents-memory-angular
npx nx build cockpit-deep-agents-memory-react
npx nx e2e cockpit-deep-agents-memory-react

Local browser tests use the actual compiled graph, installed SDK and a deterministic offline model. The deployed example uses the shared cockpit development deployment.

Looking for something specific?