@furongjun1999/dsh-memory 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +631 -629
- package/codebuddy/CODEBUDDY.md +5 -5
- package/docs/discipline/templates/zcode-user.md.tmpl +4 -1
- package/docs/eval//345/217/221/345/270/20312_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_v1.0.md +108 -0
- package/docs/eval//345/217/221/345/270/20313_/344/270/212/344/270/213/346/226/207/350/207/252/347/256/241/347/220/206/346/234/272/345/210/266_v1.0.md +114 -0
- package/docs/eval//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_P1_/346/212/275/345/217/226/345/231/250v4_/344/270/211/347/216/207/350/257/273/346/225/260_v1.0.md +231 -0
- package/docs/mdcg//344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/344/275/277/347/224/250/344/270/216/350/277/220/347/273/264_v1.0.md +305 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +45 -43
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +80 -1
- package/docs/plans//347/201/265/346/236/242/350/272/253/344/275/223/303/227/350/204/221_/344/270/226/347/225/214/346/250/241/345/236/213/345/257/271/346/216/245/350/256/276/350/256/241_v0.1.md +76 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +461 -461
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.2.md +121 -0
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.3.md +117 -0
- package/lib/hooks.d.ts +34 -0
- package/lib/hooks.js +205 -18
- package/lib/index.js +22 -1
- package/lib/lib/mdcg_client.d.ts +37 -0
- package/lib/lib/mdcg_client.js +44 -0
- package/lib/lib/session_state.d.ts +56 -0
- package/lib/lib/session_state.js +124 -0
- package/lib/tools.js +12 -1
- package/md_cg/audit.py +7 -1
- package/md_cg/bench_governance.py +4 -1
- package/md_cg/bench_locomo_zh.py +12 -2
- package/md_cg/bench_zh_mad.py +12 -2
- package/md_cg/interop.py +25 -9
- package/md_cg/lifecycle.py +38 -0
- package/md_cg/mcp_server.py +64 -11
- package/md_cg/mdcos.py +78 -9
- package/md_cg/sleep.py +223 -103
- package/md_cg/state_slots.py +221 -0
- package/md_cg/stg.py +68 -3
- package/md_cg/sustain.py +150 -4
- package/md_cg/test_lifecycle_retire_leak.py +408 -0
- package/md_cg/test_read_face_semantics.py +6 -3
- package/md_cg/test_state_event_op.py +514 -0
- package/md_cg/test_state_slots.py +691 -0
- package/md_cg/test_sustain_bounded.py +624 -0
- package/md_cg/tokens.py +8 -1
- package/md_cg/units.py +4 -1
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/src/hooks.ts +217 -17
- package/src/index.ts +22 -1
- package/src/lib/mdcg_client.ts +48 -0
- package/src/lib/session_state.ts +127 -0
- package/src/tools.ts +12 -1
- package/zcode/AGENTS.md +5 -5
package/md_cg/sleep.py
CHANGED
|
@@ -75,6 +75,14 @@ GIT_USER_EMAIL = "sleep@lingshu.local"
|
|
|
75
75
|
FORBIDDEN_BASENAMES = ("_keys.json", "_access.log", "_index.json")
|
|
76
76
|
FORBIDDEN_SUFFIXES = (".tmp", ".lock")
|
|
77
77
|
DEFAULT_LOCK_TIMEOUT = 30.0
|
|
78
|
+
#: 单条 git 子进程的**硬超时**(秒)——issue #63 主修(单点可调)。
|
|
79
|
+
#: 取值依据:本机 git 操作为秒级(init / add / commit / rev-parse / worktree /
|
|
80
|
+
#: merge / revert 全是单库本地动作),120s 为安全上界;超时即 kill
|
|
81
|
+
#: (`subprocess.run(timeout=)` 语义),异常消息里带「若是残留锁则人工核删」的
|
|
82
|
+
#: 指引(本模块**不自动删** index.lock——可能是他进程的锁,误删更糟)。
|
|
83
|
+
_GIT_TIMEOUT_S = 120
|
|
84
|
+
#: git 子进程超时后可能的残留锁名(仅用于异常消息里的核删指引,不自动删)。
|
|
85
|
+
_GIT_LOCK_NAME = "index.lock"
|
|
78
86
|
#: 睡眠轮次的**台账**(append-only JSONL)——落 `sleep_root()` 侧(状态面,
|
|
79
87
|
#: **不是数据根**):台账是运行态,不该成为真源面的一部分、也不该进版本库。
|
|
80
88
|
SLEEP_LEDGER = "_sleep.jsonl"
|
|
@@ -116,6 +124,16 @@ class SleepError(Exception):
|
|
|
116
124
|
"""git 操作失败(带 stderr 摘要)。"""
|
|
117
125
|
|
|
118
126
|
|
|
127
|
+
class SleepGitTimeout(SleepError):
|
|
128
|
+
"""git 子进程**超时已被 kill**(issue #63)——与「git 返回非零」区分。
|
|
129
|
+
|
|
130
|
+
为什么单列一支:超时与普通失败的处理面不同——普通失败(rc≠0)是 git
|
|
131
|
+
对本次操作的裁决(冲突 / 非法状态),超时是**本次操作没有完成**(可能
|
|
132
|
+
留下半成品与残留锁),台账文案必须能分开(物化超时 vs 物化失败)。
|
|
133
|
+
`SleepError` 的子类 ⇒ 所有既有 `except SleepError` 面照旧收得住。
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
|
|
119
137
|
# 生效条件:name 为 SLEEP_ENV_DEFAULTS 的键时,按 SLEEP_ENV_KEYS[name] 从 environ(缺省 os.environ)取名取值,缺键(None)时回落真源缺省;返回**原始字符串**(不做 bool/float 解释——解释归各具名包装)。name 非表内键时抛 KeyError(不做静默回落:拼错键名即编程错误)。
|
|
120
138
|
def sleep_env(name: str, environ=None) -> str:
|
|
121
139
|
"""§4.7 env 表的唯一读取出口(原始字面量)。"""
|
|
@@ -265,23 +283,47 @@ def face_delta(before: dict, after: dict) -> dict:
|
|
|
265
283
|
if before[k] != after[k])}
|
|
266
284
|
|
|
267
285
|
|
|
268
|
-
# 生效条件:git_dir_path 与 work_tree 给定时以 ["git", "--git-dir=<git_dir_path>", "--work-tree=<work_tree>", *args] 调 subprocess.run(capture_output + text + 显式 utf-8/errors=replace + env 带 PYTHONUTF8=1 + shell=False
|
|
286
|
+
# 生效条件:git_dir_path 与 work_tree 给定时以 ["git", "--git-dir=<git_dir_path>", "--work-tree=<work_tree>", *args] 调 subprocess.run(capture_output + text + 显式 utf-8/errors=replace + env 带 PYTHONUTF8=1 + shell=False + **timeout=_GIT_TIMEOUT_S** + **stdin=DEVNULL**)并返回 CompletedProcess;returncode 非零不抛(由调用方判);TimeoutExpired **不吞**——单点转 SleepGitTimeout(消息含「超时(_GIT_TIMEOUT_S)已被 kill」+ 命令名 + 残留锁人工核删指引)。
|
|
269
287
|
def _git(git_dir_path: str, work_tree: str, *args: str) -> subprocess.CompletedProcess:
|
|
270
288
|
"""git 调用的**唯一出口**:`--git-dir` 与 `--work-tree` 一律显式给。
|
|
271
289
|
|
|
272
290
|
`--work-tree` 不是装饰:物化与合并传数据根,对账+提交传影子——同一个版本库
|
|
273
291
|
两个工作树,靠这两个显式开关切换,**不依赖进程 cwd**(`hive/wm.py` 用 `-C`
|
|
274
292
|
是因为它的工作树就是版本库本身;这里两者分居,故用分裂形态)。
|
|
293
|
+
|
|
294
|
+
**有界化(issue #63)**——三件套缺一不可:
|
|
295
|
+
|
|
296
|
+
① `timeout=_GIT_TIMEOUT_S`:子进程**无界等待**是常驻循环停摆的直接成因
|
|
297
|
+
(实测停 9.5 小时:`SustainLoop` 单线程串行六档,任一档挂住即全停)。
|
|
298
|
+
② `stdin=subprocess.DEVNULL`:不指定 stdin 时子进程**继承父进程 stdin**
|
|
299
|
+
——DSH 常驻宿主的 stdin 是 **JSON-RPC 活管道**(不写入也不关闭),
|
|
300
|
+
任何读 stdin 的子进程会永久悬挂(实测:活管道下 6s 仍 poll=None;
|
|
301
|
+
DEVNULL 下 0.02s 立即 EOF)。先例形态:`md_cg/run_tests.py:105`。
|
|
302
|
+
③ `TimeoutExpired` **不吞**:转 `SleepGitTimeout`(`SleepError` 子类,
|
|
303
|
+
既有 `except SleepError` 面照旧收得住),消息带命令名与残留锁核删
|
|
304
|
+
指引——`subprocess.run(timeout=)` kill 子进程后 git 可能留下
|
|
305
|
+
`index.lock`;**本模块不自动删**(可能是他进程的锁,误删更糟),
|
|
306
|
+
是否自动清理列入待裁(见 issue #63 台账)。
|
|
275
307
|
"""
|
|
276
308
|
argv = ["git", "--git-dir=" + git_dir_path, "--work-tree=" + work_tree]
|
|
277
309
|
argv.extend(args)
|
|
278
310
|
env = dict(os.environ, PYTHONUTF8="1")
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
311
|
+
try:
|
|
312
|
+
return subprocess.run(argv, capture_output=True, text=True,
|
|
313
|
+
encoding="utf-8", errors="replace",
|
|
314
|
+
env=env, shell=False,
|
|
315
|
+
timeout=_GIT_TIMEOUT_S,
|
|
316
|
+
stdin=subprocess.DEVNULL)
|
|
317
|
+
except subprocess.TimeoutExpired as exc:
|
|
318
|
+
raise SleepGitTimeout(
|
|
319
|
+
"git %s 超时(%ss)已被 kill(命令:git %s)——"
|
|
320
|
+
"本次 git 操作未完成;若后续 git 操作报 lock,请人工核删 "
|
|
321
|
+
"%s(可能是被 kill 的进程留下的残留锁,本模块不自动删)"
|
|
322
|
+
% (" ".join(args[:2]), _GIT_TIMEOUT_S, " ".join(args[:6]),
|
|
323
|
+
os.path.join(git_dir_path, _GIT_LOCK_NAME))) from exc
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# 生效条件:同 _git 的调用形态;returncode != 0 时抛 SleepError(消息取 stderr 为真值、为空则取 stdout,strip 后截 300 字符),为 0 时返回 stdout;_git 侧超时以 SleepGitTimeout(SleepError 子类,消息含「超时…已被 kill」)原样冒出——两条失败面**可区分**(rc≠0 = git 的裁决;超时 = 操作没完成)。
|
|
285
327
|
def _git_ok(git_dir_path: str, work_tree: str, *args: str) -> str:
|
|
286
328
|
r = _git(git_dir_path, work_tree, *args)
|
|
287
329
|
if r.returncode != 0:
|
|
@@ -380,26 +422,36 @@ def materialize(root: str = None, *, git_dir_path: str = None,
|
|
|
380
422
|
with FileLock(lock_path(), timeout=timeout, strict=False) as lk:
|
|
381
423
|
if not lk.acquired:
|
|
382
424
|
return _busy("materialize", timeout)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
425
|
+
try:
|
|
426
|
+
rep = ensure_repo(root, G)
|
|
427
|
+
init = _stage_and_commit(G, root, "sleep: 基线(真源面入册)") \
|
|
428
|
+
if not _has_head(G, root) else {"committed": False,
|
|
429
|
+
"reason": "已有基线提交"}
|
|
430
|
+
if os.path.isdir(S):
|
|
431
|
+
r = _git(G, root, "worktree", "remove", "--force", S)
|
|
432
|
+
if r.returncode != 0:
|
|
433
|
+
# 非注册工作树(上一次运行被杀、目录残留)→ 直接摘目录
|
|
434
|
+
shutil.rmtree(S, ignore_errors=True)
|
|
435
|
+
r = _git(G, root, "worktree", "add", "-b", branch, S, BASE_BRANCH)
|
|
389
436
|
if r.returncode != 0:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
437
|
+
r2 = _git(G, root, "worktree", "add", S, branch)
|
|
438
|
+
if r2.returncode != 0:
|
|
439
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
440
|
+
"phase": "materialize", "lock": lock_path(),
|
|
441
|
+
"error": (r2.stderr or r2.stdout).strip()[:300],
|
|
442
|
+
"hint": "影子分支/目录未能挂上"}
|
|
443
|
+
reused = True
|
|
444
|
+
else:
|
|
445
|
+
reused = False
|
|
446
|
+
except SleepGitTimeout as e:
|
|
447
|
+
# 超时与「物化失败」分开记账(issue #63):超时 = git 操作**没完成**
|
|
448
|
+
# (已被 kill),与 rc≠0 的「git 的裁决失败」不是一回事——
|
|
449
|
+
# run_cycle 据此写「物化超时:…」(见 mat_fail 分流)。
|
|
450
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
451
|
+
"phase": "materialize", "lock": lock_path(),
|
|
452
|
+
"timeout": True, "error": str(e),
|
|
453
|
+
"hint": ("git 子进程超时已被 kill:本轮物化放弃;"
|
|
454
|
+
"残留锁核删指引见 error")}
|
|
403
455
|
after = source_face_hashes(root)
|
|
404
456
|
delta = face_delta(before, after)
|
|
405
457
|
# ok = **物化动作成功**(影子已挂上);真源面漂移与否是另一个读数
|
|
@@ -446,9 +498,17 @@ def reconcile_and_commit(root: str = None, *, git_dir_path: str = None,
|
|
|
446
498
|
"error": "影子工作树不存在:先 materialize", "shadow": S}
|
|
447
499
|
# 链式工作树用自己的 gitdir(否则 HEAD/index 取主那一份,提交会落到 main)
|
|
448
500
|
GS = worktree_git_dir(S) or G
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
501
|
+
try:
|
|
502
|
+
br = branch or _git_ok(GS, S, "rev-parse", "--abbrev-ref",
|
|
503
|
+
"HEAD").strip()
|
|
504
|
+
msg = "sleep: %s 轮对账提交" % (batch or _stamp())
|
|
505
|
+
res = _stage_and_commit(GS, S, msg)
|
|
506
|
+
except SleepGitTimeout as e:
|
|
507
|
+
# 超时(issue #63):本阶段未完成——与「Δ 为空/未提交」等读数分开报。
|
|
508
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
509
|
+
"phase": "reconcile_and_commit", "git_dir": G,
|
|
510
|
+
"worktree_git_dir": GS, "shadow": S, "timeout": True,
|
|
511
|
+
"error": str(e), "lock": lock_path()}
|
|
452
512
|
after = source_face_hashes(root)
|
|
453
513
|
delta = face_delta(before, after)
|
|
454
514
|
return {"ok": not (delta["added"] or delta["removed"] or delta["changed"]),
|
|
@@ -481,27 +541,40 @@ def merge(root: str = None, *, git_dir_path: str = None, branch: str,
|
|
|
481
541
|
with FileLock(lock_path(), timeout=timeout, strict=False) as lk:
|
|
482
542
|
if not lk.acquired:
|
|
483
543
|
return _busy("merge", timeout)
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
544
|
+
try:
|
|
545
|
+
cur = _git_ok(G, root, "rev-parse", "--abbrev-ref", "HEAD").strip()
|
|
546
|
+
if cur != BASE_BRANCH:
|
|
547
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
548
|
+
"phase": "merge", "lock": lock_path(), "head": cur,
|
|
549
|
+
"error": "当前 HEAD 在 %s,merge 须在 %s 上执行"
|
|
550
|
+
% (cur, BASE_BRANCH)}
|
|
551
|
+
r = _git(G, root, "merge", "--no-ff", branch, "-m",
|
|
552
|
+
"sleep: merge %s into %s" % (branch, BASE_BRANCH))
|
|
553
|
+
if r.returncode != 0:
|
|
554
|
+
out = (r.stdout + r.stderr).strip()
|
|
555
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
556
|
+
"phase": "merge", "lock": lock_path(), "shadow": S,
|
|
557
|
+
"conflict": "CONFLICT" in out, "error": out[:500],
|
|
558
|
+
"source_face_delta": face_delta(before,
|
|
559
|
+
source_face_hashes(root)),
|
|
560
|
+
"hint": ("冲突不自动解决(照 hive/wm.py 既有裁决):"
|
|
561
|
+
"人工/LLM 裁决后 git add + git commit 收口,"
|
|
562
|
+
"或 git merge --abort 放弃本次合并")}
|
|
563
|
+
head = _git_ok(G, root, "rev-parse", "HEAD").strip()
|
|
564
|
+
except SleepGitTimeout as e:
|
|
565
|
+
# 超时(issue #63):合并**未完成**(不是冲突、也不是被拒)——
|
|
566
|
+
# 单独一档读数,冲突裁决路径不受影响。
|
|
567
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
568
|
+
"phase": "merge", "lock": lock_path(), "shadow": S,
|
|
569
|
+
"timeout": True, "error": str(e),
|
|
570
|
+
"hint": ("git 子进程超时已被 kill:合并未完成;"
|
|
571
|
+
"残留锁核删指引见 error")}
|
|
499
572
|
after = source_face_hashes(root)
|
|
500
573
|
delta = face_delta(before, after)
|
|
501
574
|
return {"ok": True, "busy": False, "lock_held": True, "phase": "merge",
|
|
502
|
-
"lock": lock_path(), "merged": branch,
|
|
503
|
-
"
|
|
504
|
-
"
|
|
575
|
+
"lock": lock_path(), "merged": branch, "commit": head,
|
|
576
|
+
"source_face_delta": delta,
|
|
577
|
+
"main_written": delta["added"] + delta["changed"]}
|
|
505
578
|
|
|
506
579
|
|
|
507
580
|
# 生效条件:commit 为版本库中存在的提交(或分支名);在根级锁内以其父提交数判是否 merge(git rev-list --parents -n1 字段数 > 2 即 merge),是 merge 则附 -m 1,执行 git revert --no-edit <参数> <commit>;returncode != 0 时返回 ok=False 与 conflict/error/hint,成功返回 ok=True、reverted、commit(新提交)与 source_face_delta。**只走 revert,不提供抹历史的强推档**。
|
|
@@ -521,22 +594,31 @@ def revert(commit: str, root: str = None, *, git_dir_path: str = None,
|
|
|
521
594
|
with FileLock(lock_path(), timeout=timeout, strict=False) as lk:
|
|
522
595
|
if not lk.acquired:
|
|
523
596
|
return _busy("revert", timeout)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
args
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
597
|
+
try:
|
|
598
|
+
parents = _git_ok(G, root, "rev-list", "--parents", "-n1",
|
|
599
|
+
commit).split()
|
|
600
|
+
args = ["revert", "--no-edit"]
|
|
601
|
+
if len(parents) > 2:
|
|
602
|
+
args += ["-m", "1"]
|
|
603
|
+
args.append(commit)
|
|
604
|
+
r = _git(G, root, *args)
|
|
605
|
+
if r.returncode != 0:
|
|
606
|
+
out = (r.stdout + r.stderr).strip()
|
|
607
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
608
|
+
"phase": "revert", "lock": lock_path(),
|
|
609
|
+
"conflict": "CONFLICT" in out, "error": out[:500],
|
|
610
|
+
"hint": ("冲突不自动解决;处理后可 git revert --continue "
|
|
611
|
+
"/ --abort")}
|
|
612
|
+
head = _git_ok(G, root, "rev-parse", "HEAD").strip()
|
|
613
|
+
except SleepGitTimeout as e:
|
|
614
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
615
|
+
"phase": "revert", "lock": lock_path(), "timeout": True,
|
|
616
|
+
"error": str(e),
|
|
617
|
+
"hint": ("git 子进程超时已被 kill:回滚未完成;"
|
|
618
|
+
"残留锁核删指引见 error")}
|
|
536
619
|
after = source_face_hashes(root)
|
|
537
620
|
return {"ok": True, "busy": False, "lock_held": True, "phase": "revert",
|
|
538
|
-
"lock": lock_path(), "reverted": commit,
|
|
539
|
-
"commit": _git_ok(G, root, "rev-parse", "HEAD").strip(),
|
|
621
|
+
"lock": lock_path(), "reverted": commit, "commit": head,
|
|
540
622
|
"merge_commit": len(parents) > 2,
|
|
541
623
|
"source_face_delta": face_delta(before, after)}
|
|
542
624
|
|
|
@@ -981,10 +1063,20 @@ def reconcile_gated(root: str = None, *, git_dir_path: str = None,
|
|
|
981
1063
|
gated = gate_delta(cgm, delta, baseline_face=baseline_face,
|
|
982
1064
|
shadow_root=S)
|
|
983
1065
|
GS = worktree_git_dir(S) or G
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1066
|
+
try:
|
|
1067
|
+
br = branch or _git_ok(GS, S, "rev-parse", "--abbrev-ref",
|
|
1068
|
+
"HEAD").strip()
|
|
1069
|
+
rels = _stage_set(delta, gated["accepted"])
|
|
1070
|
+
res = _commit_paths(GS, S, rels,
|
|
1071
|
+
"sleep: %s 轮对账提交" % (batch or _stamp()))
|
|
1072
|
+
except SleepGitTimeout as e:
|
|
1073
|
+
# 超时(issue #63):四闸裁决读数(delta / accepted / conflicts /
|
|
1074
|
+
# rejected)照带——只是入册没完成。
|
|
1075
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
1076
|
+
"phase": "reconcile_gated", "git_dir": G,
|
|
1077
|
+
"worktree_git_dir": GS, "shadow": S, "timeout": True,
|
|
1078
|
+
"error": str(e), "lock": lock_path(),
|
|
1079
|
+
"delta": delta, **gated}
|
|
988
1080
|
after = source_face_hashes(root)
|
|
989
1081
|
return {"ok": True, "busy": False, "lock_held": True,
|
|
990
1082
|
"phase": "reconcile_gated", "git_dir": G, "worktree_git_dir": GS,
|
|
@@ -1061,7 +1153,14 @@ def merge_cycle(cg_main, root: str = None, *, git_dir_path: str = None,
|
|
|
1061
1153
|
with FileLock(lock_path(), timeout=timeout, strict=False) as lk:
|
|
1062
1154
|
if not lk.acquired:
|
|
1063
1155
|
return _busy("merge_cycle", timeout)
|
|
1064
|
-
|
|
1156
|
+
try:
|
|
1157
|
+
cur = _git_ok(G, root, "rev-parse", "--abbrev-ref", "HEAD").strip()
|
|
1158
|
+
except SleepGitTimeout as e:
|
|
1159
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
1160
|
+
"phase": "merge_cycle", "lock": lock_path(),
|
|
1161
|
+
"branch": branch, "timeout": True, "error": str(e),
|
|
1162
|
+
"hint": ("git 子进程超时已被 kill:合并尚未开始;"
|
|
1163
|
+
"残留锁核删指引见 error")}
|
|
1065
1164
|
if cur != BASE_BRANCH:
|
|
1066
1165
|
return {"ok": False, "busy": False, "lock_held": True,
|
|
1067
1166
|
"phase": "merge_cycle", "lock": lock_path(), "head": cur,
|
|
@@ -1079,39 +1178,54 @@ def merge_cycle(cg_main, root: str = None, *, git_dir_path: str = None,
|
|
|
1079
1178
|
# 层迁移会把源层目录搬空,`pathspecs` 滤空目录后就把「旧位置删除」漏掉,
|
|
1080
1179
|
# 主库 HEAD 会留着旧文件 ⇒ 与影子分支的树不同(本机实测过这个漏)。
|
|
1081
1180
|
ps = _stage_set(delta or {}, accepted or [])
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1181
|
+
try:
|
|
1182
|
+
if ps:
|
|
1183
|
+
_git_ok(G, root, "add", "--", *ps)
|
|
1184
|
+
if _git(G, root, "diff", "--cached", "--quiet").returncode != 0:
|
|
1185
|
+
_git_ok(G, root, "commit", "-m",
|
|
1186
|
+
"sleep: %s 语义重放 Δ" % (batch or _stamp()))
|
|
1187
|
+
# **本轮内容所在的提交**(回退入口):按 §4.4 的次序,内容由
|
|
1188
|
+
# 「语义重放」先写进主库并提交,随后的 `--no-ff` 合并只是**拓扑记录**
|
|
1189
|
+
# (其树与第一父提交相同 ⇒ `git revert -m 1 <合并提交>` 对内容零影响,
|
|
1190
|
+
# 回退要指向本提交;ops 文档据此登记)。
|
|
1191
|
+
out["round_commit"] = _git_ok(G, root, "rev-parse",
|
|
1192
|
+
"HEAD").strip()
|
|
1193
|
+
out["replay_commit"] = _git_ok(G, root, "rev-parse", "HEAD").strip()
|
|
1194
|
+
# 仅当两侧树**完全相同**才合并(否则让 git 的文本自动合并去裁决就会绕过四闸)
|
|
1195
|
+
same = _git(G, root, "diff", "--quiet", branch,
|
|
1196
|
+
"HEAD").returncode == 0
|
|
1197
|
+
if not same:
|
|
1198
|
+
changed = [ln.strip() for ln in _git(
|
|
1199
|
+
G, root, "diff", "--name-only", branch,
|
|
1200
|
+
"HEAD").stdout.splitlines() if ln.strip()]
|
|
1201
|
+
out.update(deferred="tree_divergence",
|
|
1202
|
+
note=("影子分支与重放后的主库树不同 ⇒ 挂起不合并"
|
|
1203
|
+
"(不自动解决)"),
|
|
1204
|
+
conflict_ids=sorted({os.path.basename(p)[:-3]
|
|
1205
|
+
for p in changed}))
|
|
1206
|
+
out["source_face_delta"] = face_delta(before,
|
|
1207
|
+
source_face_hashes(root))
|
|
1208
|
+
return out
|
|
1209
|
+
r = _git(G, root, "merge", "--no-ff", branch, "-m",
|
|
1210
|
+
"sleep: merge %s into %s" % (branch, BASE_BRANCH))
|
|
1211
|
+
if r.returncode != 0:
|
|
1212
|
+
txt = (r.stdout + r.stderr).strip()
|
|
1213
|
+
out.update(ok=False, conflict="CONFLICT" in txt, error=txt[:500],
|
|
1214
|
+
hint=("冲突不自动解决(照 hive/wm.py 既有裁决):"
|
|
1215
|
+
"人工裁决后 git add + git commit 收口,"
|
|
1216
|
+
"或放弃本次合并"))
|
|
1217
|
+
out["source_face_delta"] = face_delta(before,
|
|
1218
|
+
source_face_hashes(root))
|
|
1219
|
+
return out
|
|
1220
|
+
out["merge_commit"] = _git_ok(G, root, "rev-parse", "HEAD").strip()
|
|
1221
|
+
except SleepGitTimeout as e:
|
|
1222
|
+
# 超时(issue #63):重放后的入册/合并**未完成**(既非冲突、也非
|
|
1223
|
+
# 树分歧)——单独一档读数,与上面两条裁决路径分开。
|
|
1224
|
+
out.update(ok=False, timeout=True, error=str(e),
|
|
1225
|
+
hint=("git 子进程超时已被 kill:入册/合并未完成;"
|
|
1226
|
+
"残留锁核删指引见 error"))
|
|
1112
1227
|
out["source_face_delta"] = face_delta(before, source_face_hashes(root))
|
|
1113
1228
|
return out
|
|
1114
|
-
out["merge_commit"] = _git_ok(G, root, "rev-parse", "HEAD").strip()
|
|
1115
1229
|
out["merged"] = branch
|
|
1116
1230
|
after = source_face_hashes(root)
|
|
1117
1231
|
dlt = face_delta(before, after)
|
|
@@ -1270,13 +1384,18 @@ def run_cycle(cg=None, *, root: str = None, batch: str = None,
|
|
|
1270
1384
|
phases["materialize"] = {k: m.get(k) for k in
|
|
1271
1385
|
("ok", "face_stable", "busy", "branch", "shadow",
|
|
1272
1386
|
"git_dir", "baseline", "source_face_delta",
|
|
1273
|
-
"reason", "error")}
|
|
1387
|
+
"reason", "error", "timeout")}
|
|
1274
1388
|
if not m.get("ok"):
|
|
1275
|
-
# 物化**未完成**(取不到根级锁 /
|
|
1276
|
-
#
|
|
1389
|
+
# 物化**未完成**(取不到根级锁 / 影子未能挂上 / git 超时)——与
|
|
1390
|
+
# 「门拦」区分记账:门拦是「按设计不动手」,这里是「想动手却没做成」,
|
|
1391
|
+
# 用户要能一眼分开;超时再单列一档文案(issue #63:物化超时 ≠
|
|
1392
|
+
# 物化失败——前者是操作没完成、后者是 git 的裁决)。
|
|
1277
1393
|
mat_fail = ("物化未完成:根级锁被占(另一进程在周期内)"
|
|
1278
1394
|
if m.get("busy")
|
|
1279
|
-
else "
|
|
1395
|
+
else ("物化超时:%s" % (m.get("error") or "未知原因")
|
|
1396
|
+
if m.get("timeout")
|
|
1397
|
+
else "物化未完成:%s" % (m.get("error")
|
|
1398
|
+
or "未知原因")))
|
|
1280
1399
|
if m.get("ok"):
|
|
1281
1400
|
# **物化成功即迭代**(issue #60):`ok` 现在只说「影子挂上了没」;
|
|
1282
1401
|
# 物化窗口内主库真源面的漂移另记 `face_stable`,**不阻断**迭代——
|
|
@@ -1292,7 +1411,7 @@ def run_cycle(cg=None, *, root: str = None, batch: str = None,
|
|
|
1292
1411
|
cg=cg, timeout=timeout)
|
|
1293
1412
|
phases["reconcile"] = {k: rc.get(k) for k in
|
|
1294
1413
|
("committed", "commit", "branch", "reason",
|
|
1295
|
-
"busy")}
|
|
1414
|
+
"busy", "timeout", "error")}
|
|
1296
1415
|
delta = rc.get("delta") or {}
|
|
1297
1416
|
digest = {"n": len(delta), "ids": sorted(delta)[:20],
|
|
1298
1417
|
"kinds": {k: sum(1 for v in delta.values() if v["kind"] == k)
|
|
@@ -1313,7 +1432,8 @@ def run_cycle(cg=None, *, root: str = None, batch: str = None,
|
|
|
1313
1432
|
("ok", "merged", "merge_commit",
|
|
1314
1433
|
"round_commit", "replay_commit",
|
|
1315
1434
|
"deferred", "conflict", "conflict_ids",
|
|
1316
|
-
"error", "main_written", "busy"
|
|
1435
|
+
"error", "main_written", "busy",
|
|
1436
|
+
"timeout")}
|
|
1317
1437
|
phases["merge"]["replayed"] = (mg.get("replayed") or {}).get("written")
|
|
1318
1438
|
rec["merged"] = bool(mg.get("ok")) and not mg.get("deferred")
|
|
1319
1439
|
if mg.get("deferred"):
|