Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,7 @@ export function PersonalWorkspacePage({
const [quickCompletingTodoIds, setQuickCompletingTodoIds] = useState<ReadonlySet<string>>(() => new Set());
const [refreshState, setRefreshState] = useState<"idle" | "loading" | "done" | "error">("idle");
const [sessionProposalIds, setSessionProposalIds] = useState<string[]>([]);
const [managerChannelProposalIds, setManagerChannelProposalIds] = useState<string[]>([]);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [theme, setTheme] = useState<WorkspaceTheme>(readWorkspaceTheme);
const [goalContexts, setGoalContexts] = useState<Record<string, GoalRepositoryContext>>({});
Expand Down Expand Up @@ -985,8 +986,9 @@ export function PersonalWorkspacePage({
}, [goalMessages, selectedGoal, selectedGoalTab]);
const managerChatItems = useMemo(
() => items.filter((item) => item.kind === "message"
|| (item.kind === "proposal" && sessionProposalIds.includes(item.proposal.previewId))),
[items, sessionProposalIds],
|| (item.kind === "proposal" && (sessionProposalIds.includes(item.proposal.previewId)
|| managerChannelProposalIds.includes(item.proposal.previewId)))),
[items, sessionProposalIds, managerChannelProposalIds],
);
const lastChatItem = managerChatItems[managerChatItems.length - 1];
const latestMessageTextLength = lastChatItem?.kind === "message" ? lastChatItem.message.text.length : 0;
Expand Down Expand Up @@ -1074,14 +1076,20 @@ export function PersonalWorkspacePage({
void listTypedActions(selectedGoalId ? { goalId: selectedGoalId } : { contextKind: "manager" })
.then((stored) => {
if (cancelled) return;
const restored = Object.fromEntries(stored
const restoreable = stored
.filter((proposal) => ["preview_ready", "gated", "deferred", "applying"].includes(proposal.status)
|| (proposal.action_kind === "operation.execute" && proposal.status === "applied"))
.map((proposal) => {
const projected = workspaceProposal(proposal, t);
return [projected.previewId, projected];
}));
.map((proposal) => workspaceProposal(proposal, t));
const restored = Object.fromEntries(restoreable.map((proposal) => [proposal.previewId, proposal]));
setProposals((current) => ({ ...current, ...restored }));
// The manager conversation shows the cards this channel offered: a team
// plan the steward proposed from here is confirmed here, instead of the
// owner hunting for the Goal whose workspace happens to hold the card.
// A Goal-scoped fetch belongs to that Goal's workspace, not to this
// conversation, so it is left alone.
if (!selectedGoalId) {
setManagerChannelProposalIds(restoreable.map((proposal) => proposal.previewId));
}
})
.catch(() => {
// The workspace remains usable when the optional local proposal store is unavailable.
Expand Down
4 changes: 3 additions & 1 deletion docs/architecture/rfcs/harness-selection-dsh-pi-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ condition. Validation produces `applies: false`. Preview is not execution.
| Validation/admission (#4519/#4522/#4532/#4533) | Exact Goal, registered Agents, supported advancement kinds and bounded public-safe fields; channel-scoped Goal lookup; unavailable facts drop the proposal while preserving answer text | `ready` checks registration/action support, not executor health, tool eligibility or budget admission |
| Staffing gaps | Unknown Agent produces `agent_not_registered` and retains `declined_first_todo`; explicit `capability_not_granted` / `audience_not_authorized` gaps admit no work | These reason codes do not prove all capability/audience conditions are automatically detected |
| Materialization (#4524/#4528/#4535/#4538) | Revalidates the named Goal; calls canonical Todo owner per ready lane; records proposal digest and bounded `lane_todo_ids`; existing receipt shape remains readable; no monitor key | Confirmed priority is dropped; acceptance/quota/stop are not execution constraints on this path; no atomic team commit or automatic partial-recovery proof |
| Confirmation (#4547/#4548/#4552) | Existing frontend displays lanes/gaps and submits `team.plan`; bundle and browser fixture shipped | Lark and real worker execution were not qualified by this fixture; all-gap apply currently reports `team_plan_lanes_already_present` with no Todos |
| Confirmation (#4547/#4548/#4552) | Existing frontend displays lanes/gaps and submits `team.plan`; bundle and browser fixture shipped; the manager conversation now lists the card its own channel stored, so an owner confirms where the sentence was typed while a Goal-scoped fetch stays in that Goal's workspace | Lark and real worker execution were not qualified by this fixture; all-gap apply currently reports `team_plan_lanes_already_present` with no Todos |
| Freshness | Registry byte changes make the Chat preview stale; optional `intent_basis` reads alignment source facts before materialization | No exact Goal-intent/authorization/work precondition at commit; `intent_basis` is neither the full intent revision nor a CAS fence |

Unchanged-plan retries are covered by focused tests. Do not generalize those
Expand All @@ -541,6 +541,8 @@ Latest integration checkpoint: #4569 (`f1166e81e`) keeps unsupported action kind

Subsequent #4572 (`0aa6179de`) appends a channel-authored confirmation-location pointer to manager answers, naming the Goal workspace. Local and remote manager audiences receive it; Goal channels are not annotated. A remote pointer does not create a Lark card or prove card persistence/team execution; model prose is preserved.

The manager conversation that produced a plan now also lists the card its channel stored, instead of only the Goal workspace the pointer names. This is a presentation change over the same validated proposal: it adds no Lark card, creates nothing before confirmation, and a proposal fetched for a selected Goal stays in that Goal's workspace because it belongs to that context.

### Relationship to multi-agent and shared authority

The [alignment RFC](shared-goal-alignment-and-governed-amendment-v0.md) owns
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ journal 与配额语义;B 作为上游接口出现时的低成本替代;只
| 校验/准入(#4519/#4522/#4532/#4533) | 确切 Goal、注册 Agent、支持的 advancement kind、有界公开安全字段;按通道范围查询 Goal;缺少事实时丢弃提案并保留答案正文 | `ready` 只校验注册/action 支持,不证明执行器健康、工具资格或预算准入 |
| Staffing gap | 未注册 Agent 产生 `agent_not_registered` 并保留 `declined_first_todo`;显式 `capability_not_granted` / `audience_not_authorized` gap 不允许工作 | 存在这些 reason code 不证明全部 capability/audience 条件已经自动检测 |
| 物化(#4524/#4528/#4535/#4538) | 重新校验点名 Goal,逐 ready lane 调 canonical Todo owner;回执保存 proposal digest 与有界 `lane_todo_ids`,兼容旧回执且没有 monitor key | 确认的 priority 丢失;acceptance/quota/stop 未成为此路径的执行约束;没有整队原子提交或自动 partial recovery 证明 |
| 确认(#4547/#4548/#4552) | 现有前端展示 lanes/gaps 并提交 `team.plan`,bundle 与 browser fixture 已交付 | 此 fixture 未验收 Lark 或真实 worker 执行;all-gap apply 仍以零 Todo 返回 `team_plan_lanes_already_present` |
| 确认(#4547/#4548/#4552) | 现有前端展示 lanes/gaps 并提交 `team.plan`,bundle 与 browser fixture 已交付;产生计划的**管家会话本身也会列出该通道存入的卡片**,业主在说出这句话的地方即可确认,而为已选 Goal 拉取的提案仍留在该 Goal 工作区 | 此 fixture 未验收 Lark 或真实 worker 执行;all-gap apply 仍以零 Todo 返回 `team_plan_lanes_already_present` |
| 新鲜度 | registry bytes 变化会使 Chat preview stale;可选 `intent_basis` 在物化前读取 alignment source facts | commit 未绑定精确 Goal intent/授权/工作前置条件;`intent_basis` 不是完整意图修订或 CAS fence |

现有测试覆盖未变化计划的重复提交,不能推广到并发计划、多 lane 中断或中途 Todo 被编辑的情况。R1 通过实际 action/recovery 路径补这些资格。
Expand All @@ -419,6 +419,8 @@ journal 与配额语义;B 作为上游接口出现时的低成本替代;只

后续 #4572(`0aa6179de`)在 manager 回答中追加 channel-authored 的确认位置提示,指出目标 Goal 工作区;本地与远端 manager 均收到提示,Goal channel 不追加。远端提示不等于在 Lark 生成卡片,也不证明卡片写入或团队执行成功;原模型正文保留。

产生计划的管家会话现在也会列出它自己通道存入的那张卡片,而不只是提示里点名的 Goal 工作区。这是同一份已校验提案上的展示改动:不新增 Lark 卡片,确认前不创建任何东西,而为已选 Goal 拉取的提案仍留在该 Goal 工作区,因为它属于那个上下文。

### 与 multi-agent / shared authority 契约的关系

[对齐 RFC](shared-goal-alignment-and-governed-amendment-v0.zh-CN.md) 拥有共享意图及受治理修订;[共享权威 RFC](shared-goal-authority-state-provider-v0.zh-CN.md) 拥有持久化/晋升。团队计划只提议已接受 intent 内的工作,不能靠 envelope 散文修改权限、共享验收或终止条件。Stage 3 amendment commit 仍未交付。回执可选 `intent_basis` 是既有 `source_basis_digest`,不是尚未实现的完整 intent envelope 版本,不能通过改名赋予历史回执更强语义。
Expand Down
70 changes: 69 additions & 1 deletion examples/personal-workspace-browser/team-plan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { openWorkspacePage } from "./scenario-context.mjs";

const GOAL_ID = "product-release";
const PROPOSAL_ID = "proposal-team-plan-fixture";
const MANAGER_PROPOSAL_ID = "proposal-team-plan-manager-fixture";
// The manager-channel card is deliberately a different plan from the Goal-scoped
// one, so a row in the manager conversation cannot be the Goal's card leaking in.
const MANAGER_PROPOSAL_TITLE = "为 product-release 配出 3 条 lane 的团队";
const READY_TODO = "Implement the bounded intake";
const GAP_TODO = "Independently review the intake";

Expand Down Expand Up @@ -75,6 +79,46 @@ function teamPlanProposal() {
};
}

/**
* The same plan as the manager channel stored it.
*
* A plan the steward offers from the manager conversation is stored by that
* channel, and its card has to be confirmable in the conversation that produced
* it -- not only under the Goal whose workspace it is scoped to.
*/
function managerTeamPlanProposal() {
const plan = teamPlanProposal();
const parameters = plan.normalized_parameters;
return {
...plan,
proposal_id: MANAGER_PROPOSAL_ID,
summary: MANAGER_PROPOSAL_TITLE,
context: { kind: "manager", goal_id: GOAL_ID },
normalized_parameters: {
...parameters,
plan: {
...parameters.plan,
objective: "Ship the manager-channel intake",
lanes: [
...parameters.plan.lanes,
{
lane_id: "lane_manager",
agent_id: "agent-manager",
acceptance: "the manager lane reports its receipt",
staffing: "ready",
first_todo: {
text: "Review the steward handoff",
priority: "P2",
task_class: "advancement_task",
action_kind: "validate",
},
},
],
},
},
};
}

export const teamPlanScenario = {
id: "team-plan",
async run({ browser, collectCoverage, url }) {
Expand All @@ -85,7 +129,7 @@ export const teamPlanScenario = {
else failures.push(message);
};
const context = await openWorkspacePage(browser, url, {
apiOptions: { initialActionProposals: [teamPlanProposal()] },
apiOptions: { initialActionProposals: [teamPlanProposal(), managerTeamPlanProposal()] },
collectCoverage,
});
try {
Expand Down Expand Up @@ -168,6 +212,30 @@ export const teamPlanScenario = {
fullPage: false,
animations: "disabled",
});

// The manager conversation offers the card its own channel produced, so a
// plan asked for there is confirmable there instead of only under the Goal
// it staffs. The Goal-scoped card stays in that Goal's workspace.
await page.locator(".personal-manager-link").first().click();
await page.locator(".personal-goal-tabs button", { hasText: "Chat" }).click();
const managerCard = page.locator(".personal-proposal-row", { hasText: MANAGER_PROPOSAL_TITLE });
try {
await managerCard.waitFor({ state: "visible", timeout: 15_000 });
} catch (error) {
throw new Error(
`${error.message}; manager rows=${JSON.stringify(await page.locator(".personal-proposal-row").allInnerTexts())};`
+ ` body=${(await page.locator("body").innerText()).slice(0, 1200)}`,
);
}
check(
(await managerCard.innerText()).includes("team.plan"),
"the manager conversation offers the team plan card it produced",
);
await page.screenshot({
path: resolve(outputDir, "team-plan-manager-conversation.png"),
fullPage: false,
animations: "disabled",
});
// The harness collects both uncaught page errors and console errors; a
// dev-server resource status is not a client-side exception, so only the
// former is a failure here.
Expand Down
4 changes: 2 additions & 2 deletions loopx/web/chat/asset-retention.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2",
"assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2",
"assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2",
"assets/index-Cb7S1yHW.js",
"assets/index-CXvjZarX.js",
"assets/index-DWmuPX72.css"
],
[
Expand All @@ -28,7 +28,7 @@
"assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2",
"assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2",
"assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2",
"assets/index-D8O4R6-3.js",
"assets/index-Cb7S1yHW.js",
"assets/index-DWmuPX72.css"
]
]
Expand Down
129 changes: 129 additions & 0 deletions loopx/web/chat/assets/index-CXvjZarX.js

Large diffs are not rendered by default.

129 changes: 0 additions & 129 deletions loopx/web/chat/assets/index-D8O4R6-3.js

This file was deleted.

2 changes: 1 addition & 1 deletion loopx/web/chat/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
content="LoopX 个人 Agent 工作区:在同一个频道里查看、纠偏并推进 Goal。"
/>
<title>LoopX 个人 Agent 工作区</title>
<script type="module" crossorigin src="/chat/assets/index-Cb7S1yHW.js"></script>
<script type="module" crossorigin src="/chat/assets/index-CXvjZarX.js"></script>
<link rel="stylesheet" crossorigin href="/chat/assets/index-DWmuPX72.css">
</head>
<body>
Expand Down
Loading