vibe-coding-master 0.7.47 → 0.7.48
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/dist/backend/server.js +3 -1
- package/dist/backend/services/architect-restart-service.js +251 -78
- package/dist/backend/services/artifact-service.js +60 -131
- package/dist/backend/services/claude-hook-service.js +3 -0
- package/dist/backend/services/gate-review-service.js +42 -217
- package/dist/backend/services/harness-feedback-service.js +13 -24
- package/dist/backend/services/managed-artifact-validation.js +403 -0
- package/dist/backend/services/message-service.js +13 -64
- package/dist/backend/services/runtime-recovery-service.js +1 -0
- package/dist/backend/services/task-close-service.js +3 -1
- package/dist/backend/templates/harness/gate-review.js +13 -98
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +1 -1
- package/dist/shared/validation/artifact-registry.js +68 -0
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-artifact +1 -34
package/dist/backend/server.js
CHANGED
|
@@ -297,6 +297,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
297
297
|
const architectRestartService = createArchitectRestartService({
|
|
298
298
|
fs,
|
|
299
299
|
taskService,
|
|
300
|
+
projectService,
|
|
300
301
|
sessionService,
|
|
301
302
|
appSettings
|
|
302
303
|
});
|
|
@@ -399,7 +400,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
399
400
|
runtime,
|
|
400
401
|
projectService,
|
|
401
402
|
taskService,
|
|
402
|
-
translationWorkerService
|
|
403
|
+
translationWorkerService,
|
|
404
|
+
architectRestartService
|
|
403
405
|
});
|
|
404
406
|
const claudeHookService = createClaudeHookService({
|
|
405
407
|
projectService,
|
|
@@ -17,55 +17,61 @@ Before performing any assigned work, read:
|
|
|
17
17
|
- the latest Gate Review report when present
|
|
18
18
|
|
|
19
19
|
Treat the current artifacts and worktree as the source of truth. The architecture-plan Gate has accepted the current planning artifacts or VCM recorded an explicit Gate exception. Do not repeat the completed interview or planning work unless a later route explicitly reopens it.`;
|
|
20
|
+
const ARCHITECT_RESTART_STATE_FILE = "architect-restart.json";
|
|
20
21
|
export function createArchitectRestartService(deps) {
|
|
21
22
|
const pendingByTask = new Map();
|
|
23
|
+
const operationsByTask = new Map();
|
|
22
24
|
return {
|
|
23
25
|
async schedule(repoRoot, taskSlug) {
|
|
24
|
-
const session = await requireRunningArchitect(repoRoot, taskSlug);
|
|
25
|
-
await requireCompletePlan(repoRoot, taskSlug);
|
|
26
26
|
const key = taskKey(repoRoot, taskSlug);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await tryRestart(existing);
|
|
27
|
+
return withTaskLock(key, async () => {
|
|
28
|
+
const session = await requireRunningArchitect(repoRoot, taskSlug);
|
|
29
|
+
await requireCompletePlan(repoRoot, taskSlug);
|
|
30
|
+
const existing = pendingByTask.get(key);
|
|
31
|
+
if (existing?.status === "executing") {
|
|
33
32
|
return {
|
|
34
33
|
taskSlug,
|
|
35
|
-
sessionId:
|
|
36
|
-
status: "
|
|
34
|
+
sessionId: existing.replacementSessionId ?? existing.sourceSessionId,
|
|
35
|
+
status: "already_scheduled",
|
|
37
36
|
...(existing.memoryCandidatePath
|
|
38
37
|
? { memoryCandidatePath: existing.memoryCandidatePath }
|
|
39
38
|
: {})
|
|
40
39
|
};
|
|
41
40
|
}
|
|
42
|
-
|
|
41
|
+
if (existing && isSourceSession(existing, session)) {
|
|
42
|
+
if (existing.status === "blocked") {
|
|
43
|
+
existing.status = "pending";
|
|
44
|
+
existing.blocker = undefined;
|
|
45
|
+
existing.sourceSessionId = session.id;
|
|
46
|
+
existing.updatedAt = timestamp();
|
|
47
|
+
await persistPending(existing);
|
|
48
|
+
await tryRestart(existing);
|
|
49
|
+
return scheduleResult(existing, "scheduled");
|
|
50
|
+
}
|
|
51
|
+
return scheduleResult(existing, "already_scheduled");
|
|
52
|
+
}
|
|
53
|
+
const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
|
|
54
|
+
? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
|
|
55
|
+
: undefined;
|
|
56
|
+
const pending = {
|
|
57
|
+
version: 1,
|
|
58
|
+
repoRoot,
|
|
43
59
|
taskSlug,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
sourceSessionId: session.id,
|
|
61
|
+
...(session.claudeSessionId ? { sourceClaudeSessionId: session.claudeSessionId } : {}),
|
|
62
|
+
stopped: false,
|
|
63
|
+
gateAccepted: false,
|
|
64
|
+
status: "pending",
|
|
65
|
+
permissionMode: session.permissionMode,
|
|
66
|
+
model: session.model,
|
|
67
|
+
effort: session.effort,
|
|
68
|
+
memoryCandidatePath: existing?.memoryCandidatePath ?? memoryCandidatePath,
|
|
69
|
+
updatedAt: timestamp()
|
|
49
70
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
: undefined;
|
|
54
|
-
pendingByTask.set(key, {
|
|
55
|
-
repoRoot,
|
|
56
|
-
taskSlug,
|
|
57
|
-
sessionId: session.id,
|
|
58
|
-
stopped: false,
|
|
59
|
-
gateAccepted: false,
|
|
60
|
-
status: "pending",
|
|
61
|
-
memoryCandidatePath
|
|
71
|
+
pendingByTask.set(key, pending);
|
|
72
|
+
await persistPending(pending);
|
|
73
|
+
return scheduleResult(pending, "scheduled");
|
|
62
74
|
});
|
|
63
|
-
return {
|
|
64
|
-
taskSlug,
|
|
65
|
-
sessionId: session.id,
|
|
66
|
-
status: "scheduled",
|
|
67
|
-
...(memoryCandidatePath ? { memoryCandidatePath } : {})
|
|
68
|
-
};
|
|
69
75
|
},
|
|
70
76
|
getState(repoRoot, taskSlug) {
|
|
71
77
|
const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
|
|
@@ -74,7 +80,7 @@ export function createArchitectRestartService(deps) {
|
|
|
74
80
|
}
|
|
75
81
|
return {
|
|
76
82
|
taskSlug: pending.taskSlug,
|
|
77
|
-
sessionId: pending.
|
|
83
|
+
sessionId: pending.replacementSessionId ?? pending.sourceSessionId,
|
|
78
84
|
status: pending.status,
|
|
79
85
|
...(pending.memoryCandidatePath
|
|
80
86
|
? { memoryCandidatePath: pending.memoryCandidatePath }
|
|
@@ -82,46 +88,128 @@ export function createArchitectRestartService(deps) {
|
|
|
82
88
|
...(pending.blocker ? { blocker: { ...pending.blocker } } : {})
|
|
83
89
|
};
|
|
84
90
|
},
|
|
91
|
+
async recoverTask(repoRoot, taskSlug) {
|
|
92
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
93
|
+
await withTaskLock(key, async () => {
|
|
94
|
+
const stored = await loadPending(repoRoot, taskSlug);
|
|
95
|
+
if (!stored) {
|
|
96
|
+
pendingByTask.delete(key);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const pending = { ...stored, repoRoot };
|
|
100
|
+
pendingByTask.set(key, pending);
|
|
101
|
+
const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
|
|
102
|
+
if (pending.status === "executing") {
|
|
103
|
+
if (isConfirmedReplacement(pending, session)) {
|
|
104
|
+
await removePending(pending);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (session?.status === "running"
|
|
108
|
+
&& pending.replacementSessionId === session.id) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await launchReplacement(pending);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (pending.status === "pending" && restartPrerequisitesMet(pending)) {
|
|
115
|
+
if (session?.status === "running") {
|
|
116
|
+
await tryRestart(pending);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
await launchReplacement(pending);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
},
|
|
85
124
|
async recordArchitectStop(repoRoot, taskSlug, sessionId) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
126
|
+
await withTaskLock(key, async () => {
|
|
127
|
+
const pending = pendingByTask.get(key);
|
|
128
|
+
if (!pending || pending.status !== "pending") {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
|
|
132
|
+
if (!session || session.id !== sessionId || !isSourceSession(pending, session)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
pending.sourceSessionId = session.id;
|
|
136
|
+
pending.stopped = true;
|
|
137
|
+
pending.updatedAt = timestamp();
|
|
138
|
+
await persistPending(pending);
|
|
139
|
+
await tryRestart(pending);
|
|
140
|
+
});
|
|
92
141
|
},
|
|
93
142
|
async recordRouteDelivered(repoRoot, taskSlug, message) {
|
|
94
143
|
if (!isArchitectToPm(message)) {
|
|
95
144
|
return;
|
|
96
145
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
147
|
+
await withTaskLock(key, async () => {
|
|
148
|
+
const pending = pendingByTask.get(key);
|
|
149
|
+
if (!pending || pending.status !== "pending") {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
pending.deliveredMessageId = message.id;
|
|
153
|
+
pending.updatedAt = timestamp();
|
|
154
|
+
await persistPending(pending);
|
|
155
|
+
await tryRestart(pending);
|
|
156
|
+
});
|
|
103
157
|
},
|
|
104
158
|
async recordRouteAccepted(repoRoot, taskSlug, message) {
|
|
105
159
|
if (!isArchitectToPm(message)) {
|
|
106
160
|
return;
|
|
107
161
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
162
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
163
|
+
await withTaskLock(key, async () => {
|
|
164
|
+
const pending = pendingByTask.get(key);
|
|
165
|
+
if (!pending || pending.status !== "pending") {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
pending.acceptedMessageId = message.id;
|
|
169
|
+
pending.updatedAt = timestamp();
|
|
170
|
+
await persistPending(pending);
|
|
171
|
+
await tryRestart(pending);
|
|
172
|
+
});
|
|
114
173
|
},
|
|
115
174
|
async recordArchitectureGateDisposition(repoRoot, taskSlug, accepted) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
175
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
176
|
+
await withTaskLock(key, async () => {
|
|
177
|
+
const pending = pendingByTask.get(key);
|
|
178
|
+
if (!pending || pending.status !== "pending") {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
pending.gateAccepted = accepted;
|
|
182
|
+
pending.updatedAt = timestamp();
|
|
183
|
+
await persistPending(pending);
|
|
184
|
+
await tryRestart(pending);
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
async recordReplacementPromptSubmitted(repoRoot, taskSlug, sessionId) {
|
|
188
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
189
|
+
await withTaskLock(key, async () => {
|
|
190
|
+
const pending = pendingByTask.get(key);
|
|
191
|
+
if (!pending || pending.status !== "executing") {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
|
|
195
|
+
if (!session
|
|
196
|
+
|| session.id !== sessionId
|
|
197
|
+
|| !session.claudeSessionId
|
|
198
|
+
|| (pending.replacementSessionId && pending.replacementSessionId !== session.id)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await removePending(pending);
|
|
202
|
+
});
|
|
122
203
|
},
|
|
123
|
-
clear(repoRoot, taskSlug) {
|
|
124
|
-
|
|
204
|
+
async clear(repoRoot, taskSlug) {
|
|
205
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
206
|
+
await withTaskLock(key, async () => {
|
|
207
|
+
const pending = pendingByTask.get(key) ?? await loadPending(repoRoot, taskSlug);
|
|
208
|
+
pendingByTask.delete(key);
|
|
209
|
+
if (pending) {
|
|
210
|
+
await removePending({ ...pending, repoRoot });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
125
213
|
}
|
|
126
214
|
};
|
|
127
215
|
async function requireRunningArchitect(repoRoot, taskSlug) {
|
|
@@ -147,17 +235,12 @@ export function createArchitectRestartService(deps) {
|
|
|
147
235
|
}
|
|
148
236
|
}
|
|
149
237
|
async function tryRestart(pending) {
|
|
150
|
-
if (pending.status !== "pending"
|
|
151
|
-
|| !pending.stopped
|
|
152
|
-
|| !pending.deliveredMessageId
|
|
153
|
-
|| pending.deliveredMessageId !== pending.acceptedMessageId
|
|
154
|
-
|| !pending.gateAccepted) {
|
|
238
|
+
if (pending.status !== "pending" || !restartPrerequisitesMet(pending)) {
|
|
155
239
|
return;
|
|
156
240
|
}
|
|
157
241
|
const session = await deps.sessionService.getRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE);
|
|
158
|
-
if (!session
|
|
159
|
-
|
|
160
|
-
blockPending(pending, new VcmError({
|
|
242
|
+
if (!session || !isSourceSession(pending, session)) {
|
|
243
|
+
await blockPending(pending, new VcmError({
|
|
161
244
|
code: "ARCHITECT_RESTART_SESSION_UNAVAILABLE",
|
|
162
245
|
message: "Architect restart is blocked because the scheduled Architect session no longer exists.",
|
|
163
246
|
statusCode: 409
|
|
@@ -165,7 +248,7 @@ export function createArchitectRestartService(deps) {
|
|
|
165
248
|
return;
|
|
166
249
|
}
|
|
167
250
|
if (session.status !== "running") {
|
|
168
|
-
blockPending(pending, new VcmError({
|
|
251
|
+
await blockPending(pending, new VcmError({
|
|
169
252
|
code: "ARCHITECT_RESTART_SESSION_NOT_RUNNING",
|
|
170
253
|
message: "Architect restart is blocked because the scheduled Architect session is not running.",
|
|
171
254
|
statusCode: 409
|
|
@@ -175,31 +258,121 @@ export function createArchitectRestartService(deps) {
|
|
|
175
258
|
if (session.activityStatus !== "idle") {
|
|
176
259
|
return;
|
|
177
260
|
}
|
|
261
|
+
pending.sourceSessionId = session.id;
|
|
262
|
+
await launchReplacement(pending);
|
|
263
|
+
}
|
|
264
|
+
async function launchReplacement(pending) {
|
|
178
265
|
pending.status = "executing";
|
|
266
|
+
pending.blocker = undefined;
|
|
267
|
+
pending.updatedAt = timestamp();
|
|
268
|
+
await persistPending(pending);
|
|
179
269
|
try {
|
|
180
270
|
await requireCompletePlan(pending.repoRoot, pending.taskSlug);
|
|
181
271
|
await requirePlanningMemoryCandidate(pending);
|
|
182
|
-
await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
|
|
183
|
-
permissionMode:
|
|
184
|
-
model:
|
|
185
|
-
effort:
|
|
272
|
+
const replacement = await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
|
|
273
|
+
permissionMode: pending.permissionMode,
|
|
274
|
+
model: pending.model,
|
|
275
|
+
effort: pending.effort,
|
|
186
276
|
appendSystemPrompt: ARCHITECT_RESTORE_PROMPT
|
|
187
277
|
});
|
|
188
|
-
|
|
278
|
+
pending.replacementSessionId = replacement.id;
|
|
279
|
+
pending.updatedAt = timestamp();
|
|
280
|
+
await persistPending(pending);
|
|
189
281
|
}
|
|
190
282
|
catch (error) {
|
|
191
|
-
blockPending(pending, error);
|
|
283
|
+
await blockPending(pending, error);
|
|
192
284
|
}
|
|
193
285
|
}
|
|
194
|
-
function blockPending(pending, error) {
|
|
286
|
+
async function blockPending(pending, error) {
|
|
195
287
|
const normalized = toVcmError(error);
|
|
196
288
|
pending.status = "blocked";
|
|
197
289
|
pending.blocker = {
|
|
198
290
|
code: normalized.code,
|
|
199
291
|
message: normalized.message,
|
|
200
|
-
blockedAt:
|
|
292
|
+
blockedAt: timestamp()
|
|
293
|
+
};
|
|
294
|
+
pending.updatedAt = timestamp();
|
|
295
|
+
await persistPending(pending);
|
|
296
|
+
}
|
|
297
|
+
function restartPrerequisitesMet(pending) {
|
|
298
|
+
return Boolean(pending.stopped
|
|
299
|
+
&& pending.deliveredMessageId
|
|
300
|
+
&& pending.deliveredMessageId === pending.acceptedMessageId
|
|
301
|
+
&& pending.gateAccepted);
|
|
302
|
+
}
|
|
303
|
+
function isSourceSession(pending, session) {
|
|
304
|
+
return session.id === pending.sourceSessionId
|
|
305
|
+
|| Boolean(pending.sourceClaudeSessionId
|
|
306
|
+
&& session.claudeSessionId
|
|
307
|
+
&& pending.sourceClaudeSessionId === session.claudeSessionId);
|
|
308
|
+
}
|
|
309
|
+
function isConfirmedReplacement(pending, session) {
|
|
310
|
+
return Boolean(session?.claudeSessionId
|
|
311
|
+
&& ((pending.replacementSessionId && session.id === pending.replacementSessionId)
|
|
312
|
+
|| !pending.sourceClaudeSessionId
|
|
313
|
+
|| session.claudeSessionId !== pending.sourceClaudeSessionId));
|
|
314
|
+
}
|
|
315
|
+
function scheduleResult(pending, status) {
|
|
316
|
+
return {
|
|
317
|
+
taskSlug: pending.taskSlug,
|
|
318
|
+
sessionId: pending.replacementSessionId ?? pending.sourceSessionId,
|
|
319
|
+
status,
|
|
320
|
+
...(pending.memoryCandidatePath ? { memoryCandidatePath: pending.memoryCandidatePath } : {})
|
|
201
321
|
};
|
|
202
322
|
}
|
|
323
|
+
async function persistPending(pending) {
|
|
324
|
+
const { repoRoot: _repoRoot, ...stored } = pending;
|
|
325
|
+
await deps.fs.writeJsonAtomic(await statePath(pending.repoRoot, pending.taskSlug), stored);
|
|
326
|
+
pendingByTask.set(taskKey(pending.repoRoot, pending.taskSlug), pending);
|
|
327
|
+
}
|
|
328
|
+
async function loadPending(repoRoot, taskSlug) {
|
|
329
|
+
const target = await statePath(repoRoot, taskSlug);
|
|
330
|
+
if (!(await deps.fs.pathExists(target))) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
const stored = await deps.fs.readJson(target);
|
|
334
|
+
if (stored.version !== 1
|
|
335
|
+
|| stored.taskSlug !== taskSlug
|
|
336
|
+
|| !stored.sourceSessionId
|
|
337
|
+
|| !["pending", "executing", "blocked"].includes(stored.status)) {
|
|
338
|
+
throw new VcmError({
|
|
339
|
+
code: "ARCHITECT_RESTART_STATE_INVALID",
|
|
340
|
+
message: `Architect restart state is invalid for task ${taskSlug}.`,
|
|
341
|
+
statusCode: 500
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return stored;
|
|
345
|
+
}
|
|
346
|
+
async function removePending(pending) {
|
|
347
|
+
pendingByTask.delete(taskKey(pending.repoRoot, pending.taskSlug));
|
|
348
|
+
const target = await statePath(pending.repoRoot, pending.taskSlug);
|
|
349
|
+
if (await deps.fs.pathExists(target)) {
|
|
350
|
+
await deps.fs.removePath?.(target, { force: true });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function statePath(repoRoot, taskSlug) {
|
|
354
|
+
const [config, task] = await Promise.all([
|
|
355
|
+
deps.projectService.loadConfig(repoRoot),
|
|
356
|
+
deps.taskService.loadTask(repoRoot, taskSlug)
|
|
357
|
+
]);
|
|
358
|
+
return resolveRepoPath(getTaskRuntimeRepoRoot(task), path.posix.join(config.stateRoot, ARCHITECT_RESTART_STATE_FILE));
|
|
359
|
+
}
|
|
360
|
+
async function withTaskLock(key, run) {
|
|
361
|
+
const previous = operationsByTask.get(key) ?? Promise.resolve();
|
|
362
|
+
const current = previous.catch(() => undefined).then(run);
|
|
363
|
+
operationsByTask.set(key, current);
|
|
364
|
+
try {
|
|
365
|
+
return await current;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
if (operationsByTask.get(key) === current) {
|
|
369
|
+
operationsByTask.delete(key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function timestamp() {
|
|
374
|
+
return new Date().toISOString();
|
|
375
|
+
}
|
|
203
376
|
async function requirePlanningMemoryCandidate(pending) {
|
|
204
377
|
if (!pending.memoryCandidatePath) {
|
|
205
378
|
return;
|