Page actions

React Deep Agents Memory preview

Teach an agent a fact, then see whether it can recall that fact in another interaction. Long-lived memory needs an identity and storage scope beyond the composer or one component mount.

The native React APIs on this page are experimental, private source-build packages. Run the repository example to use them; they are not an npm installation recipe.

Make the memory scope visible

The example separates browser identity, conversation state, and the backend memory observation. A message saying “I remember” is not proof that a stored fact was read; the application displays the relevant read evidence independently. The source walkthrough shows when the graph reads pinned instructions and when it consults the cross-conversation memory scope.

The React conversation walkthrough connects this capability to requests, tools, UI state, and saved history.

Teach, then recall

Choose Run, type a message and choose Send or press Enter. Teach and Recall fill the draft without sending. Opening the example, changing mode or frontend, navigating history, filling a draft and choosing New conversation do not create a thread or author a run. Mode and history navigation preserve the mounted unsent draft; reload and New clear it.

Only explicit Send lazily creates an owned thread with ifExists: raise, using da-memory, maxRetries: 0 and no automatic Retry or Resume. Followups reuse that thread. New and reload use a fresh thread on the next Send while retaining the captured browser scope.

app.tsx
// eslint-disable-next-line @nx/enforce-module-boundaries -- The isolated consumer copies this internal example layout.
import { ExampleFooter, ExampleLayout } from '../../../../shared/react-example';
import { useCallback, useEffect, useState } from 'react';
import { useAgent } from '@threadplane/react';
import {
  ChatInput,
  MessageList,
  ToolObservation,
  Reasoning,
  Citations,
} from '@threadplane/react/chat';
import { Markdown } from '@threadplane/react/markdown';
import type { MessageRow } from '@threadplane/content/messages';
import { MEMORY_PATH, type MemoryProjection } from './memory-projection';
import { copyData, LIMITS, type Authority } from './authority';
import {
  createConnectedApplication,
  type memoryConnection,
} from './connection';
 
function literal(value: unknown): string {
  try {
    const copied = copyData(value);
    const text =
      typeof copied === 'string'
        ? copied
        : JSON.stringify(copied, null, 2) ?? '';
    return text.length <= LIMITS.fieldText
      ? text
      : 'Observation text unavailable: display limit exceeded.';
  } catch {
    return 'Observation text unavailable.';
  }
}
 
/** Application owns row Markdown snapshots and tool/result association. */
export function renderMessage(
  row: MessageRow,
  authority: Authority = { kind: 'unconfirmed' }
) {
  if (row.role === 'tool') return null;
  const label =
    row.role === 'user'
      ? 'You'
      : row.role === 'assistant'
      ? 'Assistant'
      : 'System';
  return (
    <article
      aria-label={label}
      className={`tp-chat-message tp-chat-message--${row.role}`}
    >
      <p className="tp-chat-message__role">{label}</p>
      {row.role === 'assistant' && row.reasoning && (
        <Reasoning snapshot={row.reasoning} />
      )}
      <Markdown snapshot={row.markdown} />
      {row.toolCalls.map((call) => {
        const error =
          call.status === 'error' ||
          (authority.kind === 'terminal' &&
            authority.rawToolStatuses[call.id] === 'error');
        return (
          <div
            key={call.id}
            className="tool-observation"
            data-call-id={call.id}
          >
            <p>
              {error
                ? 'Tool result: error'
                : `Observed delivery: ${call.status}`}
            </p>
            <ToolObservation
              name={call.name}
              argumentsText={literal(call.args)}
              resultText={
                call.status === 'complete'
                  ? literal(call.result)
                  : call.status === 'error'
                  ? literal(call.error)
                  : undefined
              }
              label={error ? 'Observed tool error' : 'Observed tool delivery'}
            />
          </div>
        );
      })}
      {row.role === 'assistant' && row.message.citations && (
        <Citations citations={row.message.citations} />
      )}
    </article>
  );
}
 
export function MemoryPanel({ memory }: { memory: MemoryProjection }) {
  return (
    <section aria-label="Agent Memory" className="memory-panel">
      <h2>Agent Memory</h2>
      <p data-testid="memory-source" data-source={memory.kind}>
        {memory.kind === 'live'
          ? 'store observed during this run'
          : memory.kind === 'checkpoint'
          ? 'confirmed checkpoint at read time'
          : memory.kind === 'unavailable'
          ? 'memory observation unavailable'
          : 'no source yet'}
      </p>
      {(memory.kind === 'live' || memory.kind === 'checkpoint') &&
        (memory.file === 'missing' ? (
          <p>No memory file at this observation’s read time.</p>
        ) : (
          <div data-testid="memory-file">
            <h3>
              <code>{MEMORY_PATH}</code>
            </h3>
            {memory.contents === '' && (
              <p>The memory file is present and empty.</p>
            )}
            <pre>{memory.contents}</pre>
          </div>
        ))}
      <p>
        This panel shows one browser identity’s store at an observed read time.
        New conversations share that identity while browser storage is
        available. Checkpoints are historical; observations do not promise
        durability or that the store is current. Concurrent conversations are
        not serialized.
      </p>
    </section>
  );
}
 
export function MemoryDemo({
  connection,
  onReady,
}: {
  connection: ReturnType<typeof memoryConnection>;
  onReady: () => void;
}) {
  const [application] = useState(() => createConnectedApplication(connection));
  const [draft, setDraft] = useState('');
  const snapshot = useAgent(application);
  const renderRow = useCallback(
    (row: MessageRow) => renderMessage(row, snapshot.authority),
    [snapshot.authority]
  );
  useEffect(() => {
    onReady();
  }, [onReady]);
  useEffect(
    () => () => {
      void application.dispose();
    },
    [application]
  );
  useEffect(() => {
    setDraft('');
  }, [snapshot.viewGeneration]);
  const status = snapshot.busy
    ? snapshot.activity === 'confirming'
      ? 'Confirming saved conversation…'
      : snapshot.activity === 'restoring'
      ? 'Restoring conversation…'
      : snapshot.activity === 'creating'
      ? 'Creating conversation…'
      : 'Receiving response…'
    : !snapshot.canSubmit
    ? snapshot.scopeWarning
      ? 'Sending is unavailable. Reload after checking browser storage.'
      : 'Start a new conversation to continue.'
    : snapshot.outcome === 'success' &&
      snapshot.authority.kind === 'terminal' &&
      snapshot.memory.kind === 'checkpoint'
    ? 'Response saved.'
    : 'Ready.';
  return (
    <ExampleLayout className="memory-demo">
      <header>
        <div>
          <h1>Deep Agents Memory</h1>
        </div>
        <button
          type="button"
          onClick={() => {
            setDraft('');
            void application.newConversation();
          }}
        >
          New conversation
        </button>
      </header>
      <p>
        Teach the agent about your operation, then start a new conversation and
        ask what it remembers. Stop cancels local delivery; backend work may
        continue.
      </p>
      <div className="seed-prompts" aria-label="Example prompts">
        <button
          type="button"
          disabled={snapshot.busy || !snapshot.canSubmit}
          onClick={() => {
            const current = application.getSnapshot();
            if (current.canSubmit && !current.busy)
              setDraft(
                'I fly a Citation CJ3 out of KASE, and I always want briefings in bullet points. Please remember that.'
              );
          }}
        >
          Teach
        </button>
        <button
          type="button"
          disabled={snapshot.busy || !snapshot.canSubmit}
          onClick={() => {
            const current = application.getSnapshot();
            if (current.canSubmit && !current.busy)
              setDraft('What do you already know about my operation?');
          }}
        >
          Recall
        </button>
      </div>
      <label>
        Saved conversations{' '}
        <select
          aria-label="Saved conversations"
          value={snapshot.threadId ?? ''}
          disabled={snapshot.busy}
          onChange={(event) => {
            if (!event.target.value) return;
            setDraft('');
            void application.selectConversation(event.target.value);
          }}
        >
          <option value="">Choose a saved conversation</option>
          {snapshot.history.map((entry) => (
            <option key={entry.id} value={entry.id}>
              {entry.label}
            </option>
          ))}
        </select>
      </label>
      <div className="memory-workspace">
        <div className="memory-chat">
          <MessageList
            rows={snapshot.rows}
            renderMessage={renderRow}
            label="Conversation"
          />
          {snapshot.scopeWarning && (
            <p role="alert" data-testid="memory-scope-warning">
              {snapshot.scopeWarning}
            </p>
          )}
          <ExampleFooter>
            {snapshot.error && <p role="alert">{snapshot.error}</p>}
            <ChatInput
              key={snapshot.viewGeneration}
              label="Message"
              value={draft}
              onValueChange={setDraft}
              placeholder="Teach the agent or ask what it remembers…"
              hint="Enter to send. Shift+Enter adds a new line."
              busy={snapshot.busy}
              disabled={!snapshot.canSubmit && !snapshot.busy}
              onStop={snapshot.busy ? () => void application.stop() : undefined}
              onSubmit={(text) => {
                const current = application.getSnapshot();
                if (!current.canSubmit || current.busy || !text.trim())
                  return false;
                void application.submit(text);
                return true;
              }}
            />
            <p role="status">{status}</p>
          </ExampleFooter>
        </div>
        <aside className="memory-sidebar" aria-label="Memory sidebar">
          <MemoryPanel memory={snapshot.memory} />
        </aside>
      </div>
    </ExampleLayout>
  );
}
connection.ts
import { Client } from '@langchain/langgraph-sdk';
import { createSession } from '@threadplane/langgraph';
// eslint-disable-next-line @nx/enforce-module-boundaries -- Neutral bridge copied by isolated build.
import type { RuntimeBridgeConfiguration } from '../../../../../libs/cockpit-runtime-bridge/src/index';
import {
  createMemoryApplication,
  type MemoryClient,
  type MemorySession,
} from './application';
import { copyData, record, text, LIMITS, type Checkpoint } from './authority';
import { validScope } from './browser-scope';
export interface MemoryConnection {
  readonly apiUrl: string;
  readonly headers: Record<string, string>;
}
export function memoryConnection(
  configuration: RuntimeBridgeConfiguration
): MemoryConnection {
  if (
    configuration.status === 'error' ||
    (configuration.status === 'configured' &&
      configuration.target.kind === 'ag-ui')
  )
    throw Error('Runtime configuration failed.');
  const target =
    configuration.status === 'configured' &&
    configuration.target.kind === 'langsmith'
      ? configuration.target
      : null;
  return Object.freeze({
    apiUrl: target?.apiUrl ?? new URL('/api', window.location.origin).href,
    headers: Object.freeze(
      target ? { 'x-api-key': target.apiKey } : ({} as Record<string, string>)
    ),
  });
}
type Transport = NonNullable<Parameters<typeof createSession>[0]['transport']>;
type Event = ReturnType<Transport['stream']> extends AsyncIterable<infer T>
  ? T
  : never;
/** Routing comes only from the SDK event name and cursor, never payload fields. */
function normalize(event: string, data: unknown, id: unknown): Event {
  const [base, ...namespace] = event.split('|');
  const route = {
    type: event as Event['type'],
    ...(namespace.length ? { namespace } : {}),
    sseId: typeof id === 'string' ? id : undefined,
  };
  if (
    base === 'messages' &&
    Array.isArray(data) &&
    data.length === 2 &&
    record(data[1])
  )
    return { ...route, messages: [data[0]], messageMetadata: data[1], data };
  if (
    (base === 'messages' || base.startsWith('messages/')) &&
    Array.isArray(data)
  )
    return { ...route, messages: data, data };
  if (record(data))
    return {
      ...data,
      ...route,
      ...(Object.hasOwn(data, 'namespace') && !namespace.length
        ? { namespace: undefined }
        : {}),
      data,
    };
  return { ...route, data };
}
export function createMemoryClient(input: MemoryConnection): MemoryClient {
  const connection = Object.freeze({
    apiUrl: input.apiUrl,
    headers: Object.freeze({ ...input.headers }),
  });
  const sdk = new Client({
    apiUrl: connection.apiUrl,
    apiKey: null,
    defaultHeaders: connection.headers,
    callerOptions: { maxRetries: 0 },
  });
  const known = new Set<string>(),
    observers = new Map<string, (event: unknown) => void>();
  function owned(id: string | null, signal: AbortSignal): asserts id is string {
    if (!id || !known.has(id) || signal.aborted)
      throw Error('Conversation unavailable.');
  }
  const readHistory: MemoryClient['readHistory'] = async (id, signal) => {
    owned(id, signal);
    const result = await sdk.threads.getHistory(id, { limit: 1, signal });
    owned(id, signal);
    return result;
  };
  const readCheckpoint: MemoryClient['readCheckpoint'] = async (
    requested,
    signal
  ) => {
    const v = copyData(requested, false);
    if (
      !record(v) ||
      !text(v.thread_id) ||
      v.checkpoint_ns !== '' ||
      !text(v.checkpoint_id) ||
      v.checkpoint_id.length > LIMITS.fieldText ||
      Object.keys(v).some(
        (k) =>
          ![
            'thread_id',
            'checkpoint_ns',
            'checkpoint_id',
            'checkpoint_map',
          ].includes(k)
      ) ||
      (v.checkpoint_map !== undefined &&
        (!record(v.checkpoint_map) ||
          Object.keys(v.checkpoint_map).length !== 1 ||
          v.checkpoint_map[''] !== v.checkpoint_id))
    )
      throw Error('Checkpoint unavailable.');
    const cp = v as unknown as Checkpoint;
    owned(cp.thread_id, signal);
    const result = await sdk.threads.getState(
      cp.thread_id,
      cp as Parameters<typeof sdk.threads.getState>[1],
      { signal }
    );
    owned(cp.thread_id, signal);
    return result;
  };
  return {
    async createThread(signal) {
      if (signal.aborted) throw Error('Conversation unavailable.');
      const id = crypto.randomUUID();
      if (!validScope(id)) throw Error('Conversation unavailable.');
      const result = await sdk.threads.create({
        threadId: id,
        ifExists: 'raise',
        signal,
      });
      const captured = copyData(result, false);
      if (signal.aborted || !record(captured) || captured.thread_id !== id)
        throw Error('Conversation creation was not confirmed.');
      known.add(id);
      return id;
    },
    observeMemory(id, listener) {
      if (!known.has(id)) throw Error('Unknown conversation.');
      observers.set(id, listener);
      return () => {
        if (observers.get(id) === listener) observers.delete(id);
      };
    },
    sessionFactory(id) {
      if (!known.has(id)) throw Error('Unknown conversation.');
      const transport: Transport = {
        // Capture now, at physical invocation: a late event never adopts a later turn listener.
        stream(assistant, thread, payload, signal, options) {
          owned(thread, signal);
          const observe = observers.get(thread);
          const currentThread = thread;
          return (async function* () {
            try {
              for await (const packet of sdk.runs.stream(
                currentThread,
                assistant,
                {
                  input: payload as Record<string, unknown>,
                  context: options?.context,
                  config: options?.config,
                  metadata: options?.metadata,
                  streamMode: ['values', 'messages-tuple', 'updates', 'custom'],
                  streamSubgraphs: true,
                  signal,
                }
              )) {
                const event = normalize(packet.event, packet.data, packet.id);
                if (event.type === 'custom' && !event.namespace?.length)
                  observe?.(event.data);
                yield event;
              }
            } catch {
              throw Error('Conversation delivery unavailable.');
            }
          })();
        },
        async getHistory(thread, signal) {
          return (await readHistory(thread, signal)) as Awaited<
            ReturnType<NonNullable<Transport['getHistory']>>
          >;
        },
      };
      const session = createSession({
        assistantId: 'da-memory',
        threadId: id,
        transport,
      });
      try {
        const state = session.getSnapshot();
        if (
          ![
            'getSnapshot',
            'subscribe',
            'submit',
            'load',
            'stop',
            'dispose',
          ].every(
            (k) => typeof session[k as keyof typeof session] === 'function'
          ) ||
          ![
            state.messages,
            state.toolCalls,
            state.interrupts,
            state.subgraphs,
          ].every((x) => Array.isArray(x) && x.length === 0)
        )
          throw Error('Rejected session');
        return session as MemorySession;
      } catch {
        void Promise.resolve(session.dispose()).catch(() => undefined);
        throw Error('Runtime session is unavailable.');
      }
    },
    async readCurrent(id, signal) {
      owned(id, signal);
      const result = await sdk.threads.getState(id, undefined, { signal });
      owned(id, signal);
      return result;
    },
    readHistory,
    readCheckpoint,
  };
}
export function createConnectedApplication(connection: MemoryConnection) {
  return createMemoryApplication(createMemoryClient(connection));
}
application.ts
import type {
  AgentSession,
  AgentStatus,
  CompleteOutcome,
  ToolCall,
} from '@threadplane/core';
import {
  createMessageContent,
  type MessageRow,
} from '@threadplane/content/messages';
import {
  captureAuthority,
  captureBaseline,
  captureTurn,
  copyData,
  record,
  sameData,
  LIMITS,
  type Authority,
  type Baseline,
  type Turn,
  type Checkpoint,
  type NativeSnapshot,
} from './authority';
import { projectMemory, type MemoryProjection } from './memory-projection';
import { createBrowserScope, type BrowserScope } from './browser-scope';
import type { LangGraphRunOptions } from '@threadplane/langgraph';
export interface MemorySession
  extends Omit<AgentSession, 'getSnapshot' | 'submit'> {
  getSnapshot(): NativeSnapshot;
  submit(
    input: string,
    options?: LangGraphRunOptions
  ): Promise<CompleteOutcome>;
  load(options?: { readonly signal?: AbortSignal }): Promise<void>;
}
export interface MemoryClient {
  observeMemory(id: string, listener: (event: unknown) => void): () => void;
  createThread(signal: AbortSignal): Promise<string>;
  sessionFactory(id: string): MemorySession;
  readCurrent(id: string, signal: AbortSignal): Promise<unknown>;
  readHistory(id: string, signal: AbortSignal): Promise<readonly unknown[]>;
  readCheckpoint(checkpoint: Checkpoint, signal: AbortSignal): Promise<unknown>;
}
export interface MemorySnapshot {
  readonly threadId: string | null;
  readonly rows: readonly MessageRow[];
  readonly toolCalls: readonly ToolCall[];
  readonly memory: MemoryProjection;
  readonly scopeWarning: string | null;
  readonly authority: Authority;
  readonly activity:
    | 'idle'
    | 'creating'
    | 'running'
    | 'confirming'
    | 'restoring';
  readonly busy: boolean;
  readonly canSubmit: boolean;
  readonly outcome: CompleteOutcome | null;
  readonly error: string | null;
  readonly viewGeneration: number;
  readonly history: readonly { id: string; label: string }[];
}
const initial = (
  viewGeneration = 0,
  history: MemorySnapshot['history'] = []
): MemorySnapshot => ({
  threadId: null,
  rows: [],
  toolCalls: [],
  memory: { kind: 'none' },
  scopeWarning: null,
  authority: { kind: 'unconfirmed' },
  activity: 'idle',
  busy: false,
  canSubmit: true,
  outcome: null,
  error: null,
  viewGeneration,
  history,
});
export function createMemoryApplication(
  client: MemoryClient,
  scope: BrowserScope = createBrowserScope({
    storage: () => window.localStorage,
    uuid: () => crypto.randomUUID(),
    events: window,
  })
) {
  type Terminal = Extract<Authority, { kind: 'terminal' }>;
  type Saved = { authority: Terminal; native: NativeSnapshot; label: string };
  type Owner = {
    id: string;
    key: string;
    generation: string;
    session: MemorySession;
    content: ReturnType<typeof createMessageContent>;
    baseline: Baseline;
    turn?: Turn;
    nativeGeneration?: string;
    expectedText?: string;
    confirmed?: NativeSnapshot;
    authority?: Terminal;
    release?: () => void;
    releaseMemory?: () => void;
    observation?: unknown;
    closed: boolean;
    unsafe: boolean;
    loading: boolean;
    resolved: Map<string, unknown>;
    completed: Map<string, unknown>;
  };
  let snapshot = initial(),
    owner: Owner | undefined,
    operation: AbortController | undefined,
    disposed = false,
    serial = 0,
    conversationSerial = 0,
    disposal: Promise<void> | undefined;
  const listeners = new Set<() => void>(),
    catalog = new Map<string, Saved>(),
    cleanups = new Set<Promise<void>>();
  const publish = (update: Partial<MemorySnapshot>) => {
    if (disposed) return;
    snapshot = Object.freeze({ ...snapshot, ...update });
    listeners.forEach((f) => f());
  };
  const scopeState = scope.getSnapshot();
  const memoryScope = scopeState.scope;
  const updateScope = () => {
    const state = scope.getSnapshot();
    publish({
      scopeWarning: state.status === 'blocked' ? state.warning : null,
      canSubmit:
        state.status === 'ready' &&
        !snapshot.busy &&
        !owner?.unsafe &&
        snapshot.outcome !== 'aborted' &&
        snapshot.outcome !== 'error',
    });
  };
  const releaseScope = scope.subscribe(updateScope);
  updateScope();
  const history = () => [...catalog].map(([id, s]) => ({ id, label: s.label }));
  function bounded(work: () => void | Promise<void>) {
    let timer: ReturnType<typeof setTimeout>;
    return Promise.race([
      Promise.resolve()
        .then(work)
        .catch(() => undefined),
      new Promise<void>((r) => {
        timer = setTimeout(r, 2000);
      }),
    ]).finally(() => clearTimeout(timer));
  }
  function close(o: Owner) {
    if (o.closed) return Promise.resolve();
    o.closed = true;
    // Each owned resource gets its release attempt even if another cleanup fails.
    try {
      o.release?.();
    } catch {
      /* Continue releasing the session. */
    }
    try {
      o.releaseMemory?.();
    } catch {
      /* Continue all cleanup attempts. */
    }
    try {
      o.content.dispose();
    } catch {
      /* Continue releasing the session. */
    }
    const p = bounded(() => o.session.dispose());
    cleanups.add(p);
    void p.finally(() => cleanups.delete(p));
    return p;
  }
  function detach() {
    const o = owner;
    owner = undefined;
    return o ? close(o) : Promise.resolve();
  }
  function block(o?: Owner) {
    if (o) {
      o.unsafe = true;
      catalog.delete(o.id);
    }
    publish({
      canSubmit: false,
      memory: { kind: 'unavailable' },
      authority: { kind: 'unconfirmed' },
      outcome: 'error',
      error:
        'This conversation could not be confirmed. Start a new conversation to continue.',
      history: history(),
    });
  }
  function install(
    o: Owner,
    s: NativeSnapshot,
    authority: Authority,
    source: 'observed-root' | 'checkpoint'
  ) {
    const memory = projectMemory({
      owner: o.key,
      generation: o.generation,
      currentOwner: o.key,
      currentGeneration: o.generation,
      scope: memoryScope,
      source:
        source === 'checkpoint'
          ? 'checkpoint'
          : o.observation
          ? 'custom'
          : 'none',
      event: o.observation,
      authority,
    });
    const rows = o.content.project({
      status: s.status as AgentStatus,
      messages: s.messages,
      toolCalls: s.toolCalls,
    });
    publish({
      rows,
      toolCalls: s.toolCalls,
      memory:
        source === 'checkpoint' || o.observation ? memory : { kind: 'none' },
      authority,
    });
  }
  function read(o: Owner, valid: () => boolean): NativeSnapshot | undefined {
    if (!valid() || o.loading || o.unsafe) return;
    try {
      const source = o.session.getSnapshot(),
        s = copyData(source) as NativeSnapshot;
      if (
        !record(s) ||
        ![s.messages, s.toolCalls, s.interrupts, s.subgraphs].every(
          Array.isArray
        )
      )
        throw Error('Malformed snapshot');
      if (s.subgraphs.length) throw Error('Unsupported child state');
      if (
        o.confirmed &&
        (s.messages.length < o.confirmed.messages.length ||
          o.confirmed.messages.some(
            (m, i) =>
              !sameData(
                { ...m, delivery: undefined },
                { ...s.messages[i], delivery: undefined }
              )
          ) ||
          o.confirmed.toolCalls.some(
            (t) =>
              !sameData(
                t,
                s.toolCalls.find((x) => x.id === t.id)
              )
          ))
      )
        throw Error('Changed prior baseline');
      if (
        o.expectedText !== undefined &&
        s.messages.length > o.baseline.messages.length
      ) {
        const h = s.messages[o.baseline.messages.length];
        if (
          h.role !== 'user' ||
          h.content !== o.expectedText ||
          !h.delivery?.generation ||
          s.messages
            .slice(o.baseline.messages.length + 1)
            .some((m) => m.role === 'user')
        )
          throw Error('Unowned human');
        if (!o.turn) {
          o.turn = captureTurn(o.baseline, {
            memoryScope: memoryScope!,
            owner: o.key,
            generation: o.generation,
            threadId: o.id,
            humanId: h.id,
            humanContent: o.expectedText,
          });
          o.nativeGeneration = h.delivery.generation;
          if (!o.turn) throw Error('Unavailable turn');
        }
        if (
          s.messages
            .slice(o.baseline.messages.length)
            .some((m) => m.delivery?.generation !== o.nativeGeneration)
        )
          throw Error('Foreign generation');
      }
      if (o.turn) {
        const h = s.messages[o.baseline.messages.length];
        if (
          !h ||
          h.id !== o.turn.humanId ||
          h.delivery.generation !== o.nativeGeneration
        )
          throw Error('Foreign generation');
      }
      for (const m of s.messages) {
        const semantic = { ...m, delivery: undefined };
        if (o.completed.has(m.id) && !sameData(o.completed.get(m.id), semantic))
          throw Error('Changed complete message');
        if (m.delivery?.phase === 'complete') o.completed.set(m.id, semantic);
      }
      for (const t of s.toolCalls) {
        if (o.resolved.has(t.id) && !sameData(o.resolved.get(t.id), t))
          throw Error('Changed resolved tool');
        if (t.status === 'complete' || t.status === 'error')
          o.resolved.set(t.id, t);
      }
      if (
        [...o.completed.keys()].some(
          (id) => !s.messages.some((m) => m.id === id)
        ) ||
        [...o.resolved.keys()].some(
          (id) => !s.toolCalls.some((t) => t.id === id)
        )
      )
        throw Error('Missing completed work');
      if (!valid()) return;
      if (o.session.getSnapshot() !== source) throw Error('Changed source');
      install(o, s, { kind: 'unconfirmed' }, 'observed-root');
      return s;
    } catch {
      if (valid()) block(o);
      return;
    }
  }
  function make(id: string): Owner {
    const o: Owner = {
      id,
      key: String(++serial),
      generation: String(++serial),
      session: client.sessionFactory(id),
      content: createMessageContent(),
      baseline: { messages: [] },
      closed: false,
      unsafe: false,
      loading: false,
      resolved: new Map(),
      completed: new Map(),
    };
    try {
      o.release = o.session.subscribe(() => {
        const active = operation;
        if (active && !o.loading)
          read(
            o,
            () =>
              !disposed &&
              operation === active &&
              !active.signal.aborted &&
              owner === o &&
              !o.closed
          );
      });
    } catch {
      void close(o);
      throw Error('Runtime subscription unavailable.');
    }
    return o;
  }
  async function confirm(
    o: Owner,
    controller: AbortController,
    live: NativeSnapshot,
    valid: () => boolean,
    retained?: Saved
  ) {
    const observedState = copyData(
      await client.readCurrent(o.id, controller.signal),
      false
    );
    if (!valid()) return;
    const h = copyData(
      await client.readHistory(o.id, controller.signal),
      false
    );
    if (!valid()) return;
    if (!Array.isArray(h) || !record(h[0])) throw Error('Missing history');
    const requested = copyData(h[0].checkpoint, false) as Checkpoint;
    const loadedCheckpoint = copyData(
      await client.readCheckpoint(requested, controller.signal),
      false
    );
    if (!valid()) return;
    o.loading = true;
    await o.session.load({ signal: controller.signal });
    if (!valid()) return;
    const source = o.session.getSnapshot(),
      loaded = copyData(source) as NativeSnapshot;
    if (!Array.isArray(loaded.subgraphs) || loaded.subgraphs.length)
      throw Error('Unsupported child state');
    const authority = captureAuthority({
      memoryScope,
      turn: o.turn,
      owner: o.key,
      generation: o.generation,
      threadId: o.id,
      outcome: 'success',
      observedState,
      history: h,
      loadedCheckpoint,
      snapshot: retained ? loaded : live,
      loadedSnapshot: loaded,
    });
    if (
      authority.kind !== 'terminal' ||
      (retained &&
        (!sameData(authority.checkpoint, retained.authority.checkpoint) ||
          !sameData(authority.values, retained.authority.values)))
    )
      throw Error('Unconfirmed root');
    if (!valid()) return;
    if (o.session.getSnapshot() !== source) throw Error('Changed publication');
    const projection = projectMemory({
      owner: o.key,
      currentOwner: o.key,
      generation: o.generation,
      currentGeneration: o.generation,
      scope: memoryScope,
      source: 'checkpoint',
      authority,
    });
    if (projection.kind !== 'checkpoint')
      throw Error('Memory confirmation unavailable.');
    const baseline = captureBaseline({ messages: authority.messages });
    if (!baseline) throw Error('Missing baseline');
    o.confirmed = loaded;
    o.authority = authority;
    o.baseline = baseline;
    o.loading = false;
    o.turn = undefined;
    o.expectedText = undefined;
    const saved: Saved = {
      authority,
      native: loaded,
      label: catalog.get(o.id)?.label ?? `Conversation ${++conversationSerial}`,
    };
    // Bound the entire page-local catalog, including repeated raw/native text.
    // Oldest entries leave the selectable catalog rather than remaining usable.
    copyData(saved);
    catalog.delete(o.id);
    catalog.set(o.id, saved);
    while (catalog.size > LIMITS.entries) {
      const oldest = catalog.keys().next().value;
      if (oldest !== undefined) catalog.delete(oldest);
    }
    while (true) {
      try {
        copyData([...catalog.values()]);
        break;
      } catch {
        const oldest = catalog.keys().next().value;
        if (oldest === undefined || oldest === o.id) {
          catalog.delete(o.id);
          throw Error('Unavailable catalog budget');
        }
        catalog.delete(oldest);
      }
    }
    install(o, loaded, authority, 'checkpoint');
    if (!valid()) return;
    publish({
      canSubmit: scope.getSnapshot().status === 'ready',
      outcome: 'success',
      history: history(),
    });
  }
  async function submit(text: string) {
    if (disposed || snapshot.busy || !snapshot.canSubmit || !text.trim())
      return;
    if (text.length > LIMITS.fieldText) {
      block(owner);
      return;
    }
    const context = scope.admit();
    if (!context) {
      updateScope();
      return;
    }
    const controller = new AbortController();
    operation = controller;
    const current = () =>
      !disposed && operation === controller && !controller.signal.aborted;
    publish({
      busy: true,
      canSubmit: false,
      activity: owner ? 'running' : 'creating',
      outcome: null,
      error: null,
      memory: { kind: 'none' },
      authority: { kind: 'unconfirmed' },
    });
    try {
      if (!current()) return;
      if (!owner) {
        const id = await client.createThread(controller.signal);
        if (!current()) return;
        if (typeof id !== 'string' || !id || id.length > LIMITS.fieldText)
          throw Error('Unknown thread');
        const o = make(id);
        if (!current()) {
          void close(o);
          return;
        }
        owner = o;
        publish({ threadId: id, activity: 'running' });
      }
      const o = owner,
        valid = () => current() && owner === o && !o.closed;
      const admitted = scope.admit();
      if (!admitted || admitted.memory_scope !== context.memory_scope) {
        updateScope();
        return;
      }
      o.generation = String(++serial);
      o.observation = undefined;
      o.turn = undefined;
      o.nativeGeneration = undefined;
      o.expectedText = text;
      o.completed = new Map();
      o.resolved = new Map();
      const before = read(o, valid);
      if (!valid()) return;
      if (
        !before ||
        o.unsafe ||
        before.messages.length !== o.baseline.messages.length
      )
        throw Error('Unowned baseline');
      o.releaseMemory?.();
      const observedGeneration = o.generation;
      o.releaseMemory = client.observeMemory(o.id, (event) => {
        if (!valid() || o.generation !== observedGeneration || o.unsafe) return;
        // Only actual matching custom observations are a Memory source.
        let captured: unknown;
        try {
          captured = copyData(event, false);
        } catch {
          block(o);
          return;
        }
        if (!record(captured) || captured.name !== 'deep_agents.memory') return;
        const projection = projectMemory({
          owner: o.key,
          currentOwner: o.key,
          generation: observedGeneration,
          currentGeneration: o.generation,
          scope: memoryScope,
          source: 'custom',
          event: captured,
        });
        if (projection.kind === 'unavailable') {
          block(o);
          return;
        }
        o.observation = captured;
        publish({ memory: projection });
      });
      const outcome = await o.session.submit(text, {
        context,
        signal: controller.signal,
      });
      if (!valid()) return;
      const live = read(o, valid);
      if (!valid()) return;
      const human = live?.messages[o.baseline.messages.length];
      if (
        !live ||
        o.unsafe ||
        outcome !== 'success' ||
        !human ||
        human.role !== 'user' ||
        human.content !== text ||
        !human.delivery?.generation ||
        live.messages
          .slice(o.baseline.messages.length + 1)
          .some((m) => m.role === 'user')
      )
        throw Error('Unconfirmed delivery');
      o.turn = captureTurn(o.baseline, {
        memoryScope: memoryScope!,
        owner: o.key,
        generation: o.generation,
        threadId: o.id,
        humanId: human.id,
        humanContent: text,
      });
      o.nativeGeneration = human.delivery.generation;
      if (
        !o.turn ||
        live.messages
          .slice(o.baseline.messages.length)
          .some((m) => m.delivery.generation !== o.nativeGeneration)
      )
        throw Error('Foreign generation');
      publish({ activity: 'confirming' });
      await confirm(o, controller, live, valid);
    } catch {
      if (current()) block(owner);
    } finally {
      if (current()) {
        operation = undefined;
        publish({ busy: false, activity: 'idle' });
        updateScope();
      }
    }
  }
  async function selectConversation(id: string) {
    if (disposed || !catalog.has(id)) return;
    if (operation && snapshot.threadId === id) {
      await stop();
      return;
    }
    const saved = catalog.get(id);
    if (!saved) return;
    if (operation && snapshot.threadId) catalog.delete(snapshot.threadId);
    operation?.abort();
    operation = undefined;
    const cleanup = detach();
    const controller = new AbortController();
    operation = controller;
    const current = () =>
      !disposed && operation === controller && !controller.signal.aborted;
    snapshot = initial(snapshot.viewGeneration + 1, history());
    publish({
      busy: true,
      canSubmit: false,
      activity: 'restoring',
      threadId: id,
    });
    updateScope();
    try {
      await cleanup;
      if (!current()) return;
      const o = make(id);
      owner = o;
      o.loading = true;
      o.turn = captureTurn(saved.authority.turn.baseline, {
        ...saved.authority.turn,
        owner: o.key,
        generation: o.generation,
      });
      const valid = () => current() && owner === o && !o.closed;
      await confirm(o, controller, saved.native, valid, saved);
    } catch {
      if (current()) {
        catalog.delete(id);
        block(owner);
      }
    } finally {
      if (current()) {
        operation = undefined;
        publish({ busy: false, activity: 'idle' });
        updateScope();
      }
    }
  }
  async function stop() {
    if (disposed || !operation) return;
    operation.abort();
    operation = undefined;
    const o = owner;
    const invalidated = o?.id ?? snapshot.threadId;
    if (invalidated) catalog.delete(invalidated);
    const p = detach();
    publish({
      busy: false,
      canSubmit: false,
      activity: 'idle',
      outcome: 'aborted',
      error: null,
      memory: { kind: 'none' },
      authority: { kind: 'unconfirmed' },
      history: history(),
    });
    await Promise.all([p, bounded(() => o?.session.stop())]);
  }
  async function newConversation() {
    if (disposed) return;
    if (operation && snapshot.threadId) catalog.delete(snapshot.threadId);
    operation?.abort();
    operation = undefined;
    const p = detach();
    snapshot = initial(snapshot.viewGeneration + 1, history());
    publish({});
    updateScope();
    await p;
  }
  function dispose() {
    if (disposal) return disposal;
    disposed = true;
    operation?.abort();
    operation = undefined;
    void detach();
    try {
      releaseScope();
    } catch {
      /* Finish cleanup. */
    }
    try {
      scope.dispose();
    } catch {
      /* Finish cleanup. */
    }
    listeners.clear();
    return (disposal = Promise.all([...cleanups]).then(() => undefined));
  }
  return {
    getSnapshot: () => snapshot,
    subscribe: (f: () => void) => {
      if (disposed) return () => undefined;
      listeners.add(f);
      return () => listeners.delete(f);
    },
    submit,
    stop,
    newConversation,
    selectConversation,
    dispose,
  };
}
export type MemoryApplication = ReturnType<typeof createMemoryApplication>;

Browser identity and read-time evidence

The storage key threadplane.deep-agents-memory.scope.v1 holds a canonical UUIDv4. The app captures the persisted readback winner once and sends exactly context: . Storage is partitioned by origin and browser storage partition. Angular and React hosted on the same origin share the key. Unavailable or corrupt storage blocks Send before thread creation. Later replacement or deletion blocks future turns; an active turn may settle under its captured scope with a warning.

This is a logical partition, not authentication. Public state APIs fall outside this guarantee. There is no migration, read or reset of the historical global namespace.

browser-scope.ts
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();
    },
  });
}

Agent Memory preserves literal blank lines, Unicode and whitespace. Missing and a present empty file '' are distinct. Fresh reads occur initially, before every model call and at terminal completion. Strict UTF-8 validation accepts at most 65,536 bytes after download materialization; this does not bound store allocation. The pinned middleware strips HTML comments and trailing whitespace from the model prompt, so recalled prompt text differs from the literal observation.

store observed during this run is custom live evidence, not durability. confirmed checkpoint at read time and Response saved. require native success and equality between current root state, fresh newest history, exact checkpoint and actual native transcript/load. Only absent private Memory keys may be omitted from live values; ordinary values and the full tool transcript must match. Loaded values must match fully. API values filtered through the graph output channels omit private Memory keys, while checkpoint events, custom events and raw checkpoints retain them. This is not a secrecy guarantee.

memory-projection.ts
import { copyData, record, text, type Authority } from './authority';
import { validScope } from './browser-scope';
export const MEMORY_PATH = '/memories/AGENTS.md';
export const MAX_MEMORY_BYTES = 65536;
export type MemoryMap = Readonly<Record<string, string>>;
/** Strict Unicode scalar validation precedes TextEncoder: lone JS surrogates
 * cannot silently become replacement bytes. This bounds retained display data. */
export function captureMemoryMap(input: unknown): MemoryMap | undefined {
  try {
    const captured = copyData(input, false);
    if (!record(captured)) return undefined;
    const keys = Object.keys(captured);
    if (keys.length > 1 || keys.some((k) => k !== MEMORY_PATH))
      return undefined;
    if (!keys.length) return captured as MemoryMap;
    const value = captured[MEMORY_PATH];
    if (typeof value !== 'string' || value.length > MAX_MEMORY_BYTES)
      return undefined;
    for (let i = 0; i < value.length; i++) {
      const unit = value.charCodeAt(i);
      if (unit >= 0xd800 && unit <= 0xdbff) {
        const next = value.charCodeAt(++i);
        if (!(next >= 0xdc00 && next <= 0xdfff)) return undefined;
      } else if (unit >= 0xdc00 && unit <= 0xdfff) return undefined;
    }
    if (new TextEncoder().encode(value).length > MAX_MEMORY_BYTES)
      return undefined;
    return captured as MemoryMap;
  } catch {
    return undefined;
  }
}
export interface MemoryProjectionInput {
  readonly owner: string;
  readonly currentOwner: string;
  readonly generation: string;
  readonly currentGeneration: string;
  readonly scope: string;
  readonly source: 'none' | 'unavailable' | 'custom' | 'checkpoint';
  /** Actual separately observed root custom event, never fabricated native values. */
  readonly event?: unknown;
  readonly authority?: Authority;
}
export type MemoryProjection =
  | Readonly<{ kind: 'none' | 'unavailable' }>
  | Readonly<{
      kind: 'live' | 'checkpoint';
      file: 'missing' | 'present';
      contents?: string;
      scope: string;
      generation: string;
      phase?: 'initial' | 'model' | 'terminal';
      checkpointId?: string;
    }>;
const unavailable: MemoryProjection = Object.freeze({ kind: 'unavailable' });
/** Checkpoint evidence is historical: the file was observed at that run's read
 * time. Neither custom nor checkpoint evidence promises the store is current. */
export function projectMemory(input: unknown): MemoryProjection {
  try {
    const v = copyData(input);
    if (
      !record(v) ||
      !validScope(v.scope) ||
      !text(v.owner) ||
      !text(v.generation) ||
      v.owner !== v.currentOwner ||
      v.generation !== v.currentGeneration
    )
      return unavailable;
    if (v.source === 'none') return Object.freeze({ kind: 'none' });
    let contents: unknown,
      phase: 'initial' | 'model' | 'terminal' | undefined,
      checkpointId: string | undefined;
    if (v.source === 'custom') {
      const event = v.event;
      if (
        !record(event) ||
        event.name !== 'deep_agents.memory' ||
        Object.keys(event).some((k) => !['name', 'data'].includes(k)) ||
        !record(event.data)
      )
        return unavailable;
      const d = event.data;
      if (
        Object.keys(d).length !== 3 ||
        Object.keys(d).some(
          (k) => !['memory_scope_owner', 'memory_contents', 'phase'].includes(k)
        ) ||
        d.memory_scope_owner !== v.scope ||
        !['initial', 'model', 'terminal'].includes(d.phase as string)
      )
        return unavailable;
      contents = d.memory_contents;
      phase = d.phase as typeof phase;
    } else if (v.source === 'checkpoint') {
      const a = v.authority;
      if (
        !record(a) ||
        a.kind !== 'terminal' ||
        !record(a.turn) ||
        a.turn.owner !== v.owner ||
        a.turn.generation !== v.generation ||
        a.turn.memoryScope !== v.scope ||
        !record(a.values) ||
        a.values.memory_scope_owner !== v.scope ||
        !record(a.checkpoint) ||
        !text(a.checkpoint.checkpoint_id)
      )
        return unavailable;
      contents = a.values.memory_contents;
      checkpointId = a.checkpoint.checkpoint_id;
    } else return unavailable;
    const map = captureMemoryMap(contents);
    if (!map) return unavailable;
    const present = Object.hasOwn(map, MEMORY_PATH);
    return Object.freeze({
      kind: v.source === 'custom' ? 'live' : 'checkpoint',
      file: present ? 'present' : 'missing',
      ...(present ? { contents: map[MEMORY_PATH] } : {}),
      scope: v.scope,
      generation: v.generation,
      ...(phase ? { phase } : {}),
      ...(checkpointId ? { checkpointId } : {}),
    });
  } catch {
    return unavailable;
  }
}
authority.ts
import type { Message, ToolCall } from '@threadplane/core';
import { validScope } from './browser-scope';
 
/** All limits count UTF16 code units, including property keys. */
export const LIMITS = Object.freeze({
  depth: 32,
  nodes: 10000,
  copyText: 4 * 1024 * 1024,
  displayText: 1024 * 1024,
  fieldText: 65536,
  entries: 100,
});
export const record = (v: unknown): v is Record<string, unknown> =>
  !!v && typeof v === 'object' && !Array.isArray(v);
export const text = (v: unknown): v is string =>
  typeof v === 'string' && v.length > 0;
 
/** Bounded capture before recursion or serialization. Never evaluates an accessor.
 * Enumerable keys are counted before the reflective own-key scan; descriptors
 * are inspected individually rather than allocating an unbounded descriptor map. */
export function copyData(value: unknown, allowUndefined = true): unknown {
  let nodes = 0,
    units = 0;
  const parents = new Set<object>();
  const charge = (s: string) => {
    units += s.length;
    if (units > LIMITS.copyText) throw new Error('Data text limit');
  };
  function visit(v: unknown, depth: number): unknown {
    if (++nodes > LIMITS.nodes || depth > LIMITS.depth)
      throw new Error('Data structure limit');
    if (v === null || typeof v === 'boolean') return v;
    if (typeof v === 'string') {
      charge(v);
      return v;
    }
    if (v === undefined && allowUndefined) return undefined;
    if (typeof v === 'number' && Number.isFinite(v)) return v;
    if (typeof v !== 'object' || v === null || parents.has(v))
      throw new Error('Nonplain data');
    const array = Array.isArray(v),
      proto = Object.getPrototypeOf(v);
    if (
      array
        ? proto !== Array.prototype
        : proto !== Object.prototype && proto !== null
    )
      throw new Error('Nonplain prototype');
    const length = array
      ? Object.getOwnPropertyDescriptor(v, 'length')?.value
      : undefined;
    if (
      array &&
      (!Number.isInteger(length) || length > LIMITS.nodes - nodes || length < 0)
    )
      throw new Error('Array limit');
    let enumerable = 0;
    for (const k in v)
      if (Object.hasOwn(v, k) && ++enumerable > LIMITS.nodes - nodes)
        throw new Error('Key limit');
    const keys = Reflect.ownKeys(v);
    if (keys.length > LIMITS.nodes - nodes + (array ? 1 : 0))
      throw new Error('Key limit');
    if (array && keys.length !== length + 1)
      throw new Error('Sparse or decorated array');
    const out: Record<string, unknown> | unknown[] = array
      ? []
      : Object.create(null);
    parents.add(v);
    for (const k of keys) {
      if (typeof k !== 'string') throw new Error('Symbol data');
      if (array && k === 'length') continue;
      if (array && (!/^(0|[1-9][0-9]*)$/.test(k) || Number(k) >= length))
        throw new Error('Decorated array');
      charge(k);
      const d = Object.getOwnPropertyDescriptor(v, k);
      if (!d || !('value' in d) || !d.enumerable)
        throw new Error('Accessor or hidden data');
      Object.defineProperty(out, k, {
        value: visit(d.value, depth + 1),
        enumerable: true,
      });
    }
    parents.delete(v);
    return Object.freeze(out);
  }
  return visit(value, 0);
}
 
/** Stable semantic equality for already-owned bounded plain data. */
export function sameData(a: unknown, b: unknown): boolean {
  if (a === b) return true;
  if (Array.isArray(a) && Array.isArray(b))
    return a.length === b.length && a.every((v, i) => sameData(v, b[i]));
  if (!record(a) || !record(b)) return false;
  const ak = Object.keys(a),
    bk = Object.keys(b);
  return (
    ak.length === bk.length &&
    ak.every((k) => Object.hasOwn(b, k) && sameData(a[k], b[k]))
  );
}
 
export function displayUnits(strings: readonly string[]): number {
  let total = 0;
  for (const s of strings) {
    if (typeof s !== 'string') throw new Error('Unavailable display text');
    if (s.length > LIMITS.fieldText) throw new Error('Display field limit');
    total += s.length;
    if (total > LIMITS.displayText) throw new Error('Display aggregate limit');
  }
  return total;
}
/** Already-owned optional native metadata stays literal and contract-shaped. */
export function validMessageMetadata(m: Record<string, unknown>): boolean {
  if (
    ['name', 'reasoning', 'toolCallId'].some(
      (k) => m[k] !== undefined && typeof m[k] !== 'string'
    )
  )
    return false;
  if (
    m.toolCallIds !== undefined &&
    (!Array.isArray(m.toolCallIds) ||
      m.toolCallIds.some((v) => !text(v)) ||
      new Set(m.toolCallIds).size !== m.toolCallIds.length)
  )
    return false;
  if (
    m.citations !== undefined &&
    (!Array.isArray(m.citations) ||
      m.citations.some(
        (c) =>
          !record(c) ||
          !text(c.id) ||
          typeof c.index !== 'number' ||
          !Number.isFinite(c.index) ||
          ['title', 'url', 'snippet', 'sourceType', 'iconUrl'].some(
            (k) => c[k] !== undefined && typeof c[k] !== 'string'
          ) ||
          (c.publishedAt !== undefined &&
            typeof c.publishedAt !== 'string' &&
            typeof c.publishedAt !== 'number')
      ))
  )
    return false;
  return true;
}
export interface Baseline {
  readonly messages: readonly unknown[];
}
export interface TurnIdentity {
  readonly memoryScope: string;
  readonly owner: string;
  readonly generation: string;
  readonly threadId: string;
  readonly humanId: string;
  readonly humanContent: string;
}
export interface Turn extends TurnIdentity {
  readonly baseline: Baseline;
}
export interface Checkpoint {
  readonly thread_id: string;
  readonly checkpoint_ns: '';
  readonly checkpoint_id: string;
  readonly checkpoint_map?: Readonly<Record<string, string>>;
}
export interface NativeSnapshot {
  readonly status: string;
  readonly messages: readonly Message[];
  readonly toolCalls: readonly ToolCall[];
  readonly values?: unknown;
  readonly interrupts: readonly unknown[];
  readonly subgraphs: readonly unknown[];
  readonly error?: unknown;
}
export interface AuthorityInput {
  readonly memoryScope: string;
  readonly turn: Turn | undefined;
  readonly owner: string;
  readonly generation: string;
  readonly threadId: string;
  readonly outcome: string;
  /** Current root from the owned raw client, never child values or native synthesis. */
  readonly observedState: unknown;
  /** Fresh raw history from that same client and thread, newest first. */
  readonly history: readonly unknown[];
  /** Exact getState(thread, history[0].checkpoint), not a second current lookup. */
  readonly loadedCheckpoint: unknown;
  readonly snapshot: NativeSnapshot;
  readonly loadedSnapshot: NativeSnapshot;
}
export type Authority =
  | { readonly kind: 'unconfirmed' }
  | {
      readonly kind: 'terminal';
      readonly checkpoint: Checkpoint;
      readonly messages: readonly unknown[];
      readonly values: Readonly<Record<string, unknown>>;
      readonly rawToolStatuses: Readonly<Record<string, 'success' | 'error'>>;
      readonly turn: Turn;
    };
export function captureBaseline(
  input: unknown = { messages: [] }
): Baseline | undefined {
  try {
    const v = copyData(input, false);
    if (!record(v) || !Array.isArray(v.messages)) return undefined;
    return Object.freeze({ messages: v.messages });
  } catch {
    return undefined;
  }
}
export function captureTurn(
  baseline: Baseline | undefined,
  input: TurnIdentity
): Turn | undefined {
  try {
    const b = copyData(baseline, false),
      identity = copyData(input, false);
    if (
      !record(b) ||
      !Array.isArray(b.messages) ||
      !record(identity) ||
      !validScope(identity.memoryScope) ||
      ['owner', 'generation', 'threadId', 'humanId'].some(
        (k) =>
          !text(identity[k]) ||
          (identity[k] as string).length > LIMITS.fieldText
      ) ||
      typeof identity.humanContent !== 'string' ||
      identity.humanContent.length > LIMITS.fieldText ||
      b.messages.some((m) => record(m) && m.id === identity.humanId)
    )
      return undefined;
    return Object.freeze({ ...identity, baseline: b }) as unknown as Turn;
  } catch {
    return undefined;
  }
}
function checkpointSource(
  value: unknown,
  threadId: string
): Checkpoint | undefined {
  if (
    !record(value) ||
    value.thread_id !== threadId ||
    value.checkpoint_ns !== '' ||
    !text(value.checkpoint_id) ||
    value.checkpoint_id.length > LIMITS.fieldText ||
    Object.keys(value).some(
      (k) =>
        ![
          'thread_id',
          'checkpoint_ns',
          'checkpoint_id',
          'checkpoint_map',
        ].includes(k)
    )
  )
    return undefined;
  if (
    value.checkpoint_map !== undefined &&
    (!record(value.checkpoint_map) ||
      Object.keys(value.checkpoint_map).length !== 1 ||
      value.checkpoint_map[''] !== value.checkpoint_id)
  )
    return undefined;
  return value as unknown as Checkpoint;
}
const empty = (v: unknown) =>
  v === undefined || (Array.isArray(v) && v.length === 0);
const clean = (v: Record<string, unknown>) =>
  (v.error === undefined || v.error === null) &&
  empty(v.interrupts) &&
  empty(v.subgraphs) &&
  empty(v.__interrupt__);
 
/** Canonical root semantics independent of native delivery bookkeeping. */
function normalizedRaw(raw: readonly unknown[]):
  | {
      messages: Record<string, unknown>[];
      tools: Record<string, unknown>[];
      statuses: Record<string, 'success' | 'error'>;
    }
  | undefined {
  const messages: Record<string, unknown>[] = [],
    tools = new Map<string, Record<string, unknown>>(),
    ids = new Set<string>(),
    pending = new Set<string>(),
    statuses: Record<string, 'success' | 'error'> = Object.create(null);
  for (const w of raw) {
    if (
      !record(w) ||
      !text(w.id) ||
      ids.has(w.id) ||
      typeof w.content !== 'string' ||
      !clean(w) ||
      !empty(w.invalid_tool_calls) ||
      !['human', 'ai', 'tool'].includes(w.type as string)
    )
      return undefined;
    ids.add(w.id);
    const extra = w.additional_kwargs ?? {};
    if (
      !record(extra) ||
      !empty(extra.tool_calls) ||
      extra.function_call !== undefined
    )
      return undefined;
    const reasoning =
      typeof w.reasoning === 'string'
        ? w.reasoning
        : typeof extra.reasoning_content === 'string'
        ? extra.reasoning_content
        : undefined;
    // Supported citation shape is normalized explicitly; no private runtime imports.
    const source = extra.citations ?? extra.sources;
    let citations: unknown;
    if (Array.isArray(source))
      citations = source.map((s, i) => {
        if (typeof s === 'string')
          return { id: `c${i + 1}`, index: i + 1, url: s };
        const r = record(s) ? s : {},
          out: Record<string, unknown> = {
            id:
              [r.id, r.refId].find((x) => typeof x === 'string') ?? `c${i + 1}`,
            index:
              typeof r.index === 'number' && Number.isFinite(r.index)
                ? r.index
                : i + 1,
          };
        for (const [k, aliases] of Object.entries({
          title: ['title', 'name'],
          url: ['url', 'href', 'source'],
          snippet: ['snippet', 'content', 'excerpt'],
          sourceType: ['sourceType'],
          iconUrl: ['iconUrl'],
        })) {
          const v = aliases.map((a) => r[a]).find((x) => typeof x === 'string');
          if (v !== undefined) out[k] = v;
        }
        if (
          typeof r.publishedAt === 'string' ||
          typeof r.publishedAt === 'number'
        )
          out.publishedAt = r.publishedAt;
        if (record(r.extra)) out.extra = r.extra;
        return out;
      });
    const m: Record<string, unknown> = {
      id: w.id,
      role:
        w.type === 'human' ? 'user' : w.type === 'ai' ? 'assistant' : 'tool',
      content: w.content,
      ...(typeof w.name === 'string' ? { name: w.name } : {}),
    };
    if (reasoning !== undefined) m.reasoning = reasoning;
    if (citations !== undefined) m.citations = citations;
    if (w.type === 'ai') {
      if (pending.size || w.tool_call_id !== undefined) return undefined;
      const calls = w.tool_calls ?? [];
      if (!Array.isArray(calls)) return undefined;
      m.toolCallIds = [];
      for (const c of calls) {
        if (
          !record(c) ||
          !text(c.id) ||
          !text(c.name) ||
          tools.has(c.id) ||
          !Object.hasOwn(c, 'args') ||
          (c.type !== undefined && c.type !== 'tool_call') ||
          Object.keys(c).some(
            (k) => !['id', 'name', 'args', 'type'].includes(k)
          )
        )
          return undefined;
        (m.toolCallIds as string[]).push(c.id);
        tools.set(c.id, {
          id: c.id,
          name: c.name,
          args: c.args,
          status: 'pending',
        });
        pending.add(c.id);
      }
    } else if (w.type === 'tool') {
      const t =
        typeof w.tool_call_id === 'string'
          ? tools.get(w.tool_call_id)
          : undefined;
      if (
        !t ||
        !pending.delete(w.tool_call_id as string) ||
        !['success', 'error'].includes(w.status as string) ||
        (w.name !== t.name &&
          !(
            w.status === 'error' &&
            (w.name === null || w.name === undefined)
          )) ||
        !empty(w.tool_calls)
      )
        return undefined;
      m.toolCallId = w.tool_call_id;
      t.status = 'complete';
      t.result = w.content;
      statuses[w.tool_call_id as string] = w.status as 'success' | 'error';
    } else if (
      pending.size ||
      !empty(w.tool_calls) ||
      w.tool_call_id !== undefined
    )
      return undefined;
    messages.push(m);
  }
  if (
    pending.size ||
    messages[0]?.role !== 'user' ||
    messages.at(-1)?.role !== 'assistant' ||
    !text(messages.at(-1)?.content) ||
    (messages.at(-1)?.toolCallIds as unknown[])?.length
  )
    return undefined;
  return { messages, tools: [...tools.values()], statuses };
}
function nativeSemantic(
  input: NativeSnapshot
):
  | { messages: Record<string, unknown>[]; tools: readonly ToolCall[] }
  | undefined {
  if (
    input.status !== 'idle' ||
    (input.error !== undefined && input.error !== null) ||
    !Array.isArray(input.interrupts) ||
    input.interrupts.length ||
    !Array.isArray(input.subgraphs) ||
    !Array.isArray(input.messages) ||
    !Array.isArray(input.toolCalls)
  )
    return undefined;
  const messages: Record<string, unknown>[] = [],
    ids = new Set<string>();
  for (const m of input.messages) {
    if (
      !record(m) ||
      !text(m.id) ||
      ids.has(m.id) ||
      typeof m.content !== 'string' ||
      !validMessageMetadata(m) ||
      !record(m.delivery) ||
      !text(m.delivery.generation) ||
      m.delivery.phase !== 'complete' ||
      m.delivery.outcome !== 'success' ||
      Object.keys(m).some(
        (k) =>
          ![
            'id',
            'role',
            'content',
            'delivery',
            'reasoning',
            'citations',
            'name',
            'toolCallId',
            'toolCallIds',
          ].includes(k)
      )
    )
      return undefined;
    ids.add(m.id);
    const out: Record<string, unknown> = {
      id: m.id,
      role: m.role,
      content: m.content,
    };
    for (const k of ['name', 'reasoning', 'citations', 'toolCallId'])
      if (m[k as keyof Message] !== undefined) out[k] = m[k as keyof Message];
    if (m.role === 'assistant') {
      if (m.toolCallIds !== undefined && !Array.isArray(m.toolCallIds))
        return undefined;
      out.toolCallIds = m.toolCallIds ?? [];
    } else if (!empty(m.toolCallIds)) return undefined;
    messages.push(out);
  }
  return { messages, tools: input.toolCalls };
}
/** Only contiguous, complete result batches declared by a current assistant may
 * permute. Prior canonical history and all human/assistant boundaries stay strict. */
function matchesRoot(
  raw: ReturnType<typeof normalizedRaw>,
  native: ReturnType<typeof nativeSemantic>,
  prefix: number
): boolean {
  if (
    !raw ||
    !native ||
    raw.messages.length !== native.messages.length ||
    raw.tools.length !== native.tools.length
  )
    return false;
  const catalog = new Map<string, ToolCall>();
  for (const t of native.tools) {
    if (
      !record(t) ||
      !text(t.id) ||
      catalog.has(t.id) ||
      t.status !== 'complete'
    )
      return false;
    catalog.set(t.id, t);
  }
  for (const t of raw.tools)
    if (!sameData(t, catalog.get(t.id as string))) return false;
  for (let i = 0; i < raw.messages.length; i++) {
    const r = raw.messages[i],
      n = native.messages[i];
    if (!sameData(r, n)) return false;
    const callIds = r.role === 'assistant' ? (r.toolCallIds as string[]) : [];
    if (i >= prefix && callIds.length) {
      const batch = raw.messages.slice(i + 1, i + 1 + callIds.length),
        observed = native.messages.slice(i + 1, i + 1 + callIds.length);
      if (
        batch.length !== callIds.length ||
        observed.length !== callIds.length ||
        batch.some((m) => m.role !== 'tool') ||
        observed.some((m) => m.role !== 'tool')
      )
        return false;
      const byId = new Map(observed.map((m) => [m.toolCallId, m]));
      if (
        byId.size !== callIds.length ||
        batch.some(
          (m) =>
            !callIds.includes(m.toolCallId as string) ||
            !sameData(m, byId.get(m.toolCallId))
        )
      )
        return false;
      i += callIds.length;
    }
  }
  return true;
}
 
/** Compare already-owned live values with the exact checkpoint projection.
 * Local unfiltered compiled values retain private Memory fields. The installed
 * API stream uses graph.output_channels and can omit these two top-level fields.
 * No native fields are synthesized. Every observed key
 * still agrees exactly; every ordinary checkpoint key must be observed. */
function matchesLiveValues(
  live: unknown,
  checkpoint: Record<string, unknown>
): boolean {
  if (!record(live)) return false;
  return (
    Object.keys(live).every(
      (key) =>
        Object.hasOwn(checkpoint, key) && sameData(live[key], checkpoint[key])
    ) &&
    Object.keys(checkpoint).every(
      (key) =>
        Object.hasOwn(live, key) ||
        key === 'memory_contents' ||
        key === 'memory_scope_owner'
    )
  );
}
 
/** A failure yields no replacement: callers retain previously confirmed authority.
 * The caller captures its owned client and turn once before asynchronous reads. */
export function captureAuthority(input: unknown): Authority {
  const unavailable: Authority = Object.freeze({ kind: 'unconfirmed' });
  try {
    const captured = copyData(input) as AuthorityInput;
    const turn = captured.turn;
    if (
      !turn ||
      !validScope(captured.memoryScope) ||
      turn.memoryScope !== captured.memoryScope ||
      ['owner', 'generation', 'threadId', 'humanId'].some(
        (k) =>
          !text((turn as unknown as Record<string, unknown>)[k]) ||
          ((turn as unknown as Record<string, unknown>)[k] as string).length >
            LIMITS.fieldText
      ) ||
      typeof turn.humanContent !== 'string' ||
      turn.humanContent.length > LIMITS.fieldText ||
      captured.outcome !== 'success' ||
      turn.owner !== captured.owner ||
      turn.generation !== captured.generation ||
      turn.threadId !== captured.threadId ||
      !Array.isArray(turn.baseline?.messages)
    )
      return unavailable;
    const observed = copyData(captured.observedState, false),
      loaded = copyData(captured.loadedCheckpoint, false),
      history = copyData(captured.history, false);
    if (
      !record(observed) ||
      !record(loaded) ||
      !Array.isArray(history) ||
      !history.length ||
      !sameData(observed, loaded) ||
      !sameData(history[0], loaded) ||
      !clean(loaded) ||
      !Array.isArray(loaded.next) ||
      loaded.next.length ||
      !Array.isArray(loaded.tasks) ||
      loaded.tasks.length ||
      !record(loaded.values) ||
      loaded.values.memory_scope_owner !== captured.memoryScope ||
      !clean(loaded.values) ||
      !Array.isArray(loaded.values.messages)
    )
      return unavailable;
    const cp = checkpointSource(loaded.checkpoint, captured.threadId);
    if (!cp) return unavailable;
    const checkpointIds = new Set<string>();
    for (const h of history) {
      if (!record(h)) return unavailable;
      const c = checkpointSource(h.checkpoint, captured.threadId);
      if (!c || checkpointIds.has(c.checkpoint_id)) return unavailable;
      checkpointIds.add(c.checkpoint_id);
    }
    const messages = loaded.values.messages,
      prefix = turn.baseline.messages;
    if (!sameData(prefix, messages.slice(0, prefix.length))) return unavailable;
    const human = messages[prefix.length];
    if (
      !record(human) ||
      human.type !== 'human' ||
      human.id !== turn.humanId ||
      human.content !== turn.humanContent ||
      messages.filter((m) => record(m) && m.id === turn.humanId).length !== 1 ||
      messages
        .slice(prefix.length + 1)
        .some((m) => record(m) && m.type === 'human')
    )
      return unavailable;
    const raw = normalizedRaw(messages),
      before = nativeSemantic(captured.snapshot),
      after = nativeSemantic(captured.loadedSnapshot);
    const otherValues: Record<string, unknown> = {};
    for (const k of Object.keys(loaded.values))
      if (k !== 'messages' && k !== '__interrupt__')
        otherValues[k] = loaded.values[k];
    if (
      !raw ||
      !matchesRoot(raw, before, prefix.length) ||
      !matchesRoot(raw, after, prefix.length) ||
      !matchesLiveValues(captured.snapshot.values, otherValues) ||
      !sameData(captured.loadedSnapshot.values, otherValues)
    )
      return unavailable;
    return Object.freeze({
      kind: 'terminal',
      checkpoint: cp,
      messages,
      values: loaded.values,
      rawToolStatuses: Object.freeze(raw.statuses),
      turn,
    });
  } catch {
    return unavailable;
  }
}

Restore and stop

Saved conversations restore an exact retained historical checkpoint through reads only. They do not show the current store. Failed initial, model or terminal reads and oversized observations are unavailable and cannot confirm Saved. A terminal failure after a write may leave that write persisted; there is no rollback. Stop cancels local delivery while backend work may continue. New invalidates late results. Conversations can update the same file at the same time.

Shared backend and build

The StoreBackend namespace is cockpit / deep-agents-memory / browser-v1 / scope. Both frontends use these complete sources:

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')
npx nx build cockpit-deep-agents-memory-react
npx nx e2e cockpit-deep-agents-memory-react

Browser verification uses the actual compiled graph, installed SDK and offline deterministic model. Verification controls and fixtures are excluded from public Code.

Looking for something specific?