diff --git a/integrations/dsh/docs/environment.md b/integrations/dsh/docs/environment.md
index da1e6a32..861764ff 100644
--- a/integrations/dsh/docs/environment.md
+++ b/integrations/dsh/docs/environment.md
@@ -71,8 +71,9 @@ The installation is a Host-owned job:
- Progress is polled by job id, so a second installation for another interpreter cannot replace what you see.
- **Cancel** stops pip and its build children, and the interpreter stays locked until the process has actually
exited.
-- The log is written to `$DSH_HOME/reme-memory/install-.log` and its tail is shown in place; install and
- service logs are rotated to `.log.1` once they pass 5 MB, and only the newest 20 install logs are kept.
+- The log is written to `$DSH_HOME/reme-memory/install-.log` and its tail is shown in place. Each install
+ opens a new file and only the newest 20 are kept; the long-lived service log rotates to `service.log.1` once it
+ passes 5 MB.
- When the plugin unloads, running jobs are stopped.
## Workspace `.env`
diff --git a/integrations/dsh/docs/environment_ZH.md b/integrations/dsh/docs/environment_ZH.md
index 9f09c11e..153c7d5c 100644
--- a/integrations/dsh/docs/environment_ZH.md
+++ b/integrations/dsh/docs/environment_ZH.md
@@ -61,8 +61,8 @@ ReMe 需要 **Python 3.11 或更新版本**。标签页在 Host 上探测下列
- 关闭配置页后仍在运行;重新打开标签页会为同一个解释器恢复进度。
- 进度按任务 ID 轮询,为另一个解释器发起的安装不会顶替当前显示。
- **取消**会同时终止 pip 及其构建子进程,且在进程真正退出前该解释器保持占用。
-- 日志写入 `$DSH_HOME/reme-memory/install-.log`,页面直接显示尾部;安装与服务日志超过 5 MB 会轮转为 `.log.1`,
- 并且目录只保留最近 20 个安装日志。
+- 日志写入 `$DSH_HOME/reme-memory/install-.log`,页面直接显示尾部。每次安装都会新建文件,目录只保留最近
+ 20 个;长期运行的服务日志超过 5 MB 后轮转为 `service.log.1`。
- 插件卸载时,仍在运行的安装会被停止。
## Workspace `.env`
diff --git a/integrations/dsh/src/environment/client/locales.ts b/integrations/dsh/src/environment/client/locales.ts
index 72733783..f5e2b9cc 100644
--- a/integrations/dsh/src/environment/client/locales.ts
+++ b/integrations/dsh/src/environment/client/locales.ts
@@ -118,6 +118,8 @@ export const envEn = {
"The previous service could not be stopped; check the log.",
startErrorUnverifiedRecord:
"The recorded service identity could not be verified, so it was left untouched; stop it manually, then retry.",
+ startErrorUnverifiedRecordPid:
+ "The recorded service identity could not be verified (PID {pid}), so it was left untouched; stop it manually, then retry.",
startErrorNoPython: "Select a Python interpreter first.",
startErrorNotInstalled:
"reme-ai is not installed in the selected interpreter.",
@@ -301,6 +303,8 @@ export const envZh: typeof envEn = {
startErrorPreviousNotStopped: "上一次服务未能停止,请查看日志。",
startErrorUnverifiedRecord:
"无法验证记录中的进程身份,已保持其运行;请手动停止后重试。",
+ startErrorUnverifiedRecordPid:
+ "无法验证记录中的进程身份(PID {pid}),已保持其运行;请手动停止后重试。",
startErrorNoPython: "请先选择一个 Python 解释器。",
startErrorNotInstalled: "所选解释器尚未安装 reme-ai。",
startErrorVersionTooOld: "已安装的 reme-ai 版本过低,请先升级。",
diff --git a/integrations/dsh/src/environment/client/python.tsx b/integrations/dsh/src/environment/client/python.tsx
index a140329e..cd6cfd9d 100644
--- a/integrations/dsh/src/environment/client/python.tsx
+++ b/integrations/dsh/src/environment/client/python.tsx
@@ -178,7 +178,7 @@ export function PythonSection({
{detection.error ? `: ${detection.error}` : ""}
{t("emptyTitle")}
diff --git a/integrations/dsh/src/environment/client/service.tsx b/integrations/dsh/src/environment/client/service.tsx
index ecf8275a..f99080a9 100644
--- a/integrations/dsh/src/environment/client/service.tsx
+++ b/integrations/dsh/src/environment/client/service.tsx
@@ -38,6 +38,10 @@ const START_ERROR_KEYS: Record = {
};
function startErrorText(t: EnvTranslator, result: ServiceStartResult): string {
+ // The unverified record carries the pid it refused to touch, so the message
+ // can tell the user what to look for instead of a generic sentence.
+ if (result.code === "unverified-record" && result.pid !== undefined)
+ return fill(t("startErrorUnverifiedRecordPid"), { pid: result.pid });
const key = result.code ? START_ERROR_KEYS[result.code] : undefined;
return key ? t(key) : result.error || t("serviceError");
}
@@ -221,7 +225,11 @@ export function useService(
return;
}
} catch {
- // Fall back to the reported start error.
+ // A switch of endpoint aborts this call; its error belongs to the
+ // old endpoint and must not be painted (or bump the sequence) on
+ // the new card.
+ if (controller.signal.aborted) return;
+ // Otherwise fall back to the reported start error.
}
}
applyState({ phase: "error", error: startErrorText(t, started) });
@@ -446,6 +454,12 @@ export function ServiceSection({
}): JSX.Element {
const { state, stopping, stopError, log } = control;
const [confirming, setConfirming] = useState(false);
+ // The confirmation belongs to one phase and endpoint: keeping it open across
+ // a transition would re-show "Stop?" on top of a live service later.
+ useEffect(() => {
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ setConfirming(false);
+ }, [endpoint, state.phase]);
// A conflict can still be our own unhealthy service (booting, wrong token);
// offer the same Stop affordance instead of leaving the tab without a way
// out other than editing the record by hand. `stopped`/`starting` with a
@@ -463,6 +477,11 @@ export function ServiceSection({
state.data?.managed === true ||
((state.phase === "stopped" || state.phase === "starting") &&
state.data?.pid !== undefined);
+ // A stale record may be a recycled pid that now belongs to a foreign
+ // process. Stop then refuses, so Start must stay available: the Host stops a
+ // provably owned previous launch, discards a foreign record, and reports an
+ // unverifiable one. Hiding Start would leave that card without a way out.
+ const canRestart = state.phase === "stopped" && state.data?.pid !== undefined;
return (
@@ -584,7 +603,8 @@ export function ServiceSection({
{t("stopService")}
)
- ) : (
+ ) : null}
+ {!canStop || canRestart ? (
- )}
+ ) : null}