Skip to content

Commit bb94abf

Browse files
committed
fix: preserve Windows lock retry policy
1 parent cce1573 commit bb94abf

4 files changed

Lines changed: 28 additions & 3 deletions

File tree

‎changelogs/releases/v1.19.1.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ v1.19.1 是 v1.19.0 发布失败后的收敛补丁。它保留 v1.19.0 已完成
1414
- **ContextRead 稳定折叠**:观察结果先持久化,再在 TaskRecoveryStoreV5 CAS 内读取完整 durable ledger;按计划顺序和确定性质量规则选择每个 source 的唯一结果,避免乱序/重复事件把 rebind 或 `load_stage` 拉回陈旧循环。
1515
- **ManagedValidationRunner 可恢复**:增加 `node-start`、30 秒持久化心跳和逐节点紧凑检查点;同一 run/candidate/plan/manifest/lease 下可验证成功前缀并精确续跑,候选漂移或检查点不匹配则失败关闭。
1616
- **V5 Windows 写入韧性**:派生的 singleton usage ledger 遇到 Windows `EPERM`、`EACCES` 或 `EBUSY` 时先执行有界重试;仍被瞬时共享锁占用时,只对可重建账本使用受 store lock 保护且必须精确 readback 的 copy fallback。正式 task hot A/B 槽不放宽为非原子 copy,`EIO` 等非共享故障继续失败关闭,hard-limit 预充仍须先于状态写入成功。
17-
- **Windows writer lock 生命周期收敛**:TaskRecoveryStoreV5、SkillRoute、WorkspaceSessionRouteIndex、derived index、workflow completion、runtime-generation GC 与全局宿主事务共用有界 sharing retry;record 写失败只清理本 writer 创建的半锁,release 回读 owner,unreadable owner 不再被误删,`process.kill(pid, 0)` 的 `EPERM` 继续代表 live owner。真实 `EEXIST`、lease/owner 冲突和 `EIO` 仍失败关闭。
17+
- **Windows writer lock 生命周期收敛**:TaskRecoveryStoreV5、SkillRoute、WorkspaceSessionRouteIndex、derived index、workflow completion、runtime-generation GC 与全局宿主事务共用有界 sharing retry;全局宿主事务显式把 platform/retry budget 传到 journal-lock Owner,Windows sharing 正例与 Linux `EPERM` 失败关闭负例可在任意 CI 宿主确定性复现。record 写失败只清理本 writer 创建的半锁,release 回读 owner,unreadable owner 不再被误删,`process.kill(pid, 0)` 的 `EPERM` 继续代表 live owner。真实 `EEXIST`、lease/owner 冲突和 `EIO` 仍失败关闭。
1818
- **收敛优先验证**:多问题和发布任务先冻结完整问题集、批量修复,再执行一次 affected 验证;runner 在失败后继续执行独立节点,用 `failedNodes` 一次收齐失败,只跳过依赖失败/阻断节点并记录原因,durable terminal projection 同步保留有界完整失败节点集合与紧凑摘要。只有最终候选冻结后才执行一次 release V3/full,禁止逐问题重复跑重型验证。
1919
- **控制中止原因完整**:worker send、protocol、timeout、runner-state persistence 等控制面终止现在为每个尚未执行的节点写入 `VALIDATION_CONTROL_ABORTED + controlCode`,`abortedNodes` 与 `abortedNodeReasons` 始终一一对应,不再留下无法解释的中止清单。
2020
- **发布路由与工作区卫生闭包**:`test-router` 现在显式记录 publish dry-run,并明确它不能替代 exact artifact、远端 CI、真实 publish 或 registry 后验收。`release-verification` 不建立影子权限判断,实际操作权限继续由宿主决定;发布流程自身不得自动删除、reset、checkout、stash 或覆盖无归属、用户或并行任务 dirty 状态。

‎changelogs/unreleased.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- **CP3 v1.6 工作流与任务所有权 P0 候选(发布资格验证中)**:新增 `ActualInstructionEnvelopeV1 → WorkItemSetV1 → WorkflowRouteDecisionV2`,附件、截图/OCR、引用文档、工具输出与 ambient UI 仅作证据,不能反向覆盖用户真实指令。`WorkspaceSessionRouteIndexV1` 只提供 hint,`ProjectTargetLeaseV2` 精确绑定 session/project/root/context/route;正式任务由 server-owned `TaskAdmissionTransactionV1` 一次性写入 task identity、`00-需求概况.md` 与用户原样 `01-产品需求.md`,以 ingress idempotency 和 `FencedTaskWriteOwnerLeaseV2` 阻止重复准入、跨项目/跨任务写入、mtime 选错任务及 terminal 旧绑定复活。Simple task 仅使用 server-issued 两路径/两次租约,漂移立即升级正式流程。
1111
- **正式产物、Hook mutation 与恢复单一权威**:`MutationFootprintV2 → LayeredArtifactSlotRegistryV2 → ArtifactSlotDecisionV2 → TaskOwnedMutationLeaseV2 → TaskRecoveryStoreV5 prewrite → MutationObservationReceiptV1` 成为 Hook/MCP/宿主工具的统一写链;unknown writer、未观察实际效果、跨 active-root、重复消费与错误槽位均 fail closed。V5 继续使用 task hot A/B、cold stub、terminal 退出缓存、256/512 MiB 和 8 MiB closeout reserve,不再按每次 Hook/工具状态创建 UUID generation;hot 槽的容量判定、usage ledger 预充与实际写盘共用紧凑 JSON 字节序列,避免格式化空白把合法恢复状态误判为超过 256 KiB。产品提供型 `01-产品需求.md` 保持用户原始真相且 AI 不得改写。
1212
- **V5 Windows ledger replace 收敛(PI-338 / PF-425)**:派生 singleton usage ledger 的 `EPERM/EACCES/EBUSY` replace 使用有界重试,耗尽后仅对该可重建账本启用 store-lock 内的精确 readback copy fallback;task hot A/B 不采用非原子 fallback,`EIO` 等非共享故障继续 fail closed,预充账本成功仍是 task 状态写入前提。
13-
- **Windows writer lock 生命周期收敛(PI-340 / PF-429~PF-434)**:TaskRecoveryStoreV5、SkillRoute、WorkspaceSessionRouteIndex、derived index、workflow completion、runtime-generation GC 与全局宿主事务统一把 Windows `EPERM/EACCES/EBUSY` 作为有界 sharing retry;record 写失败只清理本 writer 创建的半锁,release 回读 owner,unreadable owner 不再被误删,`process.kill(pid, 0)` 的 `EPERM` 继续代表 live owner。真实 `EEXIST`、lease/owner 冲突和 `EIO` 仍失败关闭。
13+
- **Windows writer lock 生命周期收敛(PI-340 / PI-343 / PF-429~PF-434 / PF-438)**:TaskRecoveryStoreV5、SkillRoute、WorkspaceSessionRouteIndex、derived index、workflow completion、runtime-generation GC 与全局宿主事务统一把 Windows `EPERM/EACCES/EBUSY` 作为有界 sharing retry;全局宿主事务现在把显式 platform/retry budget 传到 journal-lock Owner,使 Ubuntu CI 可稳定复现 Windows 正例,并同时证明 Linux `EPERM` 只尝试一次后失败关闭。record 写失败只清理本 writer 创建的半锁,release 回读 owner,unreadable owner 不再被误删,`process.kill(pid, 0)` 的 `EPERM` 继续代表 live owner。真实 `EEXIST`、lease/owner 冲突和 `EIO` 仍失败关闭。
1414
- **reconciliation 恢复闭环(PI-322 / PF-398~PF-407、PF-409)**:`memory_artifact_mutation_reconcile_v1` 复证 exact current ingress、same-session formal route、operation/closeout CAS、primary/reserve 来源和实际 effect snapshot;只关闭既有 pending closeout,不执行文件 mutation、不签发权限。exact-target/controlled-root pre-observation、逐层目录遍历与 descriptor/path 稳定回读共同防止范围扩张和并发漂移;正式任务强制 exact taskId。Memory server 每次复验 ProjectTargetLease 的 digest、TTL、context、route 与 workspace 当前 physical root,不再信任扩根 fallback。`TaskAdmissionReconciliationReceiptV1` 对同一请求采用 exact readback,完整前缀自动恢复,产品原文/概况等阶段内部分落盘由 create-if-absent 幂等补齐;drift 保持 fail closed。Lifecycle、V5 projection 与 validation continuation 统一消费 `reconciled` receipt,跨宿主回执统一走 canonical MCP leaf。
1515
- **ConvergenceFirst 失败集聚合(PI-317 / PF-408)**:affected/full runner 不再首错即停;独立节点继续执行并写入 `failedNodes`,只有依赖失败/阻断的下游进入 `abortedNodes + abortedNodeReasons`;durable terminal projection 有界保存完整失败节点集合与紧凑摘要,重启后不退化为首错视图。issue-set、repair generation、implementation complete 与 final freeze 保持由工作流/发布编排 Owner 持有,不伪装成 ValidationPlanV3 原生字段。
1616
- **控制中止逐节点原因(PI-339 / PF-426)**:worker send、protocol、timeout、runner-state persistence 等控制面终态为全部未执行节点生成 `VALIDATION_CONTROL_ABORTED + controlCode`,保证 `abortedNodes` 与 `abortedNodeReasons` 一一对应并可在 durable terminal projection 中解释。

‎scripts/lib/global-host-config-transaction.js‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,10 @@ function executeGlobalHostTransaction(operations, options = {}) {
513513
pid: options.pid,
514514
processKill: options.processKill,
515515
ownerToken: options.ownerToken,
516-
nowMs: options.nowMs
516+
nowMs: options.nowMs,
517+
platform: options.platform,
518+
windowsFsRetryMaxAttempts: options.windowsFsRetryMaxAttempts,
519+
windowsFsRetryDelayMs: options.windowsFsRetryDelayMs
517520
})
518521
try {
519522
recoveredTransactions = recoverIndexedTransactions(transactionRoot, {

‎scripts/test-global-host-config.js‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,28 @@ assert.strictEqual(transientTransactionOpenAttempts, 3)
15521552
assert.strictEqual(transientTransactionUnlinkAttempts, 2)
15531553
assert.strictEqual(fs.readFileSync(transientTransactionFile, 'utf8'), 'transient lock recovered\n')
15541554

1555+
const nonWindowsTransactionRoot = path.join(tmp, 'non-windows-transaction-lock')
1556+
fs.mkdirSync(nonWindowsTransactionRoot, { recursive: true })
1557+
let nonWindowsTransactionOpenAttempts = 0
1558+
const nonWindowsTransactionFs = Object.create(fs)
1559+
nonWindowsTransactionFs.openSync = (file, flags, ...rest) => {
1560+
if (flags === 'wx' && path.basename(String(file)) === 'owner.lock') {
1561+
nonWindowsTransactionOpenAttempts += 1
1562+
throw Object.assign(new Error('injected non-Windows transaction lock EPERM'), { code: 'EPERM' })
1563+
}
1564+
return fs.openSync(file, flags, ...rest)
1565+
}
1566+
assert.throws(() => executeGlobalHostTransaction([
1567+
{ path: path.join(nonWindowsTransactionRoot, 'target.txt'), content: 'must not be written\n' }
1568+
], {
1569+
allowedRoots: [nonWindowsTransactionRoot],
1570+
fs: nonWindowsTransactionFs,
1571+
platform: 'linux',
1572+
windowsFsRetryMaxAttempts: 3,
1573+
windowsFsRetryDelayMs: 0
1574+
}), error => error?.code === 'EPERM')
1575+
assert.strictEqual(nonWindowsTransactionOpenAttempts, 1)
1576+
15551577
const metadataRoot = path.join(tmp, 'replacement-metadata')
15561578
fs.mkdirSync(metadataRoot, { recursive: true })
15571579
const metadataFile = path.join(metadataRoot, 'restricted.txt')

0 commit comments

Comments
 (0)