@elvisw/dsh-magic-context 0.0.0-bootstrap.0 → 0.0.0-stage
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 +2 -452
- package/package.json +4 -108
- package/LICENSE +0 -23
- package/NOTICE +0 -93
- package/README.zh-CN.md +0 -368
- package/cordis.patch.yml +0 -20
- package/lib/client.js +0 -2270
- package/lib/core/compartments.js +0 -424
- package/lib/core/corpus-store.js +0 -176
- package/lib/core/directive-detect.js +0 -147
- package/lib/core/domain-reconcile.js +0 -394
- package/lib/core/domains.js +0 -127
- package/lib/core/embedding-local.js +0 -247
- package/lib/core/global-autostage.js +0 -223
- package/lib/core/global-domains.js +0 -302
- package/lib/core/journal.js +0 -248
- package/lib/core/llm-deadline.js +0 -53
- package/lib/core/machine-domain.js +0 -106
- package/lib/core/maintenance.js +0 -380
- package/lib/core/memory-bridge.js +0 -818
- package/lib/core/memory-categories.js +0 -84
- package/lib/core/memory-scoring.js +0 -65
- package/lib/core/memory-search.js +0 -414
- package/lib/core/memory-write.js +0 -1176
- package/lib/core/policy.js +0 -111
- package/lib/core/profile-confirmation.js +0 -55
- package/lib/core/profile-review.js +0 -375
- package/lib/core/profile-write.js +0 -283
- package/lib/core/reference-seeds.js +0 -63
- package/lib/core/route-health.js +0 -106
- package/lib/core/stores.js +0 -1021
- package/lib/core/tagger.js +0 -7
- package/lib/core/types.js +0 -15
- package/lib/core/vector-backfill.js +0 -140
- package/lib/core/vector-lane.js +0 -184
- package/lib/dsh/command-surface.js +0 -376
- package/lib/dsh/compaction-engine.js +0 -231
- package/lib/dsh/config-switches.js +0 -56
- package/lib/dsh/config.js +0 -13
- package/lib/dsh/context-pressure.js +0 -36
- package/lib/dsh/drop-landing.js +0 -42
- package/lib/dsh/emergency.js +0 -89
- package/lib/dsh/flush-command.js +0 -148
- package/lib/dsh/global-review.js +0 -412
- package/lib/dsh/historian.js +0 -907
- package/lib/dsh/live-journal.js +0 -81
- package/lib/dsh/maintenance-wiring.js +0 -229
- package/lib/dsh/management-pass.js +0 -564
- package/lib/dsh/memory-lane.js +0 -535
- package/lib/dsh/pre-step.js +0 -675
- package/lib/dsh/project-drift.js +0 -116
- package/lib/dsh/recomp.js +0 -192
- package/lib/dsh/reduce-guidance.js +0 -188
- package/lib/dsh/seams.js +0 -588
- package/lib/dsh/search-hint.js +0 -186
- package/lib/dsh/session-notice.js +0 -100
- package/lib/dsh/status-command.js +0 -75
- package/lib/dsh/telemetry.js +0 -349
- package/lib/dsh/tool-bridge.js +0 -1009
- package/lib/dsh/volatile.js +0 -89
- package/lib/index.js +0 -931
- package/tools/migrate-profile-package-name.mjs +0 -317
- package/vendor/onnx/LICENSE +0 -27
- package/vendor/onnx/ort-wasm-simd-threaded.mjs +0 -59
- package/vendor/onnx/ort-wasm-simd-threaded.wasm +0 -0
- package/vendor/onnx/ort.node.min.mjs +0 -8
package/lib/dsh/global-review.js
DELETED
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* L1① second leg — human approval for global-domain candidates (2026-10-12).
|
|
3
|
-
*
|
|
4
|
-
* The staging leg (`global-autostage.ts`) may fill the machine domain's
|
|
5
|
-
* pending pool on its own, and `promote` feeds org candidates by hand; this
|
|
6
|
-
* module is the PUBLISHING leg's UI: one `ctx.userQuestions.ask()` card per
|
|
7
|
-
* pending candidate, answered in the operator's own session. Publishing stays
|
|
8
|
-
* a human act — the card IS the human act, and every decision is journalled
|
|
9
|
-
* with the answer verbatim. Both global domains flow through the same loop:
|
|
10
|
-
* the queue is domain-agnostic, the card names the domain.
|
|
11
|
-
*
|
|
12
|
-
* Why agent-mediated: the shipped Web answerer routes only requests that
|
|
13
|
-
* carry a live root Agent, and `ask()` authenticates that identity — a
|
|
14
|
-
* maintenance tick runs outside any turn and would resolve NO_PROVIDER. The
|
|
15
|
-
* trigger is therefore `ctx_memory action=review_global` inside a turn, so
|
|
16
|
-
* the questions and answers land in the session log like every model-visible
|
|
17
|
-
* exchange.
|
|
18
|
-
*
|
|
19
|
-
* Fail-closed everywhere: no asker wired (headless, service absent) refuses
|
|
20
|
-
* before touching state; a gate refusal is reported without showing a card
|
|
21
|
-
* and leaves the candidate pending; an aborted, malformed or unrecognised
|
|
22
|
-
* answer leaves it pending too. The approval gate runs BEFORE the card so the
|
|
23
|
-
* user is never asked to approve something no gate would ever publish.
|
|
24
|
-
*/
|
|
25
|
-
import { approvalGate, publishGlobalCandidate } from "../core/global-domains.js";
|
|
26
|
-
import { normalizedMemoryHash } from "../core/memory-write.js";
|
|
27
|
-
/** Revision rounds per candidate before the loop gives up and skips (operator typo loop guard). */
|
|
28
|
-
const MAX_REVISION_ROUNDS = 5;
|
|
29
|
-
/** Questions per modal, chained within one call — the host pager has no jump-to, so bigger cards mean blind flips and fatigue approvals (user decision 2026-10-13). */
|
|
30
|
-
const QUESTIONS_PER_CARD = 5;
|
|
31
|
-
const OPTION_APPROVE = '批准发布';
|
|
32
|
-
const OPTION_SKIP = '跳过';
|
|
33
|
-
const OPTION_REJECT = '拒绝';
|
|
34
|
-
/** Reclassification options: the card names the OTHER global domain explicitly. */
|
|
35
|
-
const OPTION_REROUTE_MACHINE = '发布到全局域';
|
|
36
|
-
const OPTION_REROUTE_ORG = '发布到工作域';
|
|
37
|
-
/**
|
|
38
|
-
* Display names follow the 2026-10-25 naming decision (docs/domain-naming-
|
|
39
|
-
* work-global-2026-10-25.md): the domain axis is AUDIENCE, not distribution —
|
|
40
|
-
* `machine` renders to every project on this machine (全局域), `org` to a
|
|
41
|
-
* hand-curated set of work projects (工作域). The internal tokens
|
|
42
|
-
* ('org'/'machine', dir:global:*) are storage keys and stay untouched.
|
|
43
|
-
*/
|
|
44
|
-
function domainLabel(domainId) {
|
|
45
|
-
if (domainId === 'dir:global:machine')
|
|
46
|
-
return '全局域';
|
|
47
|
-
if (domainId === 'dir:global:org')
|
|
48
|
-
return '工作域';
|
|
49
|
-
return domainId;
|
|
50
|
-
}
|
|
51
|
-
/** Extract every candidate verdict from a raw `ask()` reply, keyed by candidate id. */
|
|
52
|
-
function parseAnswers(answer) {
|
|
53
|
-
const byId = new Map();
|
|
54
|
-
if (answer === null || typeof answer !== 'object')
|
|
55
|
-
return byId;
|
|
56
|
-
const answers = answer.answers;
|
|
57
|
-
if (!Array.isArray(answers))
|
|
58
|
-
return byId;
|
|
59
|
-
for (const item of answers) {
|
|
60
|
-
if (item === null || typeof item !== 'object')
|
|
61
|
-
continue;
|
|
62
|
-
const record = item;
|
|
63
|
-
const match = /^global-candidate-(\d+)$/.exec(typeof record.id === 'string' ? record.id : '');
|
|
64
|
-
if (match === null)
|
|
65
|
-
continue;
|
|
66
|
-
const selected = Array.isArray(record.selected)
|
|
67
|
-
? record.selected.filter((entry) => typeof entry === 'string')
|
|
68
|
-
: [];
|
|
69
|
-
const custom = typeof record.custom === 'string' && record.custom.trim() !== '' ? record.custom.trim() : undefined;
|
|
70
|
-
byId.set(Number(match[1]), { label: selected[0] ?? custom ?? '', ...(custom === undefined ? {} : { custom }) });
|
|
71
|
-
}
|
|
72
|
-
return byId;
|
|
73
|
-
}
|
|
74
|
-
export async function reviewGlobalCandidates(input) {
|
|
75
|
-
const pendingAll = input.store.listGlobalCandidates({ state: 'pending' });
|
|
76
|
-
const pending = input.onlyCandidateId === undefined
|
|
77
|
-
? pendingAll
|
|
78
|
-
: pendingAll.filter(candidate => candidate.id === input.onlyCandidateId);
|
|
79
|
-
if (input.ask === undefined) {
|
|
80
|
-
return {
|
|
81
|
-
approved: 0,
|
|
82
|
-
rejected: 0,
|
|
83
|
-
reclassified: 0,
|
|
84
|
-
skipped: 0,
|
|
85
|
-
failed: 0,
|
|
86
|
-
lines: ['人工审批未接线(headless 或宿主缺 userQuestions 服务),待审队列原样保留。'],
|
|
87
|
-
remaining: pending.length,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
if (pending.length === 0) {
|
|
91
|
-
if (input.onlyCandidateId !== undefined) {
|
|
92
|
-
return {
|
|
93
|
-
approved: 0,
|
|
94
|
-
rejected: 0,
|
|
95
|
-
reclassified: 0,
|
|
96
|
-
skipped: 0,
|
|
97
|
-
failed: 1,
|
|
98
|
-
lines: [`候选 #${String(input.onlyCandidateId)} 已不在待审池(可能刚被处理过),本次未弹卡。`],
|
|
99
|
-
remaining: pendingAll.length,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
return { approved: 0, rejected: 0, reclassified: 0, skipped: 0, failed: 0, lines: ['全局域待审队列为空。'], remaining: 0 };
|
|
103
|
-
}
|
|
104
|
-
// Default covers the whole staging batch in ONE card (the host modal pages
|
|
105
|
-
// through questions; auto-stage caps the pool at 50 per tick) — the old 5
|
|
106
|
-
// split a 13-candidate promotion across three modals (user feedback
|
|
107
|
-
// 2026-10-13).
|
|
108
|
-
const batch = pending.slice(0, Math.max(1, input.batchLimit ?? 50));
|
|
109
|
-
// Local const so the gate precheck's narrowing survives the awaits between
|
|
110
|
-
// here and the dispatch loop (TS drops property narrowing across calls).
|
|
111
|
-
const publisher = input.publisher;
|
|
112
|
-
const lines = [];
|
|
113
|
-
let approved = 0;
|
|
114
|
-
let rejected = 0;
|
|
115
|
-
let reclassified = 0;
|
|
116
|
-
let skipped = 0;
|
|
117
|
-
let failed = 0;
|
|
118
|
-
// Gate BEFORE the card: the user must never be asked to approve something
|
|
119
|
-
// no gate would publish. A refusal here is a report line, state untouched.
|
|
120
|
-
// Each entry carries its writer: the precheck proved the publisher live,
|
|
121
|
-
// and the const narrowing does not survive the awaits between here and
|
|
122
|
-
// the dispatch loop.
|
|
123
|
-
const cardable = [];
|
|
124
|
-
for (const candidate of batch) {
|
|
125
|
-
const gate = approvalGate(input.store, candidate, publisher);
|
|
126
|
-
if (!gate.ok) {
|
|
127
|
-
failed += 1;
|
|
128
|
-
lines.push(`#${String(candidate.id)} ✗ ${gate.error}`);
|
|
129
|
-
input.journal?.info('global-review', `candidate #${String(candidate.id)} gate-refused`, {
|
|
130
|
-
phase: 'global-review',
|
|
131
|
-
candidateId: candidate.id,
|
|
132
|
-
domainId: candidate.domainId,
|
|
133
|
-
decision: 'gate-refused',
|
|
134
|
-
reason: gate.error,
|
|
135
|
-
}, input.sessionId);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (publisher === undefined) {
|
|
139
|
-
failed += 1;
|
|
140
|
-
lines.push(`#${String(candidate.id)} ✗ 共享库当前不可写(无发布器),候选保留待审`);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
cardable.push({ candidate, writer: publisher });
|
|
144
|
-
}
|
|
145
|
-
// ONE card for the whole batch: the host modal pages through multiple
|
|
146
|
-
// questions natively, so N pending candidates must not surface as N
|
|
147
|
-
// stacked modals (user feedback 2026-10-13). Answers come back keyed by
|
|
148
|
-
// question id; card copy differs by LANE, and ALL metadata lives in the
|
|
149
|
-
// `question`: the host renders `detail` flush-left while header/question
|
|
150
|
-
// are indented, so any field line in `detail` reads as a stray island
|
|
151
|
-
// (user feedback 2026-10-12). `detail` therefore carries ONLY the memory
|
|
152
|
-
// body. An instant promotion (onlyCandidateId) is the operator's OWN
|
|
153
|
-
// just-requested act — the header already names the action, so the
|
|
154
|
-
// question line itself is dropped (empty string: the host validates
|
|
155
|
-
// shape, not content) rather than parroting what the operator just typed;
|
|
156
|
-
// a batch card keeps a neutral question carrying the staging statistics.
|
|
157
|
-
const instant = input.onlyCandidateId !== undefined;
|
|
158
|
-
const buildQuestion = (candidate, bodyText, revised) => {
|
|
159
|
-
const domainName = domainLabel(candidate.domainId);
|
|
160
|
-
const question = instant
|
|
161
|
-
? ''
|
|
162
|
-
: `发布这条自动暂存的 [${candidate.category ?? '未分类'}] 记忆到${domainName}?` +
|
|
163
|
-
`(${String(candidate.distinctIdentities)} 个项目独立观察 / ${String(candidate.observationCount)} 次)`;
|
|
164
|
-
const llmNote = instant || candidate.suggestedDomain === undefined
|
|
165
|
-
? ''
|
|
166
|
-
: '\n>\n> LLM 建议域:' + domainLabel(candidate.suggestedDomain === 'machine' ? 'dir:global:machine' : 'dir:global:org') +
|
|
167
|
-
(candidate.suggestionReason === undefined ? '' : `(${candidate.suggestionReason})`);
|
|
168
|
-
// Reclassification precheck: the OTHER enabled global domain, and only if
|
|
169
|
-
// this candidate's category is on its allowlist — the card must never
|
|
170
|
-
// offer a reroute the gate would refuse (same principle as gating before
|
|
171
|
-
// the card).
|
|
172
|
-
const otherDomain = input.store.listDomains()
|
|
173
|
-
.find(domain => domain.enabled && domain.domainId !== candidate.domainId
|
|
174
|
-
&& (domain.kind === 'machine' || domain.kind === 'org'));
|
|
175
|
-
const rerouteAllowed = otherDomain !== undefined && candidate.category !== undefined
|
|
176
|
-
&& otherDomain.shareCategories.includes(candidate.category);
|
|
177
|
-
const rerouteOption = otherDomain === undefined ? undefined
|
|
178
|
-
: otherDomain.kind === 'org' ? OPTION_REROUTE_ORG : OPTION_REROUTE_MACHINE;
|
|
179
|
-
const options = [
|
|
180
|
-
{
|
|
181
|
-
label: OPTION_APPROVE,
|
|
182
|
-
description: instant
|
|
183
|
-
? `写入${domainName},该域所有成员项目可见`
|
|
184
|
-
: '写入共享域,所有成员项目可见(source_type=user)',
|
|
185
|
-
},
|
|
186
|
-
...(rerouteAllowed && rerouteOption !== undefined
|
|
187
|
-
? [{
|
|
188
|
-
label: rerouteOption,
|
|
189
|
-
description: `改判到${domainLabel(otherDomain.domainId)}并发布(原候选退役,journal 记 reclassified)`,
|
|
190
|
-
}]
|
|
191
|
-
: []),
|
|
192
|
-
...instant ? [] : [{ label: OPTION_SKIP, description: '留在待审池,下次再决定' }],
|
|
193
|
-
{ label: OPTION_REJECT, description: instant ? '终局拒绝,不再进入待审' : '标记 rejected,不再进入待审' },
|
|
194
|
-
];
|
|
195
|
-
// The host renders `detail` nearly flush-left (margin 0 2px) while the
|
|
196
|
-
// question sits at 24px — bare body text reads as misaligned (user
|
|
197
|
-
// feedback, two rounds). detail goes through MarkdownText, whose
|
|
198
|
-
// blockquote style indents 16px with a rule: wrapping the body in a
|
|
199
|
-
// quote block lines it up under the question and reads as quoted
|
|
200
|
-
// review content.
|
|
201
|
-
const quoted = bodyText.split('\n').map(line => `> ${line}`).join('\n');
|
|
202
|
-
const revisedTag = revised ? '\n>\n> ✎ 已按你的意见修改,请确认' : '';
|
|
203
|
-
return {
|
|
204
|
-
id: `global-candidate-${String(candidate.id)}`,
|
|
205
|
-
question,
|
|
206
|
-
detail: `${quoted}${revisedTag}${llmNote}`,
|
|
207
|
-
header: `记忆发布审批 · ${domainName}`,
|
|
208
|
-
options,
|
|
209
|
-
};
|
|
210
|
-
};
|
|
211
|
-
// One card per QUESTIONS_PER_CARD candidates, CHAINED within this call:
|
|
212
|
-
// the caller keeps hitting Submit until the queue is drained — no repeated
|
|
213
|
-
// `review_global` invocations. Cancelling a card settles only its own
|
|
214
|
-
// slice; the rest of the queue stays pending and the chain stops (a
|
|
215
|
-
// dismissed modal must not summon the next one — that reads as nagging).
|
|
216
|
-
const queue = [...cardable];
|
|
217
|
-
while (queue.length > 0) {
|
|
218
|
-
const slice = queue.splice(0, QUESTIONS_PER_CARD);
|
|
219
|
-
const verdicts = new Map();
|
|
220
|
-
try {
|
|
221
|
-
const answer = await input.ask({
|
|
222
|
-
sessionId: input.sessionId,
|
|
223
|
-
questions: slice.map(entry => buildQuestion(entry.candidate, entry.candidate.content, false)),
|
|
224
|
-
});
|
|
225
|
-
for (const [candidateId, parsed] of parseAnswers(answer))
|
|
226
|
-
verdicts.set(candidateId, parsed);
|
|
227
|
-
}
|
|
228
|
-
catch (error) {
|
|
229
|
-
// Only the raised card's slice counts as failed; the un-raised rest
|
|
230
|
-
// was never asked anything — it just stays pending.
|
|
231
|
-
failed += slice.length;
|
|
232
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
233
|
-
const rest = queue.length;
|
|
234
|
-
lines.push(`✗ 审批卡未能送达(${String(slice.length)} 条保留待审)${rest > 0 ? `,剩余 ${String(rest)} 条本次不再弹出` : ''}:${message}`);
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
for (const { candidate, writer } of slice) {
|
|
238
|
-
let verdict = verdicts.get(candidate.id);
|
|
239
|
-
if (verdict === undefined) {
|
|
240
|
-
failed += 1;
|
|
241
|
-
lines.push(`#${String(candidate.id)} ✗ 答案缺失或无法解析,候选保留待审`);
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
// A typed answer with NO option selected is a REVISION OPINION (user
|
|
245
|
-
// decision 2026-10-12): the typed text becomes the new body and the card
|
|
246
|
-
// is re-raised — for THIS candidate alone, as a single-question card —
|
|
247
|
-
// looping until an option is chosen or MAX_REVISION_ROUNDS is hit (skip,
|
|
248
|
-
// stay pending). Approve and reject stay pure: an option click settles
|
|
249
|
-
// the candidate on the body currently shown.
|
|
250
|
-
let body = candidate.content;
|
|
251
|
-
let revised = false;
|
|
252
|
-
let settled = true;
|
|
253
|
-
for (let round = 1; verdict.custom !== undefined; round += 1) {
|
|
254
|
-
if (round >= MAX_REVISION_ROUNDS) {
|
|
255
|
-
skipped += 1;
|
|
256
|
-
lines.push(`#${String(candidate.id)} → 修改轮次达上限(${String(MAX_REVISION_ROUNDS)}),跳过留池`);
|
|
257
|
-
settled = false;
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
body = verdict.custom;
|
|
261
|
-
revised = true;
|
|
262
|
-
input.journal?.info('global-review', `candidate #${String(candidate.id)} revised`, {
|
|
263
|
-
phase: 'global-review',
|
|
264
|
-
candidateId: candidate.id,
|
|
265
|
-
domainId: candidate.domainId,
|
|
266
|
-
decision: 'revised',
|
|
267
|
-
round,
|
|
268
|
-
note: verdict.custom,
|
|
269
|
-
}, input.sessionId);
|
|
270
|
-
let answer;
|
|
271
|
-
try {
|
|
272
|
-
answer = await input.ask({
|
|
273
|
-
sessionId: input.sessionId,
|
|
274
|
-
questions: [buildQuestion(candidate, body, true)],
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
catch (error) {
|
|
278
|
-
failed += 1;
|
|
279
|
-
lines.push(`#${String(candidate.id)} ✗ 修订卡未能送达,候选保留待审:${error instanceof Error ? error.message : String(error)}`);
|
|
280
|
-
settled = false;
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
const next = parseAnswers(answer).get(candidate.id);
|
|
284
|
-
if (next === undefined) {
|
|
285
|
-
failed += 1;
|
|
286
|
-
lines.push(`#${String(candidate.id)} ✗ 修订答案无法解析,候选保留待审`);
|
|
287
|
-
settled = false;
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
verdict = next;
|
|
291
|
-
}
|
|
292
|
-
if (!settled)
|
|
293
|
-
continue;
|
|
294
|
-
// Reclassification precheck for the dispatch below (the card builder
|
|
295
|
-
// computes the same fact for the option list).
|
|
296
|
-
const otherDomain = input.store.listDomains()
|
|
297
|
-
.find(domain => domain.enabled && domain.domainId !== candidate.domainId
|
|
298
|
-
&& (domain.kind === 'machine' || domain.kind === 'org'));
|
|
299
|
-
const rerouteOption = otherDomain === undefined ? undefined
|
|
300
|
-
: otherDomain.kind === 'org' ? OPTION_REROUTE_ORG : OPTION_REROUTE_MACHINE;
|
|
301
|
-
const journalDecision = (decision, extra) => {
|
|
302
|
-
input.journal?.info('global-review', `candidate #${String(candidate.id)} ${decision}`, {
|
|
303
|
-
phase: 'global-review',
|
|
304
|
-
candidateId: candidate.id,
|
|
305
|
-
domainId: candidate.domainId,
|
|
306
|
-
decision,
|
|
307
|
-
answer: verdict.label,
|
|
308
|
-
...(revised ? { revisedBody: body } : {}),
|
|
309
|
-
...extra,
|
|
310
|
-
}, input.sessionId);
|
|
311
|
-
};
|
|
312
|
-
if ((rerouteOption !== undefined && verdict.label === rerouteOption)
|
|
313
|
-
|| ((verdict.label === OPTION_REROUTE_ORG || verdict.label === OPTION_REROUTE_MACHINE)
|
|
314
|
-
&& rerouteOption !== undefined)) {
|
|
315
|
-
// Human overrule of the staging domain. The target row is upserted in
|
|
316
|
-
// the other domain (idempotent per domain+hash), gated, published; the
|
|
317
|
-
// ORIGINAL candidate retires as rejected — journal carries the verdict.
|
|
318
|
-
const targetDomainId = otherDomain?.domainId;
|
|
319
|
-
if (targetDomainId === undefined || publisher === undefined) {
|
|
320
|
-
failed += 1;
|
|
321
|
-
lines.push(`#${String(candidate.id)} ✗ 改判目标域不可用,候选保留待审`);
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
const rerouted = input.store.upsertGlobalCandidate({
|
|
325
|
-
domainId: targetDomainId,
|
|
326
|
-
content: revised ? body : candidate.content,
|
|
327
|
-
normalizedHash: revised ? normalizedMemoryHash(body) : candidate.normalizedHash,
|
|
328
|
-
firstIdentity: candidate.firstIdentity,
|
|
329
|
-
...(candidate.sessionId === undefined ? {} : { sessionId: candidate.sessionId }),
|
|
330
|
-
...(candidate.compartmentStart === undefined ? {} : { compartmentStart: candidate.compartmentStart }),
|
|
331
|
-
...(candidate.compartmentEnd === undefined ? {} : { compartmentEnd: candidate.compartmentEnd }),
|
|
332
|
-
category: candidate.category,
|
|
333
|
-
sourceMemoryId: candidate.sourceMemoryId,
|
|
334
|
-
suggestedDomain: candidate.suggestedDomain,
|
|
335
|
-
...(candidate.suggestionReason === undefined ? {} : { suggestionReason: candidate.suggestionReason }),
|
|
336
|
-
}, input.now());
|
|
337
|
-
if (rerouted.state === 'approved') {
|
|
338
|
-
// The content already lives in the target domain — retire the original
|
|
339
|
-
// as a duplicate instead of publishing twice.
|
|
340
|
-
input.store.setGlobalCandidateState(candidate.id, 'rejected', input.now());
|
|
341
|
-
reclassified += 1;
|
|
342
|
-
lines.push(`#${String(candidate.id)} ⤴ 目标域已有同内容行(memory 已发布),原候选退役`);
|
|
343
|
-
journalDecision('reclassified-duplicate', { toDomain: targetDomainId, reroutedCandidateId: rerouted.id });
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
const rerouteGate = approvalGate(input.store, rerouted, writer);
|
|
347
|
-
if (!rerouteGate.ok) {
|
|
348
|
-
failed += 1;
|
|
349
|
-
lines.push(`#${String(candidate.id)} ✗ 改判被目标域 gate 拒绝:${rerouteGate.error}`);
|
|
350
|
-
journalDecision('reroute-refused', { toDomain: targetDomainId, reason: rerouteGate.error });
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
const published = publishGlobalCandidate(input.store, {
|
|
354
|
-
candidateId: rerouted.id,
|
|
355
|
-
writer,
|
|
356
|
-
now: input.now(),
|
|
357
|
-
});
|
|
358
|
-
if (!published.ok) {
|
|
359
|
-
failed += 1;
|
|
360
|
-
lines.push(`#${String(candidate.id)} ✗ 改判发布失败:${published.error}`);
|
|
361
|
-
journalDecision('reroute-publish-failed', { toDomain: targetDomainId, reason: published.error });
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
input.store.setGlobalCandidateState(candidate.id, 'rejected', input.now());
|
|
365
|
-
reclassified += 1;
|
|
366
|
-
lines.push(`#${String(candidate.id)} ⤴ 改判发布到 ${domainLabel(targetDomainId)}`
|
|
367
|
-
+ `(memory #${String(published.memoryId)}),原候选退役`);
|
|
368
|
-
journalDecision('reclassified', {
|
|
369
|
-
toDomain: targetDomainId,
|
|
370
|
-
reroutedCandidateId: rerouted.id,
|
|
371
|
-
memoryId: published.memoryId,
|
|
372
|
-
});
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (verdict.label === OPTION_APPROVE || verdict.label.startsWith('批准')) {
|
|
376
|
-
const published = publishGlobalCandidate(input.store, {
|
|
377
|
-
candidateId: candidate.id,
|
|
378
|
-
writer,
|
|
379
|
-
now: input.now(),
|
|
380
|
-
...(revised ? { contentOverride: body } : {}),
|
|
381
|
-
});
|
|
382
|
-
if (!published.ok) {
|
|
383
|
-
failed += 1;
|
|
384
|
-
lines.push(`#${String(candidate.id)} ✗ 发布失败:${published.error}`);
|
|
385
|
-
journalDecision('publish-failed', { reason: published.error });
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
approved += 1;
|
|
389
|
-
lines.push(`#${String(candidate.id)} ✓ 已发布为 ${published.identity} [${published.category}]`
|
|
390
|
-
+ `(memory #${String(published.memoryId)}${published.created ? '' : published.revived === true ? ',复活已归档行' : ',已存在'})`
|
|
391
|
-
+ (revised ? ',按修改稿发布' : ''));
|
|
392
|
-
journalDecision('approved', { memoryId: published.memoryId });
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
if (verdict.label === OPTION_REJECT || verdict.label.startsWith('拒绝')) {
|
|
396
|
-
input.store.setGlobalCandidateState(candidate.id, 'rejected', input.now());
|
|
397
|
-
rejected += 1;
|
|
398
|
-
lines.push(`#${String(candidate.id)} ✗ 已拒绝`);
|
|
399
|
-
journalDecision('rejected');
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
skipped += 1;
|
|
403
|
-
lines.push(`#${String(candidate.id)} → 跳过,留在待审池`);
|
|
404
|
-
journalDecision('skipped');
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
const remaining = input.store.listGlobalCandidates({ state: 'pending' }).length;
|
|
408
|
-
if (pending.length > batch.length) {
|
|
409
|
-
lines.push(`(本次调用上限 ${String(batch.length)} 条已处理完,剩余 ${String(remaining)} 条待审,再次调用继续)`);
|
|
410
|
-
}
|
|
411
|
-
return { approved, rejected, reclassified, skipped, failed, lines, remaining };
|
|
412
|
-
}
|