@lmzhen/dsh-evolution-core 0.32.3 → 0.32.4
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 +5 -5
- package/lib/composition/index.js +2 -2
- package/lib/{composition-DSesmTvf.js → composition-3QFfEe56.js} +1 -1
- package/lib/{ensure-BuWsjPGD.js → ensure-C2tUoF5U.js} +1 -1
- package/lib/{errors-Br2-uUSK.js → errors-DRZ9A187.js} +1 -0
- package/lib/index.js +423 -113
- package/lib/restrictor.js +1 -1
- package/lib/subagent/index.js +1 -1
- package/lib/types/errors.d.ts +1 -0
- package/lib/types/errors.js +1 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/mutation-storage-adapter.d.ts +5 -2
- package/lib/types/mutation-storage-adapter.js +59 -22
- package/lib/types/mutation-unit.d.ts +2 -2
- package/lib/types/mutation-unit.js +18 -45
- package/lib/types/params.js +1 -1
- package/lib/types/platform-services.js +0 -1
- package/lib/types/session-projection.d.ts +14 -0
- package/lib/types/session-projection.js +12 -3
- package/lib/types/skill-reads.d.ts +19 -0
- package/lib/types/skill-reads.js +42 -2
- package/lib/types/skill-store.js +4 -2
- package/lib/types/tool-dispatch.d.ts +16 -0
- package/lib/types/tool-dispatch.js +42 -11
- package/lib/types/unit-journal.d.ts +5 -3
- package/lib/types/unit-journal.js +7 -6
- package/lib/types/unit-storage.d.ts +5 -4
- package/lib/types/unit-storage.js +8 -8
- package/lib/types/unit-transaction.js +5 -7
- package/lib/types/write-gates.d.ts +4 -0
- package/lib/types/write-gates.js +16 -1
- package/lib/types/write-kernel.d.ts +81 -0
- package/lib/types/write-kernel.js +155 -0
- package/package.json +2 -2
- package/persisted-write-inventory.json +1 -10
package/lib/restrictor.js
CHANGED
package/lib/subagent/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as restrictedPresetId, n as ensureRestrictedPreset, r as restrictedPresetState } from "../ensure-
|
|
1
|
+
import { h as restrictedPresetId, n as ensureRestrictedPreset, r as restrictedPresetState } from "../ensure-C2tUoF5U.js";
|
|
2
2
|
//#region lib/types/subagent/readiness.js
|
|
3
3
|
function serviceOf$1(ctx, name) {
|
|
4
4
|
return ctx.get(name);
|
package/lib/types/errors.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export declare const EVOLUTION_ERRORS: Readonly<{
|
|
|
35
35
|
readonly 'e-310-the-settings-service-refused': "E-310: 设置服务拒绝了这次写入:%a1% —— 归属插件的规则优先(跨字段成对约束,或只能收紧的上限)。/evolution params 会显示当前值。";
|
|
36
36
|
readonly 'e-317-skill-write-not-confirmed': "E-317: 技能 \"%a1%\" 未被%a2% —— 确认提示被拒绝或关闭,因此没有写入任何内容。每次创建与裸删除都会出现该提示;只有操作者要求时才重复调用。";
|
|
37
37
|
readonly 'e-318-skill-write-without-a-read': "E-318: 本会话没有读取技能 \"%a1%\",因此拒绝这次写入。请先用 `skill` 工具读取它(失败的读取不算),然后重复该写入。";
|
|
38
|
+
readonly 'e-326-support-write-without-a-read': "E-326: 本会话没有读取技能 \"%a1%\" 的支撑文件 \"%a2%\",因此拒绝这次写入。只读技能正文不算读过支撑文件;请先读取该文件(失败的读取不算),然后重复该写入。";
|
|
38
39
|
readonly 'e-320-the-profile-home-is-not': "E-320: 本宿主没有报告 profile home,因此无法定位旧设置文档。/evolution migrate 读取 `<profile home>/settings.yaml.imported`;请在由 profile 启动的会话里运行。";
|
|
39
40
|
readonly 'e-322-a-parameter-would-break-a': "E-322: %a1% 至少应为 %a2%,但这次写入把它设成 %a3%。未写入任何内容。";
|
|
40
41
|
readonly 'e-321-no-io-provider-is': "E-321: 没有挂载 evolution-io 提供方,因此无法读取旧设置文档。请挂载 evolution-io 行(evolution-host/evolution-all)后重试。";
|
package/lib/types/errors.js
CHANGED
|
@@ -36,6 +36,7 @@ export const EVOLUTION_ERRORS = Object.freeze({
|
|
|
36
36
|
// 0.9.0: the write-admission sequence's own refusals (tool-skill-manage/src/write-gates.ts).
|
|
37
37
|
'e-317-skill-write-not-confirmed': 'E-317: 技能 "%a1%" 未被%a2% —— 确认提示被拒绝或关闭,因此没有写入任何内容。每次创建与裸删除都会出现该提示;只有操作者要求时才重复调用。',
|
|
38
38
|
'e-318-skill-write-without-a-read': 'E-318: 本会话没有读取技能 "%a1%",因此拒绝这次写入。请先用 `skill` 工具读取它(失败的读取不算),然后重复该写入。',
|
|
39
|
+
'e-326-support-write-without-a-read': 'E-326: 本会话没有读取技能 "%a1%" 的支撑文件 "%a2%",因此拒绝这次写入。只读技能正文不算读过支撑文件;请先读取该文件(失败的读取不算),然后重复该写入。',
|
|
39
40
|
// 0.12.0: the confirmation's own deadline (`skillWriteConfirm: 'timeout'`).
|
|
40
41
|
'e-320-the-profile-home-is-not': 'E-320: 本宿主没有报告 profile home,因此无法定位旧设置文档。/evolution migrate 读取 `<profile home>/settings.yaml.imported`;请在由 profile 启动的会话里运行。',
|
|
41
42
|
// G5.12/U-4: the cross-field constraint the registry declares (params.ts) has one enforcement
|
package/lib/types/index.d.ts
CHANGED
|
@@ -100,6 +100,7 @@ export * from './skill-revision.ts';
|
|
|
100
100
|
export * from './store-ledger.ts';
|
|
101
101
|
export * from './reachability.ts';
|
|
102
102
|
export * from './state-store.ts';
|
|
103
|
+
export * from './write-kernel.ts';
|
|
103
104
|
export * from './threats.ts';
|
|
104
105
|
export * from './tool-dispatch.ts';
|
|
105
106
|
export * from './usage.ts';
|
package/lib/types/index.js
CHANGED
|
@@ -101,6 +101,7 @@ export * from "./skill-revision.js";
|
|
|
101
101
|
export * from "./store-ledger.js";
|
|
102
102
|
export * from "./reachability.js";
|
|
103
103
|
export * from "./state-store.js";
|
|
104
|
+
export * from "./write-kernel.js";
|
|
104
105
|
export * from "./threats.js";
|
|
105
106
|
export * from "./tool-dispatch.js";
|
|
106
107
|
export * from "./usage.js";
|
|
@@ -6,9 +6,12 @@ import type { MutationJournalPort, MutationSnapshotPort } from './mutation-unit.
|
|
|
6
6
|
* snapshots do not carry that kind and are ignored by the reader.
|
|
7
7
|
*/
|
|
8
8
|
export declare function unitSnapshotPort(io: EvolutionIoLike, home: string): MutationSnapshotPort;
|
|
9
|
+
/** Keep a bounded number of terminal tree-change snapshots; prepared units are never pruned. */
|
|
10
|
+
export declare function pruneTreeChangeUnits(io: EvolutionIoLike, home: string, keep?: number): Promise<number>;
|
|
9
11
|
/**
|
|
10
|
-
* Production adapter onto the
|
|
11
|
-
*
|
|
12
|
+
* Production adapter onto the skill-owned journal directory. Merge recovery
|
|
13
|
+
* reads merge/unit-journals only; legacy tree-change journals carry kind and
|
|
14
|
+
* are skipped by the recovery coordinator.
|
|
12
15
|
*/
|
|
13
16
|
export declare function unitJournalPort(io: EvolutionIoLike, home: string): MutationJournalPort;
|
|
14
17
|
//# sourceMappingURL=mutation-storage-adapter.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { contentHash } from "./mutations.js";
|
|
3
3
|
import { readUnitSnapshot, unitSnapshotsDir, unitStorageKey, writeUnitSnapshot } from "./unit-storage.js";
|
|
4
|
-
import { writeUnitJournal } from "./unit-journal.js";
|
|
4
|
+
import { unitJournalPath, writeUnitJournal } from "./unit-journal.js";
|
|
5
5
|
/**
|
|
6
6
|
* Production adapter onto the existing unit snapshot store. A tree-change
|
|
7
7
|
* snapshot is encoded as a UnitSnapshot with kind='tree-change'; merge
|
|
@@ -36,10 +36,10 @@ export function unitSnapshotPort(io, home) {
|
|
|
36
36
|
...(op.skill === undefined ? {} : { skill: op.skill }),
|
|
37
37
|
})),
|
|
38
38
|
};
|
|
39
|
-
await writeUnitSnapshot(io, home, body);
|
|
39
|
+
await writeUnitSnapshot(io, home, body, 'skill');
|
|
40
40
|
},
|
|
41
41
|
async read(unitId) {
|
|
42
|
-
const snapshot = await readUnitSnapshot(io, home, unitId);
|
|
42
|
+
const snapshot = await readUnitSnapshot(io, home, unitId, 'skill') ?? await readUnitSnapshot(io, home, unitId);
|
|
43
43
|
if (snapshot === null || snapshot.kind !== 'tree-change' || snapshot.ops === undefined)
|
|
44
44
|
return null;
|
|
45
45
|
return {
|
|
@@ -51,29 +51,64 @@ export function unitSnapshotPort(io, home) {
|
|
|
51
51
|
};
|
|
52
52
|
},
|
|
53
53
|
async listPrepared() {
|
|
54
|
-
const names = await io.list(unitSnapshotsDir(home));
|
|
55
54
|
const out = [];
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const dir of [unitSnapshotsDir(home, 'skill'), unitSnapshotsDir(home)]) {
|
|
57
|
+
const names = await io.list(dir);
|
|
58
|
+
for (const name of names) {
|
|
59
|
+
if (!name.endsWith('.json'))
|
|
60
|
+
continue;
|
|
61
|
+
const raw = await io.readText(join(dir, name));
|
|
62
|
+
if (raw === null)
|
|
63
|
+
continue;
|
|
64
|
+
let snapshot;
|
|
65
|
+
try {
|
|
66
|
+
snapshot = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (snapshot.kind !== 'tree-change' || snapshot.mutationState !== 'prepared' || snapshot.ops === undefined)
|
|
72
|
+
continue;
|
|
73
|
+
if (seen.has(snapshot.unitId))
|
|
74
|
+
continue;
|
|
75
|
+
seen.add(snapshot.unitId);
|
|
76
|
+
out.push({ unitId: snapshot.unitId, state: 'prepared', ops: snapshot.ops, ...(snapshot.cleanup === undefined ? {} : { cleanup: snapshot.cleanup }), createdAt: snapshot.createdAt });
|
|
65
77
|
}
|
|
66
|
-
catch {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (snapshot.kind !== 'tree-change' || snapshot.mutationState !== 'prepared' || snapshot.ops === undefined)
|
|
70
|
-
continue;
|
|
71
|
-
out.push({ unitId: snapshot.unitId, state: 'prepared', ops: snapshot.ops, ...(snapshot.cleanup === undefined ? {} : { cleanup: snapshot.cleanup }), createdAt: snapshot.createdAt });
|
|
72
78
|
}
|
|
73
79
|
return out;
|
|
74
80
|
},
|
|
75
81
|
};
|
|
76
82
|
}
|
|
83
|
+
/** Keep a bounded number of terminal tree-change snapshots; prepared units are never pruned. */
|
|
84
|
+
export async function pruneTreeChangeUnits(io, home, keep = 500) {
|
|
85
|
+
if (!Number.isInteger(keep) || keep < 1)
|
|
86
|
+
return 0;
|
|
87
|
+
const dir = unitSnapshotsDir(home, 'skill');
|
|
88
|
+
const names = await io.list(dir);
|
|
89
|
+
const terminal = [];
|
|
90
|
+
for (const name of names) {
|
|
91
|
+
if (!name.endsWith('.json'))
|
|
92
|
+
continue;
|
|
93
|
+
try {
|
|
94
|
+
const snapshot = JSON.parse(await io.readText(join(dir, name)) ?? '');
|
|
95
|
+
if (snapshot.kind !== 'tree-change' || snapshot.mutationState === 'prepared')
|
|
96
|
+
continue;
|
|
97
|
+
terminal.push({ name, unitId: snapshot.unitId, createdAt: snapshot.createdAt });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// unreadable files are left for the explicit recovery/audit path
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
terminal.sort((a, b) => b.createdAt - a.createdAt);
|
|
104
|
+
let removed = 0;
|
|
105
|
+
for (const entry of terminal.slice(keep)) {
|
|
106
|
+
await io.remove(join(dir, entry.name));
|
|
107
|
+
await io.remove(unitJournalPath(home, entry.unitId, 'skill')).catch(() => { });
|
|
108
|
+
removed += 1;
|
|
109
|
+
}
|
|
110
|
+
return removed;
|
|
111
|
+
}
|
|
77
112
|
const PHASE = {
|
|
78
113
|
prepared: 'prepared',
|
|
79
114
|
'unit-written': 'unit-written',
|
|
@@ -82,8 +117,9 @@ const PHASE = {
|
|
|
82
117
|
undone: 'rolled-back',
|
|
83
118
|
};
|
|
84
119
|
/**
|
|
85
|
-
* Production adapter onto the
|
|
86
|
-
*
|
|
120
|
+
* Production adapter onto the skill-owned journal directory. Merge recovery
|
|
121
|
+
* reads merge/unit-journals only; legacy tree-change journals carry kind and
|
|
122
|
+
* are skipped by the recovery coordinator.
|
|
87
123
|
*/
|
|
88
124
|
export function unitJournalPort(io, home) {
|
|
89
125
|
return {
|
|
@@ -98,9 +134,10 @@ export function unitJournalPort(io, home) {
|
|
|
98
134
|
phase: PHASE[record.phase],
|
|
99
135
|
storageKey: unitStorageKey(record.unitId),
|
|
100
136
|
snapshotSha256: contentHash(JSON.stringify(record)),
|
|
137
|
+
kind: 'tree-change',
|
|
101
138
|
updatedAt: Date.now(),
|
|
102
139
|
};
|
|
103
|
-
await writeUnitJournal(io, home, journal);
|
|
140
|
+
await writeUnitJournal(io, home, journal, 'skill');
|
|
104
141
|
},
|
|
105
142
|
};
|
|
106
143
|
}
|
|
@@ -107,7 +107,7 @@ export interface MutationUnitOptions {
|
|
|
107
107
|
hooks?: MutationUnitHooks;
|
|
108
108
|
validateWrite?: (op: TreeChangeOp) => string | null;
|
|
109
109
|
transact?: typeof transactIo;
|
|
110
|
-
/**
|
|
110
|
+
/** @deprecated the kernel always re-checks the baseline; kept for source compatibility. */
|
|
111
111
|
transactGuard?: boolean;
|
|
112
112
|
now?: () => number;
|
|
113
113
|
}
|
|
@@ -135,7 +135,7 @@ export declare class MutationUnit {
|
|
|
135
135
|
private readonly snapshots;
|
|
136
136
|
private readonly hooks;
|
|
137
137
|
private readonly validateWrite;
|
|
138
|
-
private readonly
|
|
138
|
+
private readonly kernel;
|
|
139
139
|
private readonly transactGuard;
|
|
140
140
|
private readonly now;
|
|
141
141
|
constructor(options: MutationUnitOptions);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { isCommittedWarning, isProcessAlive } from "./io.js";
|
|
4
|
+
import { WriteKernel } from "./write-kernel.js";
|
|
4
5
|
function underRoot(rootDir, target) {
|
|
5
6
|
if (rootDir === undefined || rootDir === '')
|
|
6
7
|
return false;
|
|
@@ -101,7 +102,7 @@ export class MutationUnit {
|
|
|
101
102
|
snapshots;
|
|
102
103
|
hooks;
|
|
103
104
|
validateWrite;
|
|
104
|
-
|
|
105
|
+
kernel;
|
|
105
106
|
transactGuard;
|
|
106
107
|
now;
|
|
107
108
|
constructor(options) {
|
|
@@ -113,9 +114,10 @@ export class MutationUnit {
|
|
|
113
114
|
this.snapshots = options.snapshots;
|
|
114
115
|
this.hooks = options.hooks;
|
|
115
116
|
this.validateWrite = options.validateWrite;
|
|
116
|
-
this.
|
|
117
|
-
this.transactGuard = options.transactGuard ?? true;
|
|
117
|
+
this.transactGuard = options.transactGuard !== false;
|
|
118
118
|
this.now = options.now ?? Date.now;
|
|
119
|
+
const transact = options.transact;
|
|
120
|
+
this.kernel = new WriteKernel({ io: this.io, ...(transact === undefined ? {} : { transact }), now: this.now });
|
|
119
121
|
}
|
|
120
122
|
preview(request) {
|
|
121
123
|
const validation = validateMutationRequest(request, this.limits);
|
|
@@ -163,7 +165,7 @@ export class MutationUnit {
|
|
|
163
165
|
if (current !== baseline) {
|
|
164
166
|
return { ok: false, unitId, dryRun: false, changed, errors: [{ code: 'cas-drift', message: 'target changed after plan', target: op.target }] };
|
|
165
167
|
}
|
|
166
|
-
snapshots.push({ target: op.target, before: current, baseline });
|
|
168
|
+
snapshots.push({ target: op.target, before: current, after: op.kind === 'write' ? op.content : null, baseline });
|
|
167
169
|
}
|
|
168
170
|
for (const op of validation.ops) {
|
|
169
171
|
const refusal = this.validateWrite?.(op) ?? null;
|
|
@@ -188,7 +190,8 @@ export class MutationUnit {
|
|
|
188
190
|
if (snapshot === undefined)
|
|
189
191
|
throw new Error('snapshot missing for ' + op.target);
|
|
190
192
|
try {
|
|
191
|
-
await this.applyOp(op, snapshot.baseline);
|
|
193
|
+
const opWarnings = await this.applyOp(op, snapshot.baseline);
|
|
194
|
+
warnings.push(...opWarnings);
|
|
192
195
|
changed.push(op.target);
|
|
193
196
|
}
|
|
194
197
|
catch (error) {
|
|
@@ -200,9 +203,11 @@ export class MutationUnit {
|
|
|
200
203
|
}
|
|
201
204
|
}
|
|
202
205
|
await bestEffort(async () => this.journal?.write({ unitId, phase: 'unit-written', changed, target: primary, roots: validation.roots.map(root => root.name) }));
|
|
203
|
-
|
|
206
|
+
// P2-1: the committed snapshot is the recovery authority. Write it BEFORE the journal's
|
|
207
|
+
// committed phase so a crash in this window recovers as committed, never rolled back.
|
|
204
208
|
if (this.snapshots !== undefined)
|
|
205
209
|
await bestEffort(() => this.snapshots.write({ unitId, state: 'committed', ops: eventOps, ...cleanup, createdAt: this.now() }));
|
|
210
|
+
await bestEffort(async () => this.journal?.write({ unitId, phase: 'committed', changed, target: primary, roots: validation.roots.map(root => root.name) }));
|
|
206
211
|
try {
|
|
207
212
|
await this.hooks?.committed?.({ unitId, changed, ops: eventOps, roots: validation.roots.map(root => root.name) });
|
|
208
213
|
}
|
|
@@ -378,48 +383,16 @@ export class MutationUnit {
|
|
|
378
383
|
});
|
|
379
384
|
}
|
|
380
385
|
async applyOp(op, baseline) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return op.content;
|
|
386
|
-
});
|
|
387
|
-
if (!ran)
|
|
388
|
-
throw new Error('internal error: the write transaction did not invoke the task; no write was performed');
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
const next = op.kind === 'delete' ? null : op.content;
|
|
392
|
-
if (this.io.transact !== undefined) {
|
|
393
|
-
let conflict = false;
|
|
394
|
-
let wrote = false;
|
|
395
|
-
await this.io.transact(op.target, current => {
|
|
396
|
-
if (current !== baseline) {
|
|
397
|
-
if (this.transactGuard)
|
|
398
|
-
throw new Error('concurrent modification detected: ' + op.target);
|
|
399
|
-
conflict = true;
|
|
400
|
-
return current;
|
|
401
|
-
}
|
|
402
|
-
wrote = true;
|
|
403
|
-
return next;
|
|
404
|
-
});
|
|
405
|
-
if (conflict && !wrote)
|
|
406
|
-
throw Object.assign(new Error('concurrent modification detected: ' + op.target), { noWrite: true });
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
const current = await this.io.readText(op.target);
|
|
410
|
-
if (current !== baseline)
|
|
411
|
-
throw new Error('concurrent modification detected: ' + op.target);
|
|
412
|
-
if (next === null)
|
|
413
|
-
await this.io.remove(op.target);
|
|
414
|
-
else
|
|
415
|
-
await this.io.writeText(op.target, next);
|
|
386
|
+
const result = await this.kernel.writeOne({ path: op.target, expected: baseline, next: op.kind === 'delete' ? null : op.content, ...op.skill === undefined ? {} : { skill: op.skill } }, { guardBaseline: this.transactGuard });
|
|
387
|
+
if (!result.ok)
|
|
388
|
+
throw Object.assign(new Error(result.errors[0]?.message ?? 'write failed: ' + op.target), { noWrite: true });
|
|
389
|
+
return [...(result.warnings ?? [])];
|
|
416
390
|
}
|
|
417
391
|
async rollback(snapshots) {
|
|
418
392
|
for (const snapshot of [...snapshots].reverse()) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
await this.io.writeText(snapshot.target, snapshot.before);
|
|
393
|
+
const result = await this.kernel.restore({ path: snapshot.target, before: snapshot.before, after: snapshot.after });
|
|
394
|
+
if (!result.ok)
|
|
395
|
+
throw new Error(result.errors[0]?.message ?? 'rollback failed: ' + snapshot.target);
|
|
423
396
|
}
|
|
424
397
|
}
|
|
425
398
|
}
|
package/lib/types/params.js
CHANGED
|
@@ -187,7 +187,7 @@ export const PARAM_EXPOSURE = Object.freeze([
|
|
|
187
187
|
{ id: 'evolution-approval.enabled', group: 'deployment', tier: 'E3', authority: 'cordis', owner: 'evolution-approval', applies: 'live', docAnchor: 'PARAMETERS.md#deployment', summary: 'Master approval switch: run the approval pipeline for foreground writes.', label: '启用批准流水线', hint: '关闭后前台写入不再经批准。', control: 'switch', unit: '', values: '', card: 'evolution-approval' },
|
|
188
188
|
{ id: 'evolution-approval.stageForeground', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-approval', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Stage foreground agent writes for approval too.', why: 'security' },
|
|
189
189
|
{ id: 'evolution-approval.pendingTtlMs', group: 'deployment', tier: 'E3', authority: 'cordis', owner: 'evolution-approval', applies: 'live', docAnchor: 'PARAMETERS.md#deployment', summary: 'How long a staged write may wait before it expires: expired writes are closed as rejected and are NEVER executed; 0 (the default) means the window never expires.', label: '待批过期时间', hint: '一条待批写入最多等多久:超时就按「拒绝」关掉(不会执行,写效果不会落地);0 = 永不过期(默认)。', control: 'number', unit: '毫秒', values: '', card: 'evolution-approval' },
|
|
190
|
-
{ id: 'approvalConfirm', group: 'write-caps', tier: 'E3', authority: 'cordis', owner: 'evolution-approval', applies: 'live', docAnchor: 'PARAMETERS.md#write-caps', summary: 'What the confirmation before an APPROVE does:
|
|
190
|
+
{ id: 'approvalConfirm', group: 'write-caps', tier: 'E3', authority: 'cordis', owner: 'evolution-approval', applies: 'live', docAnchor: 'PARAMETERS.md#write-caps', summary: 'What the confirmation before an APPROVE does: execute immediately, or ask first.', label: '批准前先确认', hint: '点「批准」时要不要再确认一次:直接执行 / 先确认。默认直接执行(不打断批量批准)。', control: 'select', unit: '', values: 'auto|ask', valueLabels: '直接执行|先确认', card: 'evolution-approval' },
|
|
191
191
|
{ id: 'qualityWarnThreshold', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-feedback', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Feedback score below which a skill carries a warning.', why: 'internal' },
|
|
192
192
|
{ id: 'replay.maxPlans', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-replay', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Plans compared in one replay report.', why: 'internal' },
|
|
193
193
|
{ id: 'replay.weights', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-replay', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Scoring weights of the replay comparison.', why: 'internal' },
|
|
@@ -29,7 +29,6 @@ export const PLATFORM_SERVICE_PROBES = Object.freeze([
|
|
|
29
29
|
{ service: 'evolutionPolicy', feature: 'the policy snapshot that shadows row knobs', side: 'host' },
|
|
30
30
|
{ service: 'evolutionReplay', feature: '/evolution replay', side: 'host' },
|
|
31
31
|
{ service: 'evolutionState', feature: 'the state facade behind activity and replay', side: 'host' },
|
|
32
|
-
{ service: 'evolutionStateStore', feature: 'generic durable state store for assignment queue and future collections', side: 'host' },
|
|
33
32
|
{ service: 'llm', feature: 'the curator one-shot review model call', side: 'host' },
|
|
34
33
|
{ service: 'memory', feature: 'the cross-session memory store (review and learning-graph reads)', side: 'host' },
|
|
35
34
|
{ service: 'pluginInventory', feature: 'the plugin inventory view', side: 'host' },
|
|
@@ -40,6 +40,18 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
|
40
40
|
export declare const evolutionActivityProjection: ProjectionDefinition<'evolutionActivity'>;
|
|
41
41
|
/** The `evolutionEvidence` unit (host-only: no client view is registered for it). */
|
|
42
42
|
export declare const evolutionEvidenceProjection: ProjectionDefinition<'evolutionEvidence'>;
|
|
43
|
+
/**
|
|
44
|
+
* Whether one session object can be projected at all.
|
|
45
|
+
*
|
|
46
|
+
* The family's readers also take STRUCTURAL session views — `skill-reads.ts` documents the tool
|
|
47
|
+
* path's bare stub — while the registry folds a session's own log (`snapshotEvents(from, to)`,
|
|
48
|
+
* `eventAt(seq)`, `seq`, `inheritedEventCount`) whenever it has to build or advance a cell. A
|
|
49
|
+
* view without those members has no projection state to read, and asking for one would fold a
|
|
50
|
+
* log the unit cannot index.
|
|
51
|
+
* @param session - the value a caller holds as its session.
|
|
52
|
+
* @returns true when the registry can fold this object.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isProjectableSession(session: unknown): session is Session;
|
|
43
55
|
/**
|
|
44
56
|
* The skill names one session read through a non-failed dispatch, as a fold state.
|
|
45
57
|
*
|
|
@@ -54,6 +66,8 @@ export interface EvolutionReadsState {
|
|
|
54
66
|
readonly key: string;
|
|
55
67
|
readonly skill: string | null;
|
|
56
68
|
readonly failed: boolean;
|
|
69
|
+
readonly file?: string | null | undefined;
|
|
70
|
+
readonly tool?: string | undefined;
|
|
57
71
|
}[];
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { z } from 'zod';
|
|
24
24
|
import { isBookkeepingFrame } from "./evidence.js";
|
|
25
|
-
import { dispatchStepOf, isSkillReadToolName, skillReadNameOf } from "./tool-dispatch.js";
|
|
25
|
+
import { dispatchPathOf, dispatchStepOf, isSkillReadToolName, skillReadNameOf } from "./tool-dispatch.js";
|
|
26
26
|
import { foldTurnEvent, initialTurnFold } from "./signals.js";
|
|
27
27
|
// Bounds are the persisted-row validation bounds (a millisecond timestamp): the clamp guard
|
|
28
28
|
// requires every `z.number()` field to carry one on the same line.
|
|
@@ -77,7 +77,7 @@ export const evolutionEvidenceProjection = {
|
|
|
77
77
|
* @param session - the value a caller holds as its session.
|
|
78
78
|
* @returns true when the registry can fold this object.
|
|
79
79
|
*/
|
|
80
|
-
function isProjectableSession(session) {
|
|
80
|
+
export function isProjectableSession(session) {
|
|
81
81
|
if (session === null || typeof session !== 'object')
|
|
82
82
|
return false;
|
|
83
83
|
const candidate = session;
|
|
@@ -102,6 +102,8 @@ const readsStateSchema = z.object({
|
|
|
102
102
|
key: z.string(),
|
|
103
103
|
skill: z.string().nullable(),
|
|
104
104
|
failed: z.boolean(),
|
|
105
|
+
file: z.string().nullable().optional(),
|
|
106
|
+
tool: z.string().optional(),
|
|
105
107
|
}).strict()),
|
|
106
108
|
}).strict();
|
|
107
109
|
const signalsStateSchema = z.object({
|
|
@@ -172,7 +174,7 @@ export const evolutionReadsProjection = {
|
|
|
172
174
|
if (entry.failed === failed)
|
|
173
175
|
return state;
|
|
174
176
|
const reads = [...state.reads];
|
|
175
|
-
reads[at] = { key: entry.key, skill: entry.skill, failed };
|
|
177
|
+
reads[at] = { key: entry.key, skill: entry.skill, failed, ...(entry.file === undefined ? {} : { file: entry.file }), ...(entry.tool === undefined ? {} : { tool: entry.tool }) };
|
|
176
178
|
return { reads };
|
|
177
179
|
};
|
|
178
180
|
// An event with no name only SETTLES a dispatch: it can flip the failure flag of one already
|
|
@@ -182,6 +184,13 @@ export const evolutionReadsProjection = {
|
|
|
182
184
|
// A replayed open of a tracked dispatch: keep the first entry, still honour its outcome.
|
|
183
185
|
if (at >= 0)
|
|
184
186
|
return settled();
|
|
187
|
+
// A dispatch naming a path is a candidate SUPPORT-FILE read (P2-27). The
|
|
188
|
+
// projection stores the raw path; the gate maps it to a skill later, when it
|
|
189
|
+
// knows the deployment's root and file-tool names.
|
|
190
|
+
const file = dispatchPathOf(step);
|
|
191
|
+
if (file !== null) {
|
|
192
|
+
return { reads: [...state.reads.slice(-511), { key: step.key, skill: null, failed: step.outcome?.ok === false, file, tool: step.name }] };
|
|
193
|
+
}
|
|
185
194
|
// Only a read-capable tool is worth a state entry: the reader answers skill names, and a
|
|
186
195
|
// state that tracked every dispatch would grow with the session.
|
|
187
196
|
if (!isSkillReadToolName(step.name))
|
|
@@ -56,6 +56,25 @@ export interface EvolutionSessionLogView {
|
|
|
56
56
|
* @returns the names read through a non-failed `skill` dispatch, or `undefined` when unreadable.
|
|
57
57
|
*/
|
|
58
58
|
export declare function sessionReadSkillNames(ctx: Context, session: EvolutionSessionLogView | undefined): ReadonlySet<string> | undefined;
|
|
59
|
+
/** The deployment's file-read evidence inputs: its skills root and file-tool names. */
|
|
60
|
+
export interface SupportReadOptions {
|
|
61
|
+
readonly root: string;
|
|
62
|
+
readonly toolNames?: readonly string[] | undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Skill support files ONE session read through a non-failed file tool (P2-27).
|
|
66
|
+
*
|
|
67
|
+
* platform-declaration anchor: `sessionProjections` is declared by
|
|
68
|
+
* `@deepseek-ai/dsh-session-projection` (its registry; capability-probed here).
|
|
69
|
+
* Same two readers as {@link sessionReadSkillNames}: the family projection first,
|
|
70
|
+
* then the structural log view. `undefined` means "no evidence", not "nothing was
|
|
71
|
+
* read" - the write gate then keeps its fail-open posture.
|
|
72
|
+
* @param ctx - a context of the runtime.
|
|
73
|
+
* @param session - the session whose reads are wanted.
|
|
74
|
+
* @param options - the skills root and the configured file-tool names.
|
|
75
|
+
* @returns skill -> set of skill-root-relative support paths.
|
|
76
|
+
*/
|
|
77
|
+
export declare function sessionReadSupportFiles(ctx: Context, session: EvolutionSessionLogView | undefined, options: SupportReadOptions): ReadonlyMap<string, ReadonlySet<string>> | undefined;
|
|
59
78
|
/** Actions whose target must have been read first (create is authorship, so it is exempt). */
|
|
60
79
|
export declare const READ_REQUIRED_ACTIONS: readonly string[];
|
|
61
80
|
/**
|
package/lib/types/skill-reads.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { collectReadSkillNames } from "./tool-dispatch.js";
|
|
2
|
-
import { sessionReadNames } from "./session-projection.js";
|
|
1
|
+
import { collectReadSkillNames, collectReadSupportFiles, DEFAULT_SUPPORT_READ_TOOL_NAMES, supportFileReadFromPath } from "./tool-dispatch.js";
|
|
2
|
+
import { installEvolutionProjections, isProjectableSession, sessionReadNames } from "./session-projection.js";
|
|
3
3
|
/**
|
|
4
4
|
* The skill names ONE session read, or `undefined` when its log is not readable.
|
|
5
5
|
*
|
|
@@ -24,6 +24,46 @@ export function sessionReadSkillNames(ctx, session) {
|
|
|
24
24
|
const events = session.readEvents?.();
|
|
25
25
|
return events === undefined ? undefined : collectReadSkillNames(events);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Skill support files ONE session read through a non-failed file tool (P2-27).
|
|
29
|
+
*
|
|
30
|
+
* platform-declaration anchor: `sessionProjections` is declared by
|
|
31
|
+
* `@deepseek-ai/dsh-session-projection` (its registry; capability-probed here).
|
|
32
|
+
* Same two readers as {@link sessionReadSkillNames}: the family projection first,
|
|
33
|
+
* then the structural log view. `undefined` means "no evidence", not "nothing was
|
|
34
|
+
* read" - the write gate then keeps its fail-open posture.
|
|
35
|
+
* @param ctx - a context of the runtime.
|
|
36
|
+
* @param session - the session whose reads are wanted.
|
|
37
|
+
* @param options - the skills root and the configured file-tool names.
|
|
38
|
+
* @returns skill -> set of skill-root-relative support paths.
|
|
39
|
+
*/
|
|
40
|
+
export function sessionReadSupportFiles(ctx, session, options) {
|
|
41
|
+
if (session === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (isProjectableSession(session)) {
|
|
44
|
+
installEvolutionProjections(ctx);
|
|
45
|
+
const state = ctx.get('sessionProjections')?.stateOf(session, 'evolutionReads');
|
|
46
|
+
if (state !== undefined) {
|
|
47
|
+
const toolNames = options.toolNames ?? DEFAULT_SUPPORT_READ_TOOL_NAMES;
|
|
48
|
+
const reads = new Map();
|
|
49
|
+
for (const entry of state.reads) {
|
|
50
|
+
if (entry.failed || typeof entry.file !== 'string')
|
|
51
|
+
continue;
|
|
52
|
+
if (entry.tool !== undefined && !toolNames.includes(entry.tool))
|
|
53
|
+
continue;
|
|
54
|
+
const hit = supportFileReadFromPath(entry.file, { root: options.root });
|
|
55
|
+
if (hit === null)
|
|
56
|
+
continue;
|
|
57
|
+
const set = reads.get(hit.skill) ?? new Set();
|
|
58
|
+
set.add(hit.rel);
|
|
59
|
+
reads.set(hit.skill, set);
|
|
60
|
+
}
|
|
61
|
+
return reads;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const events = session.readEvents?.();
|
|
65
|
+
return events === undefined ? undefined : collectReadSupportFiles(events, { toolNames: options.toolNames ?? DEFAULT_SUPPORT_READ_TOOL_NAMES, root: options.root });
|
|
66
|
+
}
|
|
27
67
|
/** Actions whose target must have been read first (create is authorship, so it is exempt). */
|
|
28
68
|
export const READ_REQUIRED_ACTIONS = Object.freeze([
|
|
29
69
|
'edit', 'update', 'patch', 'delete', 'write_file', 'remove_file', 'restructure', 'extract',
|
package/lib/types/skill-store.js
CHANGED
|
@@ -50,7 +50,7 @@ import { supportRefs } from "./support-refs.js";
|
|
|
50
50
|
import { selectSections } from "./section-selection.js";
|
|
51
51
|
import { MutationUnit, fileMutationLock } from "./mutation-unit.js";
|
|
52
52
|
import { SkillModel } from "./skill-model.js";
|
|
53
|
-
import { unitJournalPort, unitSnapshotPort } from "./mutation-storage-adapter.js";
|
|
53
|
+
import { pruneTreeChangeUnits, unitJournalPort, unitSnapshotPort } from "./mutation-storage-adapter.js";
|
|
54
54
|
import { projectExtractBodies } from "./extract-plan.js";
|
|
55
55
|
import { diffFindings } from "./skill-lint.js";
|
|
56
56
|
import { contentHash, loadMutations, recordMutation } from "./mutations.js";
|
|
@@ -583,7 +583,9 @@ export class SkillLibrary {
|
|
|
583
583
|
journal: unitJournalPort(this.io, home),
|
|
584
584
|
snapshots: unitSnapshotPort(this.io, home),
|
|
585
585
|
});
|
|
586
|
-
|
|
586
|
+
const results = await unit.recoverPrepared();
|
|
587
|
+
await pruneTreeChangeUnits(this.io, home).catch(() => { });
|
|
588
|
+
return results;
|
|
587
589
|
}
|
|
588
590
|
/**
|
|
589
591
|
* Run one single-file read-modify-write for a mutator. When `transact` was
|
|
@@ -239,6 +239,10 @@ export interface SupportReadHit {
|
|
|
239
239
|
/** Skill-root-relative path, e.g. `references/design-x.md`. */
|
|
240
240
|
rel: string;
|
|
241
241
|
}
|
|
242
|
+
/** The path one dispatch names, for the read-evidence projection. */
|
|
243
|
+
export declare function dispatchPathOf(step: {
|
|
244
|
+
arguments: unknown;
|
|
245
|
+
}): string | null;
|
|
242
246
|
/**
|
|
243
247
|
* Read one dispatch as a SUPPORT-FILE read attributed to a skill: a read of
|
|
244
248
|
* `<root>/<skill>/<support dir>/…` is demand evidence for that one file, the
|
|
@@ -249,6 +253,10 @@ export interface SupportReadHit {
|
|
|
249
253
|
* @returns the skill name and the skill-root-relative path, or `null` when the
|
|
250
254
|
* dispatch is not a support-file read. Pure: no IO, no service lookup.
|
|
251
255
|
*/
|
|
256
|
+
/** Attribute one raw path string to a skill support file (pure; no IO). */
|
|
257
|
+
export declare function supportFileReadFromPath(raw: string, options: {
|
|
258
|
+
root: string;
|
|
259
|
+
}): SupportReadHit | null;
|
|
252
260
|
export declare function supportFileReadOf(signal: ToolDispatchSignal, options: {
|
|
253
261
|
toolNames: readonly string[];
|
|
254
262
|
root: string;
|
|
@@ -273,6 +281,14 @@ export declare function collectReadSkillNames(events: Iterable<{
|
|
|
273
281
|
type: string;
|
|
274
282
|
data?: unknown;
|
|
275
283
|
}>): Set<string>;
|
|
284
|
+
/** Every skill support file this log read through a non-failed configured file tool. */
|
|
285
|
+
export declare function collectReadSupportFiles(events: Iterable<{
|
|
286
|
+
type: string;
|
|
287
|
+
data?: unknown;
|
|
288
|
+
}>, options: {
|
|
289
|
+
toolNames: readonly string[];
|
|
290
|
+
root: string;
|
|
291
|
+
}): Map<string, Set<string>>;
|
|
276
292
|
/**
|
|
277
293
|
* Does this dispatched tool name touch the skill library (read or mutate)?
|
|
278
294
|
* @param name - the dispatched tool name.
|