feat: 旧任务继续会话——终端任务带新消息 + 全部选项同一会话续跑

server:POST /tasks/{id}/continue(终端非 merged 任务重排队,
resume_message 列存本轮消息,不传选项沿用原值,plan/goal 冲突
与新建一致,goal_mode=1 时转新 goal 语义;CAS 防并发改状态,
审计 task_continued);tasks.resume_message 三处迁移 + PG 建表。
worker:认领后本轮 prompt 换成续跑消息(只换内存副本),
goal 续跑走 goal-resume 而不是 goal,跑完清 resume_message
(进程被杀时不清,下次认领重跑同一条消息)。
host:goal-resume --goal-text(旧任务接续 goal 模式:跳过状态
与记录守卫,旧线程上从第 1 轮开新目标循环,新语义不限轮次;
无线程时拒绝)。
sandbox:Local/Docker goal_resume_task + popen goal-resume kind;
附带 popen goal 命令改成新语义(无验收不传 --verify,轮次 0
原样传,此前 goal 任务到本地 worker 会直接报错)。
console:详情页终端非 merged 任务出继续会话表单(消息 +
模型/强度/权限/计划/goal,模型下拉复用中转列表)。
测试:新增 test_task_continue.py 11 个 + test_host_continue.py 2 个。
This commit is contained in:
host committed 2026-10-27 04:40:11 +08:00
1 parent cf293914d9
commit 3a4997d346
9 files changed
+823 -20

No files matched your search

+56 -4
View File
@@ -215,6 +215,30 @@
<input id="forkMsg" type="text" placeholder="分支首轮消息(必填,带历史的新会话)" style="width:100%; box-sizing:border-box; margin:6px 0;">
<div class="bar"><button id="btnForkConfirm" class="primary">创建分支任务</button><span id="forkMsgSpan" class="muted"></span></div>
</div>
<div id="continueForm" style="display:none">
<h3>继续会话(旧任务接回来,同一会话再跑一轮;不传的选项沿用任务原值)</h3>
<textarea id="continueText" placeholder="本轮要说的话(必填)"></textarea>
<div class="bar">
<select id="continueModel"><option value="">模型:沿用任务原值</option></select>
<select id="continueEffort">
<option value="">强度:沿用任务原值</option>
<option value="low">low</option>
<option value="medium">medium</option>
<option value="high">high</option>
<option value="xhigh">xhigh</option>
<option value="max">max</option>
</select>
<select id="continueApproval">
<option value="">权限:沿用任务原值</option>
<option value="ask">请求批准</option>
<option value="auto">帮我批准</option>
<option value="full">完全访问</option>
</select>
<label><input type="checkbox" id="continuePlan"> 计划模式(只读)</label>
<label><input type="checkbox" id="continueGoal"> goal 模式(不限轮次,模型宣告完成)</label>
</div>
<div class="bar"><button id="btnContinueConfirm" class="primary">继续会话</button><span id="continueMsgSpan" class="muted"></span></div>
</div>
<pre id="detailBody" style="display:none"></pre>
<h3>实时链路</h3>
<div id="traceBox" class="muted">点任务行查看详情后,这里显示过程链路</div>
@@ -280,10 +304,12 @@ async function loadRelayModels() {
try {
var d = await api("/relay/models", "GET");
var models = d.models || [];
sel.innerHTML = "<option value=''>模型:默认(跟随配置)</option>" +
models.map(function (m) {
return "<option value='" + esc(m) + "'>" + esc(m) + "</option>";
}).join("");
var opts = models.map(function (m) {
return "<option value='" + esc(m) + "'>" + esc(m) + "</option>";
}).join("");
sel.innerHTML = "<option value=''>模型:默认(跟随配置)</option>" + opts;
var csel = $("#continueModel");
if (csel) csel.innerHTML = "<option value=''>模型:沿用任务原值</option>" + opts;
$("#modelCustom").style.display = "none";
} catch (e) {
$("#modelCustom").style.display = "inline";
@@ -529,6 +555,8 @@ async function showDetail(taskId) {
$("#forkMsgSpan").textContent = "";
$("#followupMsg").textContent = "";
$("#followupForm").style.display = ["queued", "running"].indexOf(t.status) >= 0 ? "block" : "none";
$("#continueMsgSpan").textContent = "";
$("#continueForm").style.display = (TRACE_TERMINAL.indexOf(t.status) >= 0 && t.status !== "merged") ? "block" : "none";
startTrace(taskId, t.status);
loadFollowups(taskId);
} catch (e) { $("#detail").innerHTML = "<span class='err'>加载失败:" + esc(e.message) + "</span>"; }
@@ -723,6 +751,30 @@ $("#btnForkConfirm").onclick = async function () {
loadTasks();
} catch (e) { $("#forkMsgSpan").innerHTML = "<span class='err'>分支失败:" + esc(e.message) + "</span>"; }
};
$("#btnContinueConfirm").onclick = async function () {
if (!currentTask) return;
var msg = $("#continueText").value.trim();
if (!msg) { $("#continueMsgSpan").textContent = "本轮消息必填"; return; }
var body = { message: msg };
var model = $("#continueModel").value;
if (model) body.model = model;
var eff = $("#continueEffort").value;
if (eff) body.effort = eff;
var ap = $("#continueApproval").value;
if (ap) {
if (ap === "full" && !confirm("完全访问 = 自动批准 + 不受工作区限制,agent 可读写工作区外路径。确认继续吗?")) return;
body.approval = ap;
}
if ($("#continuePlan").checked) body.plan = 1;
if ($("#continueGoal").checked) body.goal_mode = 1;
try {
var t = await api("/tasks/" + encodeURIComponent(currentTask.id) + "/continue", "POST", body);
$("#continueMsgSpan").innerHTML = "<span class='ok'>已重新排队,同一会话继续</span>";
$("#continueText").value = "";
showDetail(currentTask.id);
loadTasks();
} catch (e) { $("#continueMsgSpan").innerHTML = "<span class='err'>继续失败:" + esc(e.message) + "</span>"; }
};
$("#btnQueueFollowup").onclick = function () { postFollowup("queued"); };
$("#btnDirectFollowup").onclick = function () { postFollowup("direct"); };
$("#btnCost").onclick = async function () {