Skip to content

feat(sync): put --watch one-way directory watch (minimal tier, carved from Phase 2 sync drive) #110

Closed

Description

@PeterGuy326
schemaVersion: requirement-record.v1
revision: R1
status: needs-design
priority: P2
productOwner: "@PeterGuy326"
technicalOwner: "unassigned"
userOutcome: "A user points mem at a local directory and new or changed files flow into the netdisk automatically via a one-way watch with a per-cycle import report — while the full sync drive stays a recorded Phase 2 item."
requirements:
  - REQ-001
  - REQ-002
  - REQ-003
  - REQ-004
acceptanceCriteria:
  - AC-001
  - AC-002
  - AC-003
  - AC-004
parent: null
dependencies: []
supersedes: []
lastDecisionAt: null

User problem and observable outcome

Founder query "does mem include scanning?" surfaced a baseline expectation: users expect to point at a folder and have content flow in automatically (netdisk baseline; GOAL promises "like an ordinary netdisk"). Current state (product-verified 2026-08-29): one-time recursive import exists (mem put -r); the one-way daemon mem put <path> --watch is designed in SPEC.md:604 but unimplemented (no watch anywhere in code, no --watch flag on put); the full sync drive is explicitly Phase 2 (SPEC.md:91/1045/1140, GOAL.md:123 unchecked). No tracking issue existed — this record is it. Implementation truth belongs to Tech P9.

Observable outcome: a watched directory yields automatic one-way ingestion of new/changed files with a per-cycle import report; the full sync drive remains recorded as Phase 2 with revival preconditions.

Requirements

  • REQ-001: One-way watch daemon — implement the SPEC.md:604 shape: mem put <path> --watch watches the designated directory and ingests new/changed files into mem; strictly local→mem one-way; flag semantics align with the SPEC design (if implementation must diverge, tech raises a SPEC-revision decision).
  • REQ-002: Per-cycle import report — each watch cycle emits an import report (added/unchanged/failed counts plus item locators); individual failures fail-closed with stable codes while the daemon continues; report content follows evidence vocabulary.
  • REQ-003: Boundary hard constraints — no write-back to the local disk, no deletion propagation (local deletions do not remove already-ingested mem content), no bidirectional sync, conflict merging, or remote sync — all explicitly Phase 2 (SPEC.md:91/1045/1140, GOAL.md:123).
  • REQ-004: Canon consolidation — this record is the formal tracking item for the scan/auto-sync family: the Phase 2 full sync drive stays deferred with recorded revival preconditions (rclone-lib route decision, adjacent to bundle v2 / incremental packages per GOAL.md:95); SPEC/GOAL/README scattered wording cross-references this record.

Acceptance criteria

  • AC-001: Watch fixture — new/changed files under a watched directory are ingested; report counts correct.
  • AC-002: Failure fixture — a failing file yields a stable code in the report; the daemon continues; no crash, no silent drop.
  • AC-003: Boundary fixture — local deletion does not propagate; no disk write-back observable; no bidirectional behavior.
  • AC-004: SPEC alignment — flag semantics match SPEC.md:604, or a tech-raised SPEC-revision decision is recorded.

Non-goals and forbidden shortcuts

  • No bidirectional sync, conflict resolution, deletion propagation, or remote sync (Phase 2, rclone route).
  • No mobile/share-link/preview scope creep (SPEC Phase 1 cut list holds).
  • No re-indexing behavior changes beyond what ingestion already triggers.

Lifecycle, status, priority, blockers, and open decisions

  • status=needs-design; priority P2; technicalOwner unassigned (assign at implementation kickoff; scheduling adjacent to bundle v2 / incremental packages, GOAL.md:95 migration-completeness cluster).
  • Blockers: none.
  • Open decisions: (1) daemon lifecycle (foreground vs background/service mode, restart behavior); (2) report shape and persistence (stdout only vs ledgered); (3) change detection semantics (mtime/hash) and scope of "changed"; (4) interaction with existing put path semantics (shared ingestion path vs watch-specific path) — tech adjudication.

Evidence plan

  • Fixtures first (watch/failure/boundary), deterministic; daemon behavior evidenced per three-axis discipline.
  • Evidence input: ops observation memo (2026-08-29) plus product-side anchor verification (SPEC.md:604/91/1045/1140, GOAL.md:123, cmds_file.go:91).

Decisions

  • DEC-MEM-110-001 (2026-08-29T03:54:00Z): product ruling recorded per founder-approved intake (2026-08-29): minimal tier promoted as P2 (one-way put --watch, carved from the full sync drive); full sync drive remains Phase 2 with revival preconditions recorded here; ops observation as evidence input; implementation truth belongs to Tech P9.

Revision history

  • R1 (2026-08-29T03:54:00Z): initial record created per founder-approved ruling (scan/auto-sync family consolidated).

Activity

  1. added
    type:featureA new user-facing capability
    priority:p2Useful backlog work outside the immediate critical path
    on Aug 29, 2026
  2. waterbro-8 commented on Aug 30, 2026

    @waterbro-8
    Collaborator

    裁决摘要(技术 P9)

    owner 已定;#110 的 4 条 open decisions 全部出解;与 #103 的关系裁定为共用摄取内核、不共用写入面("一个内核两个 sink")。取证基线:main @ 419a420,另含在途 PR #108。REQ-001 的"changed"口径收窄另立 R3(本条只记录不弱化既有要求的 additive 部分)。

    schemaVersion: requirement-decision.v1
    canonicalIssue: "https://github.com/fullstack-ai-infra/mem/issues/110"
    previousRevision: R1
    resultingRevision: R2
    decisionType: additive
    oldDecision: "technicalOwner=unassigned; openDecisions=[daemon lifecycle, report shape and persistence, change detection semantics, shared-vs-watch-specific ingestion path]; dependencies=[]"
    newDecision: "technicalOwner=@waterbro-8 (independent reviewer @PeterGuy326 or @Bindy-lbb); D-1 foreground-only daemon lifecycle with single-instance lock and SPEC 7.1 exit-code mapping; D-2 per-cycle report on stdout (--format json) plus local capped JSONL report log under the CLI state root, no server-side ledger in this record; D-4 adjudicated as one shared ingestion core with two sinks and a new predecessor https://github.com/fullstack-ai-infra/mem/issues/111, recorded as dependencies=[that issue at R1]; AC-005..AC-008 added for lifecycle, report vocabulary, report persistence, and engine convergence. REQ-001 wording is not changed by this revision."
    reason: "The record cannot leave needs-design while its four open decisions are unowned. Every ruling below is anchored in code on main or in the in-flight PR #108, so implementation truth is decidable without chat."
    impact: "Implementation is unblocked for design work; #110's first code PR consumes #111 rather than writing a second cursor/report layer. No acceptance criterion is weakened here."
    approver: "@PeterGuy326"
    decidedAt: "2026-08-30T06:17:00Z"

    Owner

    • technicalOwner: "@waterbro-8" — owns design and implementation truth for this record, and is the implementationOwner of the bounded PR.
    • Independent approval cannot come from the technical owner: AGENTS.md step 6 requires an approval from someone other than the author, and .github/CODEOWNERS names @PeterGuy326 and @Bindy-lbb as the only review authorities. Named reviewer for this issue's PRs: either code owner, requested after required CI is green and the head is stable.
    • productOwner stays @PeterGuy326; the R3 narrowing of REQ-001 needs product concurrence (see that comment).

    D-1 Daemon lifecycle — foreground-only, no supervision in this tier

    mem put <path> --watch runs in the calling terminal, blocks, and exits when signalled.

    • No daemonize, no pidfile, no --daemon, no shipped unit/service file. A detached writer into a vault with no owner is an unowned process; restart policy, log routing, upgrade behaviour and single-instance arbitration are exactly the "完整本地同步盘" semantics GOAL.md:95/123 defers to Phase 2. Users who need supervision wrap the foreground mode themselves (systemd Type=exec, launchd, or a compose command:), which costs mem zero code.
    • Signals: server/cmd/mem/main.go:25 already installs signal.NotifyContext(..., os.Interrupt) at the CLI root, so Ctrl-C is free; SIGTERM is handled only in cmd/memd/main.go:78 and must be added to the watch loop's own context so docker stop/systemd-stop terminates between cycles rather than mid-upload. Clean signal shutdown exits 0 — for a watcher, being stopped is the expected terminal state, and any non-zero code makes every wrapper think it failed.
    • Pacing: --interval <duration>, default 30s, poll loop. No fsnotify in this tier: it is absent from server/go.mod, and event APIs bring per-platform edge semantics (inotify watch limits, FSEvents coalescing, Windows) that the current CI has no coverage for. A poll loop is deterministic and satisfies AC-001; an event-driven follow-up needs its own per-platform acceptance criteria.
    • Single instance: advisory lock file per watched root under the CLI state root; a second watcher on the same root fails fast (exit 1 + hint) so operators never get two competing reports for one directory.
    • Restart: resumes from persisted cursors; safe because the server dedupes by (user_id, sha256) (server/internal/file/file.go:196-201), so a replayed cycle is a 秒传 no-op, not a duplicate object.
    • Give-up bound: after 10 consecutive cycles in which every attempt failed with a non-retryable auth / plan_quota / provider_timeout class, exit with the SPEC.md:582 mapped code (3 / 4 / 5) instead of spinning silently. A supervisor then restarts a process that reported a real error.

    D-2 Report shape and persistence — per-cycle report, local log, no server ledger

    • One structured object per cycle to stdout; --format json (SPEC.md:579 already defines this convention) makes it machine-readable, text mode keeps today's human summary. Vocabulary is closed: scanned, ingested, deduped, unchanged, local_gone, failed, plus items[] carrying locators (local absolute path, returned file id, returned virtual path, sha256 prefix, failure code).
    • Locator honesty (code-grounded): on deduped the report MUST carry the path the server returned, not the requested --to folder. Service.Put answers identical content with the pre-existing row (server/internal/file/file.go:196-201), whose virtual path can be anywhere in the workspace; uploadDir today prints put <local> -> <folder> before the call and so already implies a placement the server never confirmed (server/cmd/mem/cmds_file.go:161). A watch report that copies that habit would claim a location it did not cause.
    • Failure codes (closed set, asserted by AC-002): auth, plan_quota, provider_timeout, network, read_denied, upload_rejected, root_missing, state_corrupt. Per-item failure never aborts the cycle — uploadDir already degrades per file and continues (server/cmd/mem/cmds_file.go:162-164), and PR feat(ingest): add mem ingest qoder for AI-agent conversation transcripts (#103) #108 established the same discipline for 409s at server/cmd/mem/cmds_ingest.go:220-232.
    • Persistence: append-only JSONL at <state root>/watch/reports/<sha1(abs root)>.jsonl, reusing the cliStateRoot() convention (server/cmd/mem/cmds_ingest.go:104-112, the same $HOME/.mem the qoder connector uses), capped at the last 200 cycles with rotation. A long-running watcher must not be able to fill the user's disk.
    • Explicitly not in this record: a server-side ledger. The only server ledger is workspace_imports for bundle imports (docs/maintainers precedent, PR feat(transfer): add import history ledger endpoint and TransferPage view #102); routing watch cycles into it needs a migration, an API surface and a UI view — that is Phase 2 scope and would make a P2 minimal tier unreviewable as one PR.

    D-4 Ingestion path — one shared core, two sinks, extracted before it is used

    Answer to "does #110 share an ingestion engine with #103": yes for the engine, no for the sink, and neither is done inside PR #108.

    ACs added by this revision

    需要 maintainer 执行(本账号无写权限)

    waterbro-8 对 upstream issue 无 body/label 写权限(PATCH/labels 均 403,需 admin)。请 @PeterGuy326 按本条 + R3 条依次落 R2/R3 body,并补 triage 要求的标签:area:cli(现缺 area)、evidence:e2-source(本条已给源码级取证)、status 按 R3 结论决定(R2 本身不解锁 status:ready,REQ-001 口径仍待产品确认)。本条不改动任何旧 decision comment。

    —— 技术 P9

  3. waterbro-8 commented on Aug 30, 2026

    @waterbro-8
    Collaborator

    裁决摘要(技术 P9)

    D-3(变更检测语义)出解,并据此把 REQ-001 的 "new/changed" 口径收窄为"新文件 + 变更只上报不重摄"。收窄依据是 SPEC 锚点本身只承诺新文件,以及文件面没有版本/替换模型这两条硬事实。此条需要产品确认。

    schemaVersion: requirement-decision.v1
    canonicalIssue: "https://github.com/fullstack-ai-infra/mem/issues/110"
    previousRevision: R2
    resultingRevision: R3
    decisionType: narrowing
    oldDecision: "REQ-001: watches the designated directory and ingests new/changed files into mem; AC-001: new/changed files under a watched directory are ingested."
    newDecision: "REQ-001: watches the designated directory and ingests new files into mem; a content change to an already-ingested file is detected, counted as changed, and listed with locators, but is NOT re-ingested in this tier. AC-001 covers new-file ingestion; AC-009 requires a changed file to be reported and to leave the vault unchanged across two consecutive cycles."
    reason: "The file plane cannot represent a revision today. files has UNIQUE (user_id, sha256) only (server/internal/db/migrations/0001_init.sql:61) and no path uniqueness, so re-putting changed content inserts a second row at the same virtual path; supersedes/corrects/occurrence_of exist only on memory_relations (server/internal/memory/relation.go:19-25), so the two rows cannot be linked. A watcher over an actively edited directory would therefore add a Drive entry per save with no way to tell them apart. SPEC.md:604 promises 守护,新文件自动入 — new files — so R1's changed wording exceeded the canon anchor rather than the canon being reduced."
    impact: "Weakens no SPEC promise and preserves the founder outcome (point at a folder, content flows in). Re-ingest of changed content becomes a named limit gated on a file-revision design that belongs to the Phase 2 sync-drive cluster (SPEC.md:1140, GOAL.md:95/123); if product declines the narrowing, that design is a new issue, not a flag here."
    approver: "@PeterGuy326"
    decidedAt: "2026-08-30T06:17:00Z"

    D-3 Change detection — stat gate, hash authority, one quiet interval, append-only

    Cursor row per absolute path, under the shared state root: {abs, size, mtime, sha256, fileId, ingestedAt}.

    Local observation Action Count
    Path not in cursor, and (size, mtime) identical across two consecutive scans upload ingested
    Path not in cursor, first time seen (still growing) defer to next scan —
    Path in cursor, (size, mtime) unchanged no read at all unchanged
    Path in cursor, stat changed, recomputed sha256 equal to cursor sha no upload unchanged
    Path in cursor, recomputed sha256 differs report, do not re-ingest changed
    Path in cursor, file no longer on disk report, issue no delete call local_gone

    Three rulings inside that table matter:

    1. (size, mtime) is a cost gate, sha256 is the only authority. mtime alone must never trigger an upload — rsync -t, touch, backup restores and cloud-sync placeholders all move mtime without moving bytes. The stat gate exists so a 10k-file directory does not get 10k hashes per cycle.
    2. The stability gate (one quiet interval) is what makes append-only safe, and it is not optional. Uploading a half-written file is strictly worse than uploading it one cycle late: the truncated bytes get a permanent content-addressed row, and no later cycle can repair it, because the completed file is a change — which this tier deliberately does not re-ingest. So a file is ingested only when (size, mtime) is unchanged between two consecutive scans.
    3. A brand-new local file whose content already exists in the workspace is answered with the existing row (秒传, server/internal/file/file.go:196-201). That is a success, and it must be counted as deduped, never as ingested, with the returned id/path as the locator (AC-006). "Your content is in the vault" is true there; "it landed in the folder you passed to --to" is not necessarily.

    Why changed is reported rather than re-ingested. Service.Put always inserts a fresh row unless the content hash already exists, and nothing in files makes one path versioned — so a re-put of /Docs/report.pdf produces two rows at the same path, both visible in Drive, both searchable, indistinguishable except by id. With a watcher on an edited directory that is a new entry per save. Linking them is impossible on the file plane (memory_relations is memory-scoped), and choosing which one mem get/mem cat should return is a product decision, not an implementation detail. That design is the Phase 2 sync-drive work GOAL.md:95/123 and SPEC.md:1140 already defer; carrying changed as a visible, counted state is the honest interim, and it keeps this issue one reviewable PR.

    AC changes by this revision

    • AC-001 (rewritten): new files appearing under a watched directory are ingested after one quiet interval, and the cycle report counts are correct for scanned / ingested / deduped / unchanged.
    • AC-003 (strengthened): deleting a local file yields local_gone in the next cycle and issues zero delete or write-back requests — asserted at the HTTP-contract level with a stub transport that fails the test on any DELETE or non-upload call, following the cmds_ingest_test.go precedent.
    • AC-009 (new): modifying an already-ingested file in place reports changed with the local path and the previously ingested file id, and leaves the vault contents unchanged across two consecutive cycles.

    Non-goals added

    No file-revision or replacement model, no in-place update, no overwrite-by-path, no delete propagation, no ignore/exclude rule language (--exclude is Phase 2 with the sync drive — the directory the user picks is the boundary in this tier), and no event-API (fsnotify) dependency.

    给产品的确认点

    收窄需要 @PeterGuy326 一句书面确认(R3 已按 requirement-decision.v1 记录)。若产品选择"变更也要入",正确动作是先立"文件版本/替换语义"设计项(哪个 id 是现值、路径冲突怎么展示、mem get 返回谁),而不是在 #110 里放开重摄开关。

    —— 技术 P9

  4. added a commit that references this issue on Aug 31, 2026
  5. waterbro-8 commented on Sep 3, 2026

    @waterbro-8
    Collaborator

    Coverage read-back after today's fork closes: nothing is in flight for this issue

    Both of this issue's implementation candidates were fork pull requests, and both were closed on 2026-09-03 under the decision that fork workflow permissions stay off and the work is redone in-repo:

    Why nothing was transplanted rather than cherry-picked: cherry-pick preserves the author, and these heads are authored by liyuanyang <liyuanyang@users.noreply.github.com> (and in #147 also by a personal @qq.com address). Importing them would put exactly the attribution shape today's decision exists to prevent onto main. Preserving the original署名 and satisfying that rule are mutually exclusive for these heads, which is why the rework has to be written in-repo. Nothing was deleted -- the fork branches are untouched, so the closed heads stay inspectable.

    The gap is real, not a paperwork artifact: on main@1332bf46 there is no watch path at all -- a grep for watch across server/cmd/mem/*.go returns zero matches.

    So that nobody reads the history the wrong way: #110 is now both unassigned and uncovered. Anyone who saw #150 open earlier should not assume the minimal watch tier is in progress.

    Two conditions worth knowing before someone starts the in-repo version: Web → Audit dependencies is red on main itself today (browserslist <=4.28.6, read-back on #138), so a new PR inherits one failing required check until #159 lands; and every PR still needs @PeterGuy326 or @Bindy-lbb, since the base CODEOWNERS names only those two.

    No label, status, assignment, or body change made here by me.

  6. sun-970 commented on Sep 8, 2026

    @sun-970
    Contributor

    Draft Implementation Proposal

    Based on the existing decisions (R2/R3) from the technical P9, here is a concrete implementation sketch for team discussion. This is a draft — flag anything that looks off before any code is written.

    Sequencing

    Two phases, matching the dependency chain:

    Phase A — #111 first: Extract shared ingestion core
    Before writing watch code, extract the shared local-walk + cursor + change-detection logic into a reusable package. Two call sites: existing ingest qoder and the new put --watch.

    Phase B — #110: Implement put --watch on top of the core

    Phase A: Shared core extraction (#111)

    Proposed package layout:

    server/internal/localscan/
    ├── scanner.go      // recursive walk, stat gate, hash authority
    ├── cursor.go       // per-path cursor store (atomic rename write)
    ├── report.go       // cycle aggregation, closed vocabulary, JSON/text formatting
    └── classify.go     // failure code classification (closed set)
    

    Key interfaces:

    // Scanner walks a root, applies stat-then-hash gate, yields per-file decisions.
    type Scanner struct { ... }
    
    type FileDecision struct {
        AbsPath  string
        Action   DecisionAction // ingest, unchanged, changed, local_gone, defer
        SHA256   string
        Cursor   *CursorRow     // nil for new files
    }
    
    // Sink is what each caller implements — file plane upload or memory plane POST.
    type Sink interface {
        Ingest(ctx context.Context, path string, content io.Reader, sha256 string) (*IngestResult, error)
    }

    The ingest qoder path and put --watch path each provide their own Sink implementation. The scanner is shared; the write surface is not.

    Cursor storage reuses the existing atomic-rename pattern from qoder_checkpoint.go:37-59.

    Phase B: put --watch implementation

    New file: server/cmd/mem/cmds_watch.go

    Flag surface (aligned with SPEC.md:604):

    mem put <path> --watch [--interval 30s] [--to <vault-path>] [--format json|text] [--dry-run] [--limit N]
    

    Runtime loop:

    1. Acquire advisory lock (<state root>/watch/locks/<sha1(abs root)>)
       -> fail fast exit 1 if held
    2. Enter poll loop:
       a. Scan root -> []FileDecision
       b. For each "ingest" decision: call Sink.Ingest (file plane multipart upload)
       c. Aggregate cycle report
       d. Emit report to stdout (--format json or text)
       e. Append report JSONL to <state root>/watch/reports/<sha1(abs root)>.jsonl
       f. Rotate JSONL if > 200 entries
       g. Sleep --interval
    3. On SIGINT/SIGTERM: finish current cycle, exit 0
    4. After 10 consecutive all-fail cycles: exit with SPEC mapped code
    

    Signal handling: hook into the existing signal.NotifyContext at main.go:25, add SIGTERM alongside SIGINT for the watch subcommand.

    Advisory lock: use syscall.Flock (Linux/macOS) — no new dependency.

    Open questions for discussion

    1. Package name localscan — or would ingestcore / dirwalk be clearer? The name needs to signal "shared between watch and qoder-ingest."

    2. Lock file location — <state root>/watch/locks/ is proposed for consistency with the report log location. Alternative: lock file next to the watched root (more discoverable but pollutes user directories).

    3. Give-up bound — 10 consecutive all-fail cycles is from D-1. Should this be configurable via flag, or is a hardcoded constant sufficient for P2 tier?

    4. JSONL rotation — 200-cycle cap from D-2. Should we also add a max-file-size cap (e.g., 1MB) as a safety net?

    5. Test strategy — following the fixtures-first approach from the issue body:

      • watch_test.go: new/changed files ingested, report counts correct (AC-001)
      • watch_failure_test.go: failing file yields stable code, daemon continues (AC-002)
      • watch_boundary_test.go: local deletion -> local_gone, zero delete/write-back calls (AC-003)
      • watch_lifecycle_test.go: signal handling, lock contention, missing root (AC-005)
    6. CI gate — the latest comment notes Web -> Audit dependencies is red on main (browserslist). Should this PR wait for build(deps-dev): bump browserslist from 4.28.2 to 4.28.8 in /web #159 to land first, or proceed in parallel?

    What this proposal deliberately does NOT include

    • No fsnotify / event-driven scanning (Phase 2 per D-1)
    • No file revision / re-ingest of changed content (Phase 2 per R3)
    • No daemonize / pidfile / service file (Phase 2 per D-1)
    • No server-side ledger for watch cycles (Phase 2 per D-2)
    • No --exclude / ignore rules (Phase 2 per R3 non-goals)
    • No bidirectional sync, delete propagation, or conflict resolution
  7. wadrzl commented on Sep 8, 2026

    @wadrzl

    Issue #110 Verification (2026-09-08)

    Baseline: origin/main read today. R3 scope (9 ACs). No open PR targets this issue or its dependency #111. Fork PRs #150 (watch) and #147 (shared core) closed without merge 2026-09-03; nothing imported.

    Requirements (R3 scope)

    Req Status Evidence
    REQ-001 (one-way watch daemon: mem put --watch) ❌ NOT DONE No cmds_watch.go in server/cmd/mem/. No --watch flag in cmds_file.go. grep for "watch" across server/cmd/mem/*.go returns zero matches.
    REQ-002 (per-cycle import report) ❌ NOT DONE No report code, no JSONL persistence, no closed vocabulary implementation.
    REQ-003 (boundary: no write-back, no delete propagation) ❌ NOT DONE No watch code exists to enforce boundaries.
    REQ-004 (canon consolidation: SPEC/GOAL/README cross-ref this record) ❌ NOT DONE No cross-references to #110 found in SPEC.md, GOAL.md or README.

    Acceptance criteria (R3 scope)

    AC Status
    AC-001 (new files ingested, report counts correct) ❌ no watch path exists
    AC-002 (failing file yields stable code, daemon continues) ❌
    AC-003 (local deletion -> local_gone, zero delete calls) ❌
    AC-004 (flag semantics match SPEC.md:604) ❌ no flags exist
    AC-005 (lifecycle: signals, lock, missing root) ❌
    AC-006 (report vocabulary: closed count + failure-code set) ❌
    AC-007 (JSONL persistence, capped rotation) ❌
    AC-008 (consumes #111 shared core) ❌ #111 not done
    AC-009 (changed file reported, vault unchanged) ❌

    Dependency

    Closed PRs

    Summary

    0/4 requirements met. 0/9 ACs met. Dependency #111 open with no PR. Zero implementation candidates in-repo. The design record (R1/R2/R3) is complete and detailed; implementation has not started.

  8. waterbro-8 commented on Sep 17, 2026

    @waterbro-8
    Collaborator

    Still blocked on #111. mem put --watch is absent on main. Fork candidate #150 was closed because a long-lived watcher writing through the ingest path is unsafe until #139/#111 land.

    I am not implementing --watch on top of the unextracted qoder connector. After #191 and a re-landed #111, this can be an organization-branch PR.

  9. waterbro-8 commented on Sep 17, 2026

    @waterbro-8
    Collaborator

    Predecessor extraction is now on an organization branch: #217 (issue #111). put --watch should wait for that PR to land so it can consume server/internal/ingest instead of copying cursor/report code.

    I am not opening a --watch PR until #217 is reviewed.

  10. wadrzl commented on Sep 18, 2026

    @wadrzl

    #110 依赖拆解与实现计划

    前置 #111 已于 2026-09-18 合入 main(PR #217,commit f72e294)。server/internal/ingest 包已可用,提供:

    • 确定性递归 walk(Walk)
    • 每文件 cursor(atomic rename 写入,shrink-reset)
    • 变更判定(size-based;same-size edit 不检测,符合 R3 口径)
    • 闭集 failure code(CodeAuth/CodePlanQuota/CodeProviderTimeout/CodeNetwork/CodeReadDenied/CodeUploadRejected/CodeRootMissing/CodeStateCorrupt)
    • cycle report 聚合(Report/Run)
    • canonical path identity(CanonicalRoot/CursorPath)

    依赖关系

    依赖 状态 说明
    #111 共享 ingest 核心 ✅ 已合并 server/internal/ingest 直接在 main 上
    #139 checkpoint 并发安全 ✅ 已合并 cursor lock 已在 #111 中复用
    #191 qoder checkpoint 回退 ✅ 已合并 前置条件已清

    无剩余阻塞依赖。

    实现拆解

    一个 PR,约 5 个新文件 + 2 个改动文件:

    1. File-plane sink(新文件:server/internal/ingest/file_sink.go)

    put --watch 和 ingest qoder 的区别在 sink 层:

    • qoder:ParseFunc 按行拆 unit,UploadFunc POST /v1/memories
    • watch:ParseFunc 整文件为一个 unit(content = 文件内容),UploadFunc 走 multipart upload 到文件面

    watch 的 ParseFunc 需要返回一个 Unit,其 data 是文件内容。UploadFunc 调用现有的 Service.Put 路径(multipart upload + content-addressed dedup)。deduped 判定:server 返回已有行的 id/path(server/internal/file/file.go:196-201),report 中 item 携带 server 返回的 path,不是请求的 --to 路径。

    2. Watch 命令(新文件:server/cmd/mem/cmds_watch.go)

    mem put <path> --watch [--interval 30s] [--to <vault-path>] [--format json|text] [--dry-run] [--limit N]
    

    运行循环:

    1. CanonicalRoot 解析 watched path
    2. 获取 advisory lock(<state root>/watch/locks/<sha1(abs root)>),已持有则 exit 1
    3. 进入 poll loop:
      a. 调用 ingest.Run 执行一次 scan + ingest
      b. 聚合 cycle report
      c. 输出 report 到 stdout
      d. 追加 JSONL 到 <state root>/watch/reports/<sha1(abs root)>.jsonl
      e. 超 200 条则 rotate
      f. sleep --interval
    4. SIGINT/SIGTERM:完成当前 cycle 后 exit 0
    5. 连续 10 个全失败 cycle:exit SPEC 映射码

    3. Advisory lock(新文件:server/cmd/mem/cmds_watch_lock.go)

    复用 server/internal/ingest/cursor_lock_unix.go 的 flock 模式。每个 watched root 一个 lock 文件,syscall.Flock(LOCK_EX | LOCK_NB) 实现 fail-fast。

    4. JSONL report 持久化(新文件:server/cmd/mem/cmds_watch_report.go)

    • append-only JSONL,每 cycle 一行
    • 200 条 cap,rotate 时保留最新 200 条
    • 文件路径:<cliStateRoot>/watch/reports/<sha1(abs root)>.jsonl

    5. Signal handling

    复用 server/cmd/mem/main.go:25 的 signal.NotifyContext,watch 子命令额外注册 SIGTERM。Signal 到达时完成当前 cycle 后 exit 0。

    6. 测试

    测试文件 覆盖 AC
    cmds_watch_test.go AC-001(新文件 ingest + report 计数)
    cmds_watch_failure_test.go AC-002(失败文件 stable code + daemon 继续)
    cmds_watch_boundary_test.go AC-003(本地删除 → local_gone,零 DELETE 请求)+ AC-009(changed 只报告不重摄)
    cmds_watch_lifecycle_test.go AC-005(signal、lock 竞争、missing root)
    cmds_watch_report_test.go AC-006(闭集 vocabulary)+ AC-007(JSONL 持久化 + cap)

    不做的事

    • 无 fsnotify / event-driven(Phase 2)
    • 无文件版本 / changed 重摄(Phase 2,R3 口径)
    • 无 daemonize / pidfile / service file(Phase 2)
    • 无 server-side ledger(Phase 2)
    • 无 --exclude / ignore rules(Phase 2)
    • 无双向同步 / delete propagation / conflict resolution

    预计时间

    • 依赖已清,可以开始实现
    • 预计 1-2 天出实现 PR

    @waterbro-8 这是你作为 technicalOwner 的实现。如需我协助 review 或其他前置操作,请在群内说明。

  11. waterbro-8 commented on Sep 20, 2026

    @waterbro-8
    Collaborator

    平台组分工补派(冯浩然 / waterbro-8)

  12. waterbro-8 commented on Sep 20, 2026

    @waterbro-8
    Collaborator

    平台组群成员派活(冯浩然 / waterbro-8)

  13. sun-970 commented on Sep 20, 2026

    @sun-970
    Contributor

    Implemented in-repo mem put <path> --watch per R2/R3. Consumes server/internal/ingest (#111 / PR #217). No fsnotify, no bidirectional sync, no deletion propagation.

    PR: #225 (sun-970:feat/110-put-watch-20260920)

    @waterbro-8 please review.

  14. waterbro-8 commented on Sep 23, 2026

    @waterbro-8
    Collaborator

    关闭验收:PR #225 已合入 main(merge ca6ed33;final head 3bc3d58),实现 R2/R3 最小 one-way mem put --watch,并复用 #111 的 shared ingest core;不包含 fsnotify、双向同步或删除传播,符合非目标。final head 已正式 APPROVED,全部 hosted checks 通过;合入后的 main 四套 workflow 全绿。

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:p2Useful backlog work outside the immediate critical pathtype:featureA new user-facing capability

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions