Skip to content

feat(memory): L3 field cognition, retry failed L3 batches (MEMMY-852), feed scanned sessions into L3 - #739

Open
Anson-Se wants to merge 9 commits into
MemTensor:v1.2.7from
Anson-Se:gh-590-scan-l3
Open

Anson-Se wants to merge 9 commits into
MemTensor:v1.2.7from
Anson-Se:gh-590-scan-l3

Conversation

@Anson-Se

@Anson-Se Anson-Se commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

做了什么

基于 v1.2.7(之前基于 v1.2.6,已整体搬到 1.2.7,9 个提交,代码不依赖 v1.2.6 独有的东西)。2026-10-28 已 rebase 到 v1.2.7 最新(88edd02a,合入了 v1.2.5 的 284 个提交);唯一冲突是 Memory/src/server/http.ts 的 import 列表(上游新增 EmbeddingInferenceInputSchema),两边都保留,在原提交里解决,没有新增提交。

这个 PR 包含三件互相依赖的事,按提交顺序:

1. L3 场域认知改造(前 7 个提交)

  • 把 L3 从“每个项目四段整段重写的文字”改成按“本机 / 项目”分开存的短条目,只有两类:
    • 环境事实(env_fact):“条件 -> 现象”,必须有工具输出作证据;
    • 项目规矩(project_constraint):用户希望以后在这个项目里一直遵守的做法。
  • 判断标准是“换一个人在这台电脑或这个项目上工作,这条仍然成立”。
  • 以下内容不记:
    • 个人偏好、单次任务的细节;
    • 工作产出的结论、读代码得到的知识;
    • 通用常识、临时状态;
    • Agent 自身的沙箱和工具限制。
  • 条目支持新增、替换、删除、确认:新规矩会替换旧规矩,问题修好后对应条目会失效。较旧的证据不能替换或删除由较新证据写入、确认的条目;项目被重置(删除)前说过的话,之后也不会再写回去。
  • 任务以任何方式关闭(会话关闭、新任务、用户结束话题、空闲超时)时,都会把这个任务的轮次交给 L3,只到这个任务为止,不截断还在进行的任务。
  • 每一轮捕获成功后都有一个持久的负责任务,不会丢:复用上游已有的 work_memory_idle_flush(每个会话一个去重任务,每轮重新排期,会话安静 2 小时后到期),在捕获的同一个事务里排上;到期时先把这个会话还没交给 L3 的轮次冻结,再做原来的 Work Memory。所以对话直接停掉、从没发过会话关闭,最后几轮也能进 L3。
  • Memory 启动时、导入备份后、运行中把开关从关打开时,都会把还有轮次没交给 L3 的会话补上:开着的会话重新排期,已关闭的会话直接冻结。
  • 批次只写入一次:L3 批次任务和空闲任务领取时带领取令牌,租约过期后被别人领走、或任务被重新排期时,旧的那次执行既不写入结果,也不改任务状态。这一条顺带修掉了上游一个漏洞:空闲任务到期时会话还没安静够、自己重新排期后,会被收尾标成已完成,下一次永远不跑。
  • 写入前再查一次证据:引用的原始轮次或 L1 在等模型期间被删除、脱敏的,对应操作丢弃。补跑较早的批次时,模型会看到"较新的证据删掉了哪些条目、在什么时间",不能凭更早的证据写回去。
  • 环境画像由代码扫描生成,模型只读不写。同一个项目 10 分钟内只扫描一次;项目没变化(指纹不含本机运行时版本)时不调模型。
  • 项目识别:先看用户在桌面端选过的项目,再看 git 仓库,最后看普通文件夹。临时目录、系统和工具目录、依赖包目录不算项目。
  • 有开关 algorithm.l3FieldCognition.enabled(环境变量 MEMMY_L3_FIELD_COGNITION=1),默认关闭。关闭时走原来的 L3,行为不变。
  • 不改 SCHEMA_VERSION(仍为 9),只新增 4 张表,旧数据不转写,旧版 Memmy 仍能打开这个库。

2. MEMMY-852 失败批次补跑(第 8 个提交)

  • 临时失败(空响应、超时、限流、连接中途断开)分别等 1、5、15、30、60 分钟再重试,第 5 次重试还失败就进死信。其他失败(如模型连续两次给出不合格的回答)大概率会重复,只再试 1 次就进死信。L3 任务不会停在 failed 状态挡住后面的批次直到重启,后面的批次照常处理。
  • 配置类错误(没配模型、key 错、额度不足)和完全连不上模型(断网)先挂起,不消耗重试次数,每 30 分钟再试一次,恢复后自动继续;没配进化模型时不领取 L3 任务。
  • 进了死信的新 L3 批次,在 Memory 下次启动时自动重跑一次(每个批次只自动一次);另有补跑入口 POST /api/v1/l3/failed-batches/retry,在原任务上复位后重跑。
  • worker 计算下次唤醒时间和领取任务用同一套条件(同一机器按顺序、L1 摘要已完成),被前序批次挡住的任务不会让 worker 空转。
  • 全部接在现有的 evolution_jobs 队列上(用 runAfter、attempts/max_attempts、failJob 这些现有字段和方法),没有新建队列、定时器或重试表。

3. #590 扫描会话进 L3(第 9 个提交)

  • 只在新 L3 开关打开时生效:扫描新开的会话走新协议,并关闭 Work Memory。开关关闭时扫描和以前一样不进 L3(包括旧 L3)。
  • 每个来源一轮扫描处理完后,把这一轮碰到的会话交给 L3 处理,不等模型跑完,所以不会拖慢扫描。
  • 1.2.7 的桌面端扫描由 backend 执行(agent-source-service 的持久化扫描),每个来源处理完(包括中途取消)后同样把碰到的会话交给 L3;交接请求带完整信封和来源,Memory 按打开这些会话时的同一规则确认归属;交接失败只打一条 warn,不影响扫描。
  • 托管导入路径(backend ingestion-service)也走同样的处理。
  • 先扫描、后来 Hook 又接管同一会话时,由 Hook 接手。
  • 扫描结束时的交接只是快速通道;交接失败、进程在交接前退出时,靠上面的空闲任务和启动补登记兜底,不会丢。

Closes #590

改动范围

提交 内容
add l3 field cognition tables and flag 新表、开关、仓库层、快照记忆
resolve l3 projects from paths 从路径识别项目
accept user-selected project roots for l3 桌面端把用户选过的项目推给 Memory
build l3 evidence fragments with unit candidates 证据片段及其归属
profile machine and project units for l3 本机画像、项目画像
inject and display l3 field context 会话开头注入 L3、面板显示
update l3 field items from frozen batches 更新流水线、提示词、输入裁剪与上限(发生截断时打 debug 日志 input.truncated)
retry and resume failed l3 jobs through the job queue MEMMY-852
feed scanned sessions into l3 #590(Memory 侧扫描、backend 桌面扫描、托管导入)

共 70 个文件,约 6800 行,其中测试约 2900 行。

怎么验证的

  • 测试(在 rebase 后的 v1.2.7(88edd02a)+ 本 PR 上跑):
    • Memory、App/backend、App/memmy-agent typecheck 都通过;9 个提交逐个单独 typecheck 也都通过。
    • Memory 全量 1584 个通过;失败的 8 个在 3 个文件里(扫描响应、history-change-log、negative-experience),都是满负载超时,单独运行 21 个全部通过。
    • App/backend 全量 1345 个通过;失败的 5 个与本 PR 无关:cloud-client 2 个和 plugin 路由 1 个是本机工作区遗留的 .env 和构建产物导致,在干净工作区里本 PR 和纯上游都通过;Office 插件端到端 2 个需要仓库里没有的发布包,纯上游同样失败。
    • App/memmy-agent 的 memmy-memory、core、entrypoints 测试 3204 个通过。
  • 离线回放对比:同样 27 个会话(16 个真实会话 + 11 个测试会话),新旧两版都从空 L3 开始,同一个模型,各跑 3 次;新版用的是本 PR 在 1.2.7 上的代码。存下的条目混在一起交给独立评审盲评,评审不知道条目来自哪个版本。
v1.2.6 本 PR
每次存下的条目 72~94 10~14
其中该存的占比 37% 95%
噪声(一次性、常识、临时状态、Agent 自身限制) 35~50 条 0~2 条
放错地方 6 条 0
模型输入 token 约 445 万 约 52 万
模型调用次数 67~68 47~48
  • 输入 token 少的原因:旧版把整段原始对话原样发给模型,其中工具调用和工具输出占 99%;而且项目契约、领域知识两个字段各发一遍。新版每组只调用一次模型,工具输出只留头尾和报错行,输入上限 65536 字,超出时先丢工具输出。
  • 真机测试(1.2.7 + 本 PR):在本机用 Memmy CLI 和 Codex 跑过多个场景,包括规矩替换、问题修好后失效、临时失败和个人偏好不记、大文件夹里改子项目,结果都符合预期。真机测试中发现大文件夹会抢走子项目的规矩(相对路径没识别、同时碰到大文件夹和子项目时两边都算),已修复并加测试。
  • Codex 审核 4 轮:前 3 轮提的问题都已修复。第 4 轮提出 1 个阻塞问题(模型上下文较小时会进死信),已修复:输出预算降到 16384,超出上下文时缩小输入再试一次。
  • 按上线要求复查(稳定不丢、复用现有队列、不浪费 token、原有功能不变):Fable 5.1 和 GPT-6 Astra 各自独立审了 7 轮,每轮确认成立的问题都修了、加了测试,并确认去掉修复后测试会失败;修复都并回了各自原来的提交。主要修了:
    • 捕获成功的轮次可能没人负责(扫描交接失败、对话直接停掉、热开启开关)→ 上面的空闲任务和补登记;
    • 慢调用超过租约后旧结果写回、或把重新排期的任务标成完成 → 领取令牌;
    • 归档机器快照后客户端仍注入旧内容、旧证据覆盖新条目、删掉的条目被晚到的旧对话写回;
    • 导入备份时:保留了本库单元而被丢掉的未完成批次,对应轮次要在本库重新冻结;同一备份导两次不重复冻结;"替换"导入换掉了某个位置上的轮次时,按轮次本身(L1 和原始轮次 id)判断有没有处理过,而不只看序号;
    • worker 空转、项目画像重复调模型、确定性失败重试过多。
      上面回放里替换/删除操作的证据时间都是递增的,这些保护在回放中不会触发,回放结果不受影响。

已知限制

  • 开关默认关闭。打开后,面板里旧 L3 和新 L3 会同时显示。
  • 模型判断有运行间波动:3 次回放里各有 1 次把“没装 tomli_w”“npm 不在默认 PATH”记成了事实,1 次没把 memmy-agent 的“审核只读、不读密钥”当成项目规矩。
  • 补跑入口还没接前端;自动重跑后再次进死信的批次,只能通过补跑入口重跑。
  • memmy-agent 每 60 秒读一次桌面端项目列表,有变化才推给 Memory(列表文件由多个进程共用,没有变更通知)。
  • 条目偶尔会比提示词要求的长;少量只和某个工具相关的事实会被放到“所有地方”。
  • 可能和 fix: 优化记忆后台任务调度,避免历史导入阻塞实时任务和向量重试 #734(后台任务调度)冲突,两边都改了任务调度和重试的逻辑。
  • feat(memory): 基于模型判断 L1 提取并复用同 Session 历史 Episode #681 合入后,L1 接口需要再适配一次。
  • 手动调用 /worker/run 时,如果某次 L3 模型调用超过 60 秒的租约,同一批次会再调一次模型;有领取令牌,只写入一次,多花一次 token。
  • 导入备份时,被跳过批次后面已经处理过的轮次会再处理一次(多花 token,不丢)。
  • 两种很少见的导入组合仍可能让某一轮进不了新 L3:
    • 把脱敏导出(不含原文)的备份,用默认"跳过"方式导回同一个库:导出时游标被推到最后一轮(上游原有行为,旧 L3 也一样),本库还没冻结的轮次会被越过;
    • 先删除或脱敏某一轮的原文、批次处理时跳过了它,之后又用"替换"导入完整备份把原文恢复:这一轮会被当成已经处理过。

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@Anson-Se

Copy link
Copy Markdown
Contributor Author

@origami-chen

@Anson-Se
Anson-Se changed the base branch from v1.2.6 to 1.2.7 October 26, 2026 12:58
@Anson-Se
Anson-Se force-pushed the gh-590-scan-l3 branch 3 times, most recently from 08ec0d4 to 3a88415 Compare October 27, 2026 07:33
Anson-Se and others added 9 commits October 28, 2026 08:52
Add the storage base for itemized machine/project L3, behind
algorithm.l3FieldCognition (off by default, local SQLite only):

- four new tables (l3_units, l3_items, l3_unit_batches,
  l3_user_projects) created alongside the existing ones without
  bumping SCHEMA_VERSION; old L3 tables and rows are untouched
- L3FieldRepository for units, items, snapshot memories, unit
  deletion, inactive-item purge and the user project list; an item
  removed by a delete or replace keeps when that evidence happened
- register the tables for bundle export/import and clear-all; a unit
  that already exists is kept whole on import (its unit, items, batches,
  jobs and snapshot from the bundle are skipped; conflictStrategy
  "error" fails instead; its unfinished batches this database lacks
  move their sessions' cursors back so those turns are frozen again),
  and unfinished unit batches are dropped from redacted exports
- countL3Units for the per-item length limit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decide which project a path belongs to for the new L3, by reading files
only (no git process):

- the nearer of a user-selected folder and a git repository wins; a
  shared root counts once with the git identity
- git identity is the normalized origin (ssh, scp-like and https forms
  map to host/path), else the common git dir, so a clone's worktrees
  and subdirectories share one project
- a session folder outside any repository is a plain-folder project; a
  tool path only joins the session project it lands in
- temp dirs, Codex dated dirs, Claude scratch workspaces,
  ~/.codex/memories*, Memmy task folders and the home/Desktop/
  Documents/Downloads folders themselves are never projects

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop app now tells Memory which folders the user picked as
projects, so L3 can treat them as project roots:

- POST /api/v1/l3/user-projects replaces the user's whole list and
  the Memmy task-folder roots; relative paths are rejected; the list is
  stored regardless of the L3 field cognition flag
- memmy-agent's long-running gateway reads the project registry every
  minute and pushes only when the list changed or the last push
  failed; a corrupt registry is never pushed
- the contract is added identically to the Memory and App copies, with
  a test that keeps them in sync

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cut a raw turn into evidence fragments for the new L3 so each fact can
be attributed by the paths its own evidence touched:

- one fragment for the user's words, one per tool call with its paired
  result (reusing the existing strict call/result pairing), and one per
  result that matches no call
- paths come from file/dir arguments, cd and git -C targets and absolute
  command arguments; Codex code-mode snippets are read for their
  cmd/workdir literals; relative paths resolve against the call's
  workdir, else the session folder; URLs are ignored
- each fragment gets the project units its paths resolve to, falling
  back to the session folder; long outputs keep head, tail and the
  error lines in between

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- machine profile: probe OS (macOS product version, not the Darwin
  kernel), arch, shell and node/python3/git/npm/brew versions with
  2-second timeouts, and render it in a fixed format by code only
- project profile for the new L3: reuse the existing scan and the
  shared summarizeProjectProfile model call, but feed it only what the
  project declares (runtime versions probed on this machine belong to
  the machine profile); write it into the project unit and refresh its
  snapshot; skip the model when the scan is unchanged and drop the
  result when the unit was deleted meanwhile
- at most one pending profile job per unit, and a unit scanned in the
  last ten minutes is not scanned again (each session open asks for
  one); each request carries its own scan id because the immutable
  dedupe index never frees a key
- the old project-profile pipeline is unchanged apart from calling the
  extracted summarizeProjectProfile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new-L3 project profile fingerprint leaves out the runtime versions probed on
this machine (they belong to the machine profile), and an unchanged fingerprint
skips the model even when the last summary found nothing worth a profile. The
scan for it does not run the declared runtimes at all.

A unit whose snapshot the user archived is not injected, and rewriting the
snapshot keeps it archived. Deleting a snapshot and resetting its unit happen in
one transaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wherever a task closes (session close, new task, ended topic, idle timeout),
its turns are frozen for the new L3, only through that task so a task still in
progress is not cut; a conversation that simply stops therefore still reaches
L3. With the switch off nothing changes.

Older evidence never overturns newer state: a replace or delete whose cited
evidence is older than the item's last confirmation is dropped, and what was said
before a unit reset does not refill it, even when frozen or rerun later. The
worker's next wake-up uses the same readiness rules as leasing (scope order, L1
summaries), so a job it cannot lease does not make it spin. A batch is shown the
items removed on evidence newer than some of its own, with when (a batch rerun
late, history scanned late, or one spanning the removal), so older evidence does
not bring them back. Each lease of a batch job carries a token: a run that
outlived its lease neither writes nor changes the job, and a run that finds its
batch dead-lettered drops its result. Replace and delete skip items already gone,
and ops citing a turn or L1 deleted during the model call are dropped. A machine probe that was running when
the machine was reset is dropped. A snapshot is rewritten only when what it shows
changed, and project snapshots move only when the machine's did, or when the
machine snapshot is archived (so clients drop the cached machine part).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Temporary model failures of L3 jobs wait 1, 5, 15, 30 and 60 minutes before
retrying (runAfter) and the sixth goes to dead letter; any other failure (a
rejected answer, a bug) would likely repeat, so it gets one retry. Later batches
then continue: no failure leaves a job that holds back its scope until restart. Configuration
problems (bad key, quota 40309) and an unreachable model endpoint (offline) wait
30 minutes without using an attempt, and L3 jobs are not leased at all while no
evolution model is configured. Each dead-lettered new-L3 batch is rerun once
automatically when the worker starts. Adds POST /api/v1/l3/failed-batches/retry,
which resets the user's dead-letter l3_unit_update jobs and batches in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the new L3 on, conversations first opened by an agent-source scan use the
L3 v2 session protocol with Work Memory disabled, so their turns register as L3
input like Hook sessions; with it off, scans stay out of L3 as before, and a
session scanned while it was on never goes to the old L3 after it is switched
off: its turns wait for the new L3.

Every captured turn has a durable owner: with the new L3 on, the existing Work
Memory idle flush (one re-armed job per session, runAfter two hours after the
last turn) is armed for every v2 session, scanned ones included, and when it
comes due it freezes the session's remaining turns into L3 (trigger
session_idle). A conversation that just stops therefore still reaches L3 without
a close or a later turn. On worker start and after an import, open sessions with
unfrozen turns get it armed and closed ones are frozen right away.

The scan end is the fast path: when a scan pass finishes a source, the sessions
it touched are frozen (trigger scan_pass) and the worker is woken. The desktop
scan run by the backend (persistent scan, including a cancelled pass) and hosted
imports (importManaged) do the same through POST
/api/v1/l3/scan-sessions/freeze, also when a pass or import stops early; it
freezes the caller's own sessions and scanned sessions of its source. Freezing
only writes rows, so neither scans nor imports wait on L3. Existing sessions and
detached legacy history keep their protocol.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #739
Task: 6f43c128ec9c9410
Base: v1.2.7
Head: gh-590-scan-l3

🔍 OpenCodeReview found 53 issue(s) in this PR.

⚠️ 3 warning(s) occurred during review.


1. App/backend/src/adapters/outbound/memory-client/http-memory-client.ts (L193)

sessionIds is forwarded as a single payload, but the server contract caps it at 10,000 entries (L3ScanSessionsFreezeRequestSchema). Callers (ingestion-service/agent-source-service) accumulate every session touched by a full history import, so an import with >10k sessions fails with invalid_argument only at the very end (inside the finally block) and none of the imported sessions get frozen. Consider chunking the ids into batches of <=10_000 and aggregating batches.

💡 Suggested Change

Before:

      const body = { requestId: randomUUID(), adapterId: `agent-source:${source}`, source, sessionIds };

After:

      const sessionIdsChunkLimit = 10_000;
      let batches = 0;
      for (let offset = 0; offset < sessionIds.length; offset += sessionIdsChunkLimit) {
        const chunk = sessionIds.slice(offset, offset + sessionIdsChunkLimit);
        const body = { requestId: randomUUID(), adapterId: `agent-source:${source}`, source, sessionIds: chunk };
        batches += (await request("POST", "freezeScanSessions", L3ScanSessionsFreezeResponseSchema, { body, context })).batches;
      }
      return { batches };

2. App/backend/src/services/ingestion-service.ts (L349)

stats.sessionIds accumulates one entry per turn (duplicates included) for the whole import and is only de-duplicated in the finally, so a large import keeps a redundant array proportional to turn count. The field is also exported on IngestionStats but no caller reads it (importManaged/syncManaged only consume memoryIds/errors) — the freeze below is the sole consumer. Collecting into a Set would both bound the memory growth and remove the need for a second de-dupe pass.


3. App/memmy-agent/src/entrypoints/cli/commands.ts (L1164)

startL3UserProjectSync is a required method on AgentLoop, so the optional chaining is redundant here and would silently swallow a future rename/removal of the method instead of failing to compile. Call it directly (the method already no-ops when projectStore is absent).

💡 Suggested Change

Before:

  loop.startL3UserProjectSync?.();

After:

  loop.startL3UserProjectSync();

4. App/memmy-agent/src/memmy-memory/l3-user-project-sync.ts (L64)

standaloneRoots() is called outside the try/catch that guards readProjects(), so any throw from it rejects pushIfChanged(). Since start() and the interval both call void this.syncNow(), that becomes an unhandled promise rejection, contradicting the class contract "never throws". Include the call (and the mapping) in the guarded block, or make syncNow() reject-safe.

💡 Suggested Change

Before:

    const standaloneRoots = this.options.standaloneRoots();

After:

    let standaloneRoots: string[];
    try {
      standaloneRoots = this.options.standaloneRoots();
    } catch {
      return false;
    }

5. App/memmy-agent/src/memmy-memory/l3-user-project-sync.ts (L78-L82)

Both failure paths return false silently. Unlike MemmyMemoryHook (which records hook.lastError), a permanently failing endpoint or a reject response from Memory (e.g. a path rejected by AbsolutePathSchema) is invisible to users and diagnostics. Consider recording the last error/status (and warning once) so the sync can be debugged.


6. App/memmy-agent/src/memmy-memory/register.ts (L74-L79)

adapterId, source and namespace.profileId are hardcoded here, duplicating ADAPTER_ID/SOURCE/PROFILE_ID used by MemmyMemoryHook. If either value changes, the hook and this sync will disagree on the namespace without any compile error. Export those constants from hook.ts and reuse them.


7. Memory/src/agent-source/runtime.ts (L835)

The freeze failure is swallowed with a completely empty catch (no log, no analytics). The backend counterparts (ingestion-service.ts, agent-source-service.ts) at least emit console.warn("[agent-source] l3.freeze_failed ..."). Without any signal here, a persistent freeze failure (e.g. DB error) silently never produces L3 batches and is undiagnosable from logs. Please log the error in the catch block, consistent with the backend paths.

💡 Suggested Change

Before:

      if ((service.freezeScannedL3Sessions?.(scannedSessionIds) ?? 0) > 0) scheduleWorker?.();

After:

      if ((service.freezeScannedL3Sessions?.(scannedSessionIds) ?? 0) > 0) scheduleWorker?.();
    } catch (error) {
      // A failed freeze must not hide the scan's own result; the idle sweep hands these Sessions over later.
      console.warn(`[agent-source] l3.freeze_failed ${error instanceof Error ? error.message : String(error)}`);

8. Memory/src/agent-source/runtime.ts (L999)

scannedSessionIds collects one entry per captured turn, so an incremental scan over a long conversation grows this array proportional to turn count (with the same sessionId repeated many times). Only freezeScannedL3Sessions de-duplicates it at the very end. Consider collecting into a Set<string> (or checking membership before pushing) so a large scan does not hold a redundant array; this also keeps the contract of "Sessions this pass touched" accurate.

💡 Suggested Change

Before:

        if (result.result?.sessionId) scannedSessionIds.push(result.result.sessionId);

After:

        if (result.result?.sessionId) scannedSessionIds.push(result.result.sessionId); // collected into a Set in the caller instead

9. Memory/src/algorithm/plugin-algorithms.ts (L3449-L3451)

This new schema-2 meta is a near-verbatim copy of the existing v2Fields branch below (same structure/summary/body/vec/schemaVersion literal), and it re-implements the l3Field extraction with unchecked as casts instead of the helpers already exported by service/l3-field/snapshot-memory.ts. field.kind / field.unit_key are read from a blind cast, so any memory whose internal_info.l3_field is not an object would throw here. Prefer reading via the existing recordField() helper (and l3FieldSnapshotUnitKey) and extracting the shared meta builder so both schema-2 paths stay in sync.


10. Memory/src/algorithm/plugin-algorithms.ts (L3456)

field.unit_key as string bypasses the type system and would interpolate l3_field:undefined into domainKey if unit_key were ever absent/renamed, silently polluting the domain key used by the world-model page. Use the exported l3FieldSnapshotUnitKey(memory) accessor (which already validates the string) instead of the cast.

💡 Suggested Change

Before:

      domainKey: `l3_field:${field.unit_key as string}`,

After:

      domainKey: `l3_field:${l3FieldSnapshotUnitKey(memory) as string}`,

11. Memory/src/algorithm/plugin-algorithms.ts (L5260)

Excluding snapshots only here leaves every other retrieval-index path treating them as searchable: isMemoryReadyForRetrieval()/hasMemoryRetrievalIndex() now return true for snapshots (they were null meta before), vectorChannelsForMemory scores them via the new schema-2 meta, and retrievalDocumentForMemory() now renders a different document for them, which changes retrievalDocumentSourceHash() and will trigger needless re-embeds of snapshot memories that are supposed to only be injected at session start. Consider also guarding snapshots in isMemoryReadyForRetrieval()/retrievalDocumentForMemory() so the "never enter retrieval" invariant holds in one place.


12. Memory/src/algorithm/plugin-algorithms.ts (L3463)

legacy_fields is written with general_rules_and_safety_constraints: null always, and project_environment_profile/project_contract/domain_knowledge can also all be null (e.g. a newly created machine unit with no profile text/confirmed items). renderL3WorldModelFields drops empty sections, so summary becomes "" even though the snapshot memory has real text in internal_info.summary / memoryValue. That empty string is surfaced directly to the world-model detail payload (read-model/memory.ts maps worldModel.summary), so the detail panel shows a blank summary for such snapshots. Consider falling back to the stored summary/body.

💡 Suggested Change

Before:

      summary: renderL3WorldModelFields(snapshotFields),

After:

      summary: renderL3WorldModelFields(snapshotFields) || stringField(internal, "summary") || memory.memoryValue,

13. Memory/src/config/index.ts (L1322-L1325)

inactiveRetentionDays is accepted without any lower bound. The worker computes the purge cutoff as Date.parse(at) - days * 86_400_000, so 0 (or any negative value) puts the cutoff at/after "now" and the next worker start deletes every inactive L3 item. It is also the only field of this block that cannot be set from the environment, unlike enabled. Clamp it and expose the env var for parity.

💡 Suggested Change

Before:

      inactiveRetentionDays: numberValue(
        l3FieldCognition.inactiveRetentionDays,
        DEFAULT_MEMMY_CONFIG.algorithm.l3FieldCognition.inactiveRetentionDays
      )

After:

      inactiveRetentionDays: Math.max(
        1,
        numberValue(
          l3FieldCognition.inactiveRetentionDays ?? numberEnv("MEMMY_L3_FIELD_INACTIVE_RETENTION_DAYS"),
          DEFAULT_MEMMY_CONFIG.algorithm.l3FieldCognition.inactiveRetentionDays
        )
      )

14. Memory/src/contracts/memory-l3-world-model.ts (L92-L95)

The schema accepts duplicated rootPath entries, while replaceUserProjects inserts with ON CONFLICT(user_id, root_path), so duplicates collapse into one row. replaceL3UserProjects reports count: request.projects.length, which then overstates what was actually stored. Either reject duplicate roots here or return the real stored count.


15. Memory/src/service/l3-field/evidence-fragments.ts (L40-L41)

sessionUnits() returns the unit key without recording its checkout root in rootOf. Consequently mostSpecificUnits() treats the session-folder key as rootless and always keeps it, so when a turn has a pathless call (session fallback) plus a call in a nested repository, the user fragment's candidateUnitKeys contains both the parent folder and the nested project. Evidence is then written into the parent unit as well, contradicting the file's stated per-project isolation. Record project.checkoutRoot in rootOf here (the map must be declared before this closure is called) so the parent can be pruned.

💡 Suggested Change

Before:

    const project = input.resolve(input.sessionCwd, "session_dir");
    return project ? [input.unitKeyOf(project)] : [];

After:

    const project = input.resolve(input.sessionCwd, "session_dir");
    if (!project) return [];
    const key = input.unitKeyOf(project);
    rootOf.set(key, project.checkoutRoot);
    return [key];

16. Memory/src/service/l3-field/evidence-fragments.ts (L256-L261)

git -C <dir> only records <dir> as a target; cwd is not updated. For a command such as git -C repo add src/file.ts, the later relative argument src/file.ts is resolved against the session directory instead of repo, so the path can be attributed to the wrong project. Track a local effective cwd for the remaining arguments of this subcommand (without mutating the shell cwd used after &&).


17. Memory/src/service/l3-field/evidence-fragments.ts (L33)

input.sessionProjectRoot is declared here but never read inside buildEvidenceFragments; the caller already uses it when constructing the resolve callback. This is a dead parameter and makes the contract look like the fragment builder resolves tool paths itself. Remove it from the input type.


18. Memory/src/service/l3-field/evidence-fragments.ts (L151)

This is a nested ternary, which the project style rules disallow. The same nested-ternary pattern also appears in unquoteJsString and toolOutputText. Use an explicit if/else to keep the parsing branches readable.

💡 Suggested Change

Before:

    const command = typeof raw === "string" ? raw : Array.isArray(raw) ? raw.filter((part) => typeof part === "string").join(" ") : null;

After:

    let command: string | null = null;
    if (typeof raw === "string") command = raw;
    else if (Array.isArray(raw)) command = raw.filter((part) => typeof part === "string").join(" ");

19. Memory/src/service/l3-field/evidence-fragments.ts (L208)

This both nests ternaries and decodes only \n and \t. Other standard JS escapes such as \r, \xNN, and \uXXXX are returned as their literal letter, so code-mode tool paths containing those escapes are resolved incorrectly. Use a switch/decoder that handles the supported escape forms (or reuse a real JS-string parser).

💡 Suggested Change

Before:

  return body.replace(/\\(.)/g, (_, char: string) => (char === "n" ? "\n" : char === "t" ? "\t" : char));

After:

  return body.replace(/\\(.)/g, (_, char: string) => {
    switch (char) {
      case "n": return "\n";
      case "t": return "\t";
      case "r": return "\r";
      default: return char;
    }
  });

20. Memory/src/service/l3-field/git-identity.ts (L59)

decodeURIComponent throws URIError on malformed percent-encoding (e.g. a hand-edited origin URL containing %zz or %E0%A4%A). readGitIdentity does not catch this, so it propagates through resolveProject and can break session/project resolution. Wrap the decode and fall back to null when the URL cannot be decoded.

💡 Suggested Change

Before:

    repoPath = decodeURIComponent(parsed.pathname);

After:

    try {
      repoPath = decodeURIComponent(parsed.pathname);
    } catch {
      return null;
    }

21. Memory/src/service/l3-field/machine-profile.ts (L157)

On Windows the required npm probe (and optional pnpm/yarn, which are .cmd shims) can never succeed: Node's execFile with shell: false does not apply PATHEXT and cannot launch .bat/.cmd files (documented Node limitation). The machine profile will therefore always report npm as "未检测到" on Windows even when it is installed. Resolve .cmd/.exe via PATHEXT (or where) before spawning, or use a controlled cmd.exe /c fallback for shell shims.


22. Memory/src/service/l3-field/machine-profile.ts (L69)

Every probe failure (login-shell timeout, spawn error, transient ENOENT) is flattened to null, so probeMachine can return a profile in which every tool is "not detected" even though nothing is wrong. refreshL3MachineProfile compares only the fingerprint and will then persist that degraded profile over a previously correct one. Please distinguish "probe failed" from "tool not installed" (e.g. propagate a failure flag / require the login-shell probe to have produced output before persisting) instead of treating all spawn errors as absent tools.


23. Memory/src/service/l3-field/project-resolver.ts (L165)

The doc says a usable root may not be "a top-level container folder", but the container list only covers home and its three subfolders. The parent of home (/Users, /home) and mount containers such as /Volumes pass this check, so a session opened at such a directory creates a project unit named "Users"/"Volumes" and later attaches profiles and items to it. Reject roots that are ancestors of home (and filesystem mount roots) as well.


24. Memory/src/service/l3-field/project-resolver.ts (L49)

resolveProject performs synchronous realpathSync/existsSync/statSync for the target, home, every temp root, every standalone root and every user project, and readGitIdentity walks up the tree reading .git/config on each call. unit-update-pipeline.buildGroups calls resolve() once per evidence fragment path, so a single batch can issue hundreds of blocking FS syscalls on the event loop. Memoize the normalized home/temp/standalone/user-project roots and git lookups per batch/resolver context.


25. Memory/src/service/l3-field/unit-ops.ts (L64)

Nested ternary for targets violates the project convention (no nested ternaries) and hides the add/replace/delete/confirm branching. Flattening it into an if/else keeps it readable and equivalent.

💡 Suggested Change

Before:

    const targets = raw.op === "replace" ? raw.ids ?? [] : raw.op === "add" ? [] : [raw.id ?? ""];

After:

    const targets: string[] =
      raw.op === "replace" ? raw.ids ?? [] : raw.op === "add" ? [] : [raw.id ?? ""];
    // (prefer an explicit if/else when touching this line:) 
    // let targets: string[] = [];
    // if (raw.op === "replace") targets = raw.ids ?? [];
    // else if (raw.op !== "add") targets = [raw.id ?? ""];

26. Memory/src/service/l3-field/unit-ops.ts (L138)

mentionsProject silently skips the name check when the project's displayName is shorter than 4 characters, so a machine item can freely mention a repo/folder named e.g. "app", "web" or "api" — exactly the short names that are common for repos — defeating the "machine items must not mention a project" rule this validation exists to enforce. Consider checking short names with stricter word-boundary matching (e.g. require exact whole-token match plus the project's checkoutRoot) instead of skipping them entirely.

💡 Suggested Change

Before:

  if (!name || name.length < MIN_REPOSITORY_NAME_LENGTH) return false;

After:

  if (!name) return false;
  if (name.length < MIN_REPOSITORY_NAME_LENGTH) {
    // Too short for a loose word-boundary match, but a whole-token mention is still a mention.
    return new RegExp(`(^|[^\\p{L}\\p{N}_-])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}($|[^\\p{L}\\p{N}_-])`, "u")
      .test(lower) && project.checkoutRoot ? false : false;
  }

27. Memory/src/service/l3-field/unit-update-pipeline.ts (L251)

buildInput re-reads the session with two non-null assertions (job.sessionId! and getSession(...)!). process() already loaded and validated the session (session.userId === batch.userId) and passed it to buildGroups; if the session is deleted between the two reads, this line throws a bare TypeError instead of an L3WorldModelTerminalEvidenceError, so an unrecoverable batch would be retried (or rescheduled) rather than dead-lettered cleanly. Thread the validated session (and batch.sessionId) into buildInput instead of re-fetching with !.

💡 Suggested Change

Before:

    const session = repos.runtime.getSession(job.sessionId!)!;

After:

    const session = repos.runtime.getSession(job.sessionId ?? "");
    if (!session) throw new L3WorldModelTerminalEvidenceError(`missing session: ${job.sessionId ?? ""}`);

28. Memory/src/service/l3-field/unit-update-pipeline.ts (L216)

let first; is an implicitly-any declaration, so first.ops passed to validateL3UnitOps loses the typing that completeStrictJson → parseL3UnitOutput provides ({ reason: string; ops: RawOp[] }). Annotate it (or export RawOp and use it) so a shape change in the parser surfaces as a compile error rather than at runtime.

💡 Suggested Change

Before:

    let first;

After:

    let first: Awaited<ReturnType<typeof ask>>;

29. Memory/src/service/l3-field/unit-update-pipeline.ts (L52)

OVER_LENGTH_UNITS = 150 does not match the size rule the prompt enforces ("about 60 characters (or 30 Chinese characters) at most"), and countL3Units counts CJK characters as 1 but Latin texts as whitespace-separated words, so the metric is not comparable to the documented limit either. As written, items that clearly violate the prompt's rule are never flagged, and the audit event only fires for items ~5x over the documented size. Align the threshold with the prompt (or document that this is only a gross-overrun alarm).


30. Memory/src/service/memory-service.ts (L1448-L1449)

sessionsWithUnfrozenTurns() is unbounded (no LIMIT) and every pending session is frozen/armed inside a single synchronous transaction. This method runs on every worker startup, on every importBundle and on config reload, so on a large database it becomes a long blocking write transaction. Consider batching (limit + loop) and/or skipping already-frozen sessions cheaply.


31. Memory/src/service/memory-service.ts (L992-L993)

The concurrent-reset guard only covers a reset that happens during the probe. If the unit was already reset before this call, resetBefore is the old deleted_at, the comparison still matches, and because markUnitDeleted cleared profile_fingerprint the fingerprint check passes too — so setProfile + refreshSnapshot recreate the snapshot the user just deleted. Skip the write when the unit is marked deleted (e.g. if (!unit || unit.deletedAt) return;) so a reset unit stays empty until new evidence rebuilds it.


32. Memory/src/service/memory-service.ts (L1387-L1388)

The desktop-supplied projects / standaloneRoots are persisted verbatim: no absolute-path check, no existence/duplication check and no cap. The project resolver later walks these roots, so a malformed or unexpected value (e.g. /, a home directory, a duplicate) leads to filesystem scans and cross-project capture. Validate/normalize and dedupe the paths here or in the HTTP contract before storing them.


33. Memory/src/service/project-environment/project-environment-service.ts (L82)

requestL3UnitProfile bypasses the repository layer (ProjectEnvironmentRepository.requestScan) and hand-writes raw SQL over payload_json, duplicating the queueing/dedupe rules with knowledge of the internal payload shape. In addition, the json_extract(payload_json, '$.mode'/'$.unitKey') predicates cannot use any index, so every session open scans evolution_jobs. Prefer a repository method (and an indexed discriminator) so the payload contract stays in one place and the lookup stays cheap as job history grows.


34. Memory/src/service/project-environment/project-environment-service.ts (L89)

at is an optional caller-supplied value. A malformed value gives Date.parse(at) === NaN, so toISOString() throws RangeError on the session-open path; a valid non-UTC value (e.g. 2026-10-28T21:23:00+08:00) is converted to a Z cutoff while createdAt/updatedAt store at verbatim, so the SQL string comparison updated_at >= ? compares differently formatted timestamps and silently computes a wrong 10-minute window. Drop the optional at (the only caller omits it) or normalize both the stored timestamp and the cutoff through one formatter.


35. Memory/src/service/project-environment/project-environment-service.ts (L183)

The scan uses the checkoutRoot captured in the job payload, while the unit's current checkoutRoot may have changed since the job was queued (project moved, or the same git identity re-resolved to another checkout). A missing path makes the job fail repeatedly into dead letter; a still-existing old path writes a profile derived from a different checkout onto this unit. Re-read latest.checkoutRoot before scanning and drop the job when it no longer matches (or when the path no longer exists).


36. Memory/src/service/session/session-turn-service.ts (L1667-L1669)

The scanned session is closed but the conversation→session mapping still points at it, and closeSession is called without error handling inside turn ingestion. The next scan of the same conversation resolves to a closed session and returns source_session_closed (or opens a fresh duplicate session), splitting the conversation's L3 history; a failing close aborts the live turn. Repoint the conversation mapping to live.id after the takeover and guard the close so it cannot fail the turn.


37. Memory/src/service/worker/job-handlers.ts (L280-L283)

Force-dead-lettering here and then rethrowing makes the outer failLeasedWorkerJob see a job whose status is no longer leased, so holdsJobLease returns false: the job is reported as job.lease_lost (a misleading warning) and appendJobChange(..., "dead_letter") is never emitted, so the terminal failure never reaches the change feed/clients. Either let failLeasedWorkerJob handle the terminal case (it already forces dead letter via l3FailureHandling → dead_letter) or append the change entry here before rethrowing.


38. Memory/src/service/worker/job-handlers.ts (L402)

Nested ternary, which the project style rules forbid. A lookup table keeps this readable and makes the closed-task trigger mapping explicit.

💡 Suggested Change

Before:

    trigger: trigger === "idle_timeout" ? "episode_idle_close" : trigger === "session_closed" ? "session_close" : "new_task",

After:

    trigger: CLOSED_TASK_TRIGGERS[trigger],

39. Memory/src/service/worker/job-handlers.ts (L669)

project_environment_profile is also used by the pre-existing project-environment pipeline (mode l3_field only for the new L3). Including it here changes retry semantics for that older pipeline too: configuration errors now suspend without ever consuming an attempt, and any non-transient failure dead-letters after a single extra run instead of honouring its own max_attempts. If that is intended for the legacy path as well, it should be stated; otherwise gate the retry policy on the new-L3 job/payload.mode.


40. Memory/src/service/worker/worker-runner.ts (L378)

continue only skips embedding: the snapshot memory has already been re-indexed into FTS on the line above. Retrieval has no l3_field exclusion and renders L3 hits as injectable "Environment Knowledge", so the rendered machine/project profile text stays searchable and can pollute memory-search ranking (and surface host details). Either skip the FTS reindex for these memories as well, or exclude them explicitly in the retrieval filters.


41. Memory/src/storage/l3-field-repository.ts (L241-L242)

refreshSnapshot is not wrapped in a transaction although writeSnapshot performs three writes: the snapshot memory insert/update, the l3_units.snapshot_memory_id link update, the memory_change_log insert, plus (for machine units) one full snapshot write per project. refreshSnapshot is also called from non-transactional callers (e.g. l3-field-context.ts / project-environment-service.ts). If the link update or the change-log insert fails, the memory row is orphaned: snapshot_memory_id stays NULL while a row with memory_key = world_model:l3_field:<unitKey> exists (memory_key is not unique, see schema.ts:62), so the next refresh inserts a second snapshot memory for the same unit. Suggest executing the whole body inside this.db.transaction(...)().


42. Memory/src/storage/l3-field-repository.ts (L493)

Including !existing.deletedAt in the early-return condition means that when the snapshot memory is soft-deleted the code falls through to this.memories.update({ ...memory, deletedAt: null }), i.e. it revives a memory the user deleted and resurrects the unit's old cognition (any path that soft-deletes the snapshot without going through markUnitDeleted, e.g. retention/console deletes, would be undone on the next session start). Better to treat a deleted snapshot as "no snapshot": clear the link/return existing instead of rewriting it as active.


43. Memory/src/storage/l3-field-repository.ts (L495)

This no-op detection compares two JSON strings of the freshly built object against the value re-parsed from properties_json. JSON.stringify is key-order sensitive: if the stored document was ever re-serialized/imported with a different key order, the strings differ even though the data is identical, so every refresh writes a new version and a change-log row — exactly the churn the comment says it is avoiding. Use the stable serializer already imported here (canonicalJson) for both sides.

💡 Suggested Change

Before:

      toJson(existing.properties.internal_info) === toJson(memory.properties.internal_info)

After:

      canonicalJson(existing.properties.internal_info) === canonicalJson(memory.properties.internal_info)

44. Memory/src/storage/l3-field-repository.ts (L169)

No index covers this predicate/ordering: idx_l3_items_inactive is on inactive_at (schema.ts), while the query filters and sorts on removed_evidence_at. Every batch apply therefore scans/sorts all inactive items of the involved units. Add a partial index such as CREATE INDEX idx_l3_items_removed ON l3_items (unit_key, removed_evidence_at) WHERE status = 'inactive'.


45. Memory/src/storage/l3-field-repository.ts (L310)

The rows for this user were just deleted a few lines above (DELETE FROM l3_user_projects WHERE user_id = ?), so this conflict clause can never fire — dead code that suggests a dedupe guarantee that no longer exists. Either use a plain INSERT or drop the DELETE and keep the upsert.


46. Memory/src/storage/l3-field-repository.ts (L263)

force = true rewrites every project snapshot on the machine even when the project's own rendered content is unchanged: each iteration bumps version, updates updated_at, re-indexes FTS (skipped for these rows) and inserts a memory_change_log row containing the full previous/next memory objects. On a machine with many projects this turns one machine fact update into N memory updates plus N large log rows, and makes clients refetch all of them. Consider signalling the context change some cheaper way (or at least keep the forced version bump but skip the change-log payload when content is identical).


47. Memory/src/storage/l3-field-repository.ts (L76)

repositories.ts imports isL3FieldSnapshotMemory directly from ../service/l3-field/snapshot-memory.js, and no caller uses this re-export — it adds a second entry point for the same functions from the storage layer. Remove it (and the l3FieldSnapshotUnitKey re-export) unless an external consumer is planned.


48. Memory/src/storage/repositories.ts (L5056-L5058)

This issues one SQL statement per trace sequence inside the import transaction. A batch range is bounded by raw turns, not traces, so a single chunk can span hundreds of traces; importing a bundle with many kept units/batches turns into thousands of round trips (each also scanning json_each over the covering batches). Compute the first uncovered seq with a single query, e.g. SELECT MIN(t.trace_seq) FROM l3_world_model_input_traces t WHERE t.session_id = ? AND t.trace_seq BETWEEN ? AND ? combined with the coverage NOT EXISTS, and null-coalesce the result.


49. Memory/src/storage/repositories.ts (L8119-L8120)

bundleRowL3UnitKey runs for every row of every table in BUNDLE_TABLES, and for memories it JSON-parses the full properties_json of each row (L1/L2 memories included) only to discover that it is not an L3 field snapshot. Guard with a cheap check first (row.memory_layer === 'L3' / memory_key prefix world_model:l3_field:) before parsing; imports of large bundles otherwise pay an extra parse per memory row.


50. Memory/src/storage/repositories.ts (L3474-L3475)

The "excluding l3_unit_update also excludes l3_field profile jobs" rule is now duplicated verbatim in leaseJobs and here. Since both must stay in sync with scopedJobReadySql/the lease filters, extract it into a small helper next to scopedJobReadySql() (e.g. excludedJobFilter(excluded)) and reuse it in both queries.


51. Memory/src/storage/l3-field-repository.ts (L438)

A brand-new unit (no profile, no active items yet) is written as an activated memory with an empty memoryValue. The existing v2 writer archives empty snapshots (const status = memoryValue ? "activated" : "archived" in L3WorldModelRepository.upsertField), so this new path publishes an empty activated L3 world-model row that is synced to clients and rendered as a blank card. Consider archiving when memoryValue is empty (still keeping archived for an already archived snapshot).

💡 Suggested Change

Before:

    const status = existing?.status === "archived" ? "archived" as const : "activated" as const;

After:

    const status = existing?.status === "archived" || !memoryValue ? "archived" as const : "activated" as const;

52. Memory/src/storage/repositories.ts (L4926-L4927)

The whole-unit skip only covers the tables mapped by bundleRowL3UnitKey (l3_units, l3_items, l3_unit_batches, l3_unit_update/profile jobs, snapshot memories). Rows that reference those skipped objects are still imported row by row — notably memory_change_log entries for the kept unit's snapshot memory. Their memory_id/entity_id are the bundle's memory ids, which were not inserted here (the kept unit keeps its own snapshot id), so the local change log ends up with entries pointing at a memory that does not exist and clients receive update events for an unknown id. Either map memory_change_log rows to their unit key in bundleRowL3UnitKey or drop rows whose memory_id was skipped in this import.


53. Memory/src/storage/schema.ts (L753-L754)

l3_unit_batches grows by one row (plus two JSON id arrays) for every frozen chunk of every session, and neither this repository nor this migration provides a retention path — compare L3FieldRepository.purgeInactive(), which prunes l3_items but has no batch equivalent. Please confirm a prune job exists elsewhere, or add one (e.g. delete status = 'applied' batches older than N days) so the table does not grow unbounded.

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: ENV ISSUE

The test environment encountered an issue that requires manual attention.

Details: Executor error: Command failed: git clone --depth 1 --branch gh-590-scan-l3 git@github.com:Anson-Se/memmy-agent.git /data/test-workspaces/6f43c128ec9c9410/repo
Cloning into '/data/test-workspaces/6f43c128ec9c9410/repo'...
Branch: gh-590-scan-l3

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants