Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4638a1c
feat(lark): add approved goal channel payload delivery
cocolord Sep 12, 2026
2344852
fix(lark): include next action in report notice
cocolord Sep 12, 2026
d5908c7
fix(lark): bind report retries to rendered message
cocolord Sep 12, 2026
b7a42cc
Merge remote-tracking branch 'origin/main' into codex/capability-goal…
cocolord Sep 12, 2026
b698a24
fix(lark): serialize goal channel delivery authority
cocolord Sep 13, 2026
91b24e2
Merge remote-tracking branch origin/main into codex/capability-goal-c…
cocolord Sep 13, 2026
196be1a
Merge remote-tracking branch origin/main into codex/capability-goal-c…
cocolord Sep 13, 2026
2f3e9e4
Merge remote-tracking branch origin/main into codex/capability-goal-c…
cocolord Sep 13, 2026
c8645ef
Merge remote-tracking branch 'origin/main' into codex/capability-goal…
cocolord Sep 13, 2026
7daa48e
Merge remote-tracking branch 'origin/main' into codex/capability-goal…
cocolord Sep 13, 2026
d136948
Merge remote-tracking branch 'origin/main' into codex/pr-4275-card-de…
huangruiteng Sep 13, 2026
ba380c0
Merge remote-tracking branch 'origin/main' into codex/pr-4275-card-de…
huangruiteng Sep 13, 2026
533ed46
feat(lark): add confirmed operation card flow
huangruiteng Sep 13, 2026
77b6548
fix(dashboard): retain operation delivery receipts
huangruiteng Sep 13, 2026
355d2eb
chore(dashboard): normalize retained asset order
huangruiteng Sep 13, 2026
59a90ff
docs(lark): clarify periodic route authority
huangruiteng Sep 13, 2026
868fa91
refactor(chat): split typed action normalization
huangruiteng Sep 13, 2026
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
22 changes: 22 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,29 @@ export const typedActionKindSchema = z.enum([
"monitor.update",
"gate.resolve",
"run.correct",
"operation.execute",
]);

const typedOperationEnvelopeSchema = z.object({
schema_version: z.literal("loopx_operation_envelope_v0"),
lifecycle_state: z.enum([
"prepared",
"awaiting_confirmation",
"claimed",
"outcome_observed",
]),
operation_id: z.string().min(1),
confirmation_digest: z.string().min(1),
payload_digest: z.string().min(1),
projection_digest: z.string().min(1),
expires_at: z.string().min(1),
delivery: z.record(z.string(), z.unknown()).nullable(),
confirmation: z.record(z.string(), z.unknown()).nullable(),
claim: z.record(z.string(), z.unknown()).nullable(),
outcome: z.record(z.string(), z.unknown()).nullable(),
result_delivery: z.record(z.string(), z.unknown()).nullable().optional(),
}).passthrough();

export const typedActionProposalSchema = z.object({
schema_version: z.literal("loopx_chat_action_proposal_v1"),
proposal_id: z.string().min(1),
Expand All @@ -315,6 +336,7 @@ export const typedActionProposalSchema = z.object({
error: z.record(z.string(), z.unknown()).nullable().optional(),
checkpoint: z.record(z.string(), z.unknown()).nullable().optional(),
regenerated_from: z.string().nullable().optional(),
operation: typedOperationEnvelopeSchema.nullable().optional(),
created_at: z.string(),
updated_at: z.string(),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function compileActionReviewPlan(proposal: TypedActionProposal): ActionRe
if ((lifecycle && proposal.gate != null) || proposal.status === "gated") return held("gated", "authority_gate");
if ((lifecycle && proposal.stale != null) || proposal.status === "stale") return held("refresh", "stale_proposal");
if (proposal.status === "applied") return proposal.receipt?.projection_verified === true
&& (proposal.action_kind !== "operation.execute" || proposal.operation?.result_delivery != null)
? held("completed", "readback_verified") : held("repair", "readback_unverified");
if (proposal.status === "applying") return held("pending", "apply_pending");
if (proposal.status === "failed" || proposal.error != null) return held("repair", "apply_failed");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function ChannelTimeline({
<button className={`personal-proposal-row is-${item.proposal.status}`} key={item.id} onClick={() => onSelect({ item: item.proposal, kind: "proposal" })} type="button">
<span><Sparkles size={17} /></span>
<span><small>{item.proposal.actionKind} · {item.proposal.status}</small><strong>{item.proposal.title}</strong><p>{item.proposal.impact}</p></span>
<b>{item.proposal.status === "gated" ? t("timeline.review") : item.proposal.primaryLabel ?? t("timeline.reviewAndConfirm")}</b>
<b>{item.proposal.status === "gated" && item.proposal.actionKind !== "operation.execute" ? t("timeline.review") : item.proposal.primaryLabel ?? t("timeline.reviewAndConfirm")}</b>
</button>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -963,17 +963,21 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<small>{selection.item.actionKind} · {selection.item.status}</small>
<h3>{selection.item.title}</h3>
<p>{selection.item.impact}</p>
{selection.item.reviewPlan ? <p className="personal-proposal-explainer" data-action-review={selection.item.reviewPlan.interaction}>{t(`actionReview.${selection.item.reviewPlan.reason}`)}</p> : null}
{selection.item.reviewPlan ? <p className="personal-proposal-explainer" data-action-review={selection.item.reviewPlan.interaction}>{selection.item.actionKind === "operation.execute" && selection.item.status === "gated"
? t("actionReview.operation_group_confirmation")
: selection.item.actionKind === "operation.execute" && selection.item.reviewPlan.reason === "readback_unverified"
? t("actionReview.operation_result_delivery_pending")
: t(`actionReview.${selection.item.reviewPlan.reason}`)}</p> : null}
{selection.item.status === "ready" ? <p className="personal-proposal-explainer">{t("drawer.proposalExplainer")}</p> : null}
<dl>{selection.item.fields.map((field) => <div key={field.key}><dt>{field.label}</dt><dd>{field.value}</dd></div>)}</dl>
</section>
{selection.item.status === "applied" ? <p className="personal-proposal-state is-applied"><Check size={16} />{t("drawer.proposalApplied")}</p> : null}
{selection.item.status === "applied" && selection.item.goalId ? <button className="personal-primary-action" onClick={() => { const goalId = selection.item.goalId!; onClose(); void callbacks.onOpenGoal?.(goalId); }} type="button"><ExternalLink size={16} />{selection.item.actionKind === "goal.create" ? t("drawer.proposalEnterGoal") : t("drawer.proposalViewGoal")}</button> : null}
{selection.item.status === "applied" ? <p className={`personal-proposal-state ${selection.item.actionKind === "operation.execute" && selection.item.reviewPlan?.reason === "readback_unverified" ? "is-gated" : "is-applied"}`}><Check size={16} />{selection.item.actionKind === "operation.execute" ? selection.item.primaryLabel : t("drawer.proposalApplied")}</p> : null}
{selection.item.status === "applied" && selection.item.actionKind !== "operation.execute" && selection.item.goalId ? <button className="personal-primary-action" onClick={() => { const goalId = selection.item.goalId!; onClose(); void callbacks.onOpenGoal?.(goalId); }} type="button"><ExternalLink size={16} />{selection.item.actionKind === "goal.create" ? t("drawer.proposalEnterGoal") : t("drawer.proposalViewGoal")}</button> : null}
{selection.item.status === "stale" ? <p className="personal-proposal-state is-stale">{t("drawer.proposalStale")}</p> : null}
{selection.item.status === "error" ? <div className="personal-proposal-state is-error"><span>{selection.item.reviewPlan?.reason === "readback_unverified" ? t("actionReview.readback_unverified") : t("drawer.proposalApplyFailed")}</span>{selection.item.errorMessage ? <small>{selection.item.errorMessage}</small> : null}<small>{t("drawer.proposalApplyFailedHint")}</small></div> : null}
{selection.item.status === "rejected" ? <p className="personal-proposal-state is-error">{t("drawer.proposalRejected")}</p> : null}
{selection.item.status === "deferred" ? <p className="personal-proposal-state is-gated">{t("drawer.proposalDeferred")}</p> : null}
{selection.item.status === "gated" ? <div className="personal-proposal-state is-gated"><span><strong>{t("drawer.gateRequiresHost")}</strong>{t("drawer.gateRequiresHostDescription")}</span>{selection.item.gate?.nextAction ? <small>{selection.item.gate.nextAction}</small> : null}</div> : null}
{selection.item.status === "gated" ? <div className="personal-proposal-state is-gated"><span><strong>{selection.item.actionKind === "operation.execute" ? selection.item.primaryLabel : t("drawer.gateRequiresHost")}</strong>{selection.item.actionKind === "operation.execute" ? selection.item.impact : t("drawer.gateRequiresHostDescription")}</span>{selection.item.gate?.nextAction ? <small>{selection.item.gate.nextAction}</small> : null}</div> : null}
{selection.item.status === "gated" && selection.item.actionKind === "gate.resolve" ? (() => {
const fieldValue = (key: string) => selection.item.fields.find((field) => field.key === key)?.value;
const gateGoalId = fieldValue("goal_id");
Expand All @@ -988,9 +992,9 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
);
})() : null}
{!readOnly && selection.item.workspaceCandidates?.length ? <div className="personal-workspace-candidates" aria-label={t("drawer.workspaceCandidates")}>{selection.item.workspaceCandidates.map((candidate) => <button key={candidate.workspaceRef} onClick={() => void callbacks.onSelectWorkspaceCandidate?.(selection.item, candidate.workspaceRef)} type="button"><strong>{candidate.label}</strong><small>{candidate.workspaceRef}</small></button>)}</div> : null}
{!readOnly && selection.item.status === "error" ? <button className="personal-primary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "regenerate")} type="button"><RotateCcw size={17} />{t("drawer.proposalRegenerate")}</button> : !readOnly && selection.item.status !== "gated" ? <button className="personal-primary-action" disabled={!['ready', 'deferred'].includes(selection.item.status) || selection.item.reviewPlan?.canApply === false} onClick={() => void callbacks.onApplyProposal?.(selection.item)} type="button"><Check size={17} />{selection.item.status === "applying" ? t("drawer.applying") : selection.item.primaryLabel ?? t("drawer.apply")}</button> : null}
{!readOnly && (["stale", "gated", "rejected"].includes(selection.item.status) || (selection.item.status === "ready" && selection.item.reviewPlan?.canApply === false)) ? <button className="personal-secondary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "regenerate")} type="button"><RotateCcw size={16} />{t("drawer.proposalRecheck")}</button> : null}
{!readOnly && ["ready", "gated"].includes(selection.item.status) ? <div className="personal-drawer-action-grid"><button className="personal-secondary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "defer")} type="button">{t("drawer.proposalDefer")}</button><button className="personal-secondary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "reject")} type="button">{t("drawer.decisionReject")}</button></div> : null}
{!readOnly && selection.item.actionKind !== "operation.execute" && selection.item.status === "error" ? <button className="personal-primary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "regenerate")} type="button"><RotateCcw size={17} />{t("drawer.proposalRegenerate")}</button> : !readOnly && selection.item.actionKind !== "operation.execute" && selection.item.status !== "gated" ? <button className="personal-primary-action" disabled={!['ready', 'deferred'].includes(selection.item.status) || selection.item.reviewPlan?.canApply === false} onClick={() => void callbacks.onApplyProposal?.(selection.item)} type="button"><Check size={17} />{selection.item.status === "applying" ? t("drawer.applying") : selection.item.primaryLabel ?? t("drawer.apply")}</button> : null}
{!readOnly && selection.item.actionKind !== "operation.execute" && (["stale", "gated", "rejected"].includes(selection.item.status) || (selection.item.status === "ready" && selection.item.reviewPlan?.canApply === false)) ? <button className="personal-secondary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "regenerate")} type="button"><RotateCcw size={16} />{t("drawer.proposalRecheck")}</button> : null}
{!readOnly && selection.item.actionKind !== "operation.execute" && ["ready", "gated"].includes(selection.item.status) ? <div className="personal-drawer-action-grid"><button className="personal-secondary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "defer")} type="button">{t("drawer.proposalDefer")}</button><button className="personal-secondary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "reject")} type="button">{t("drawer.decisionReject")}</button></div> : null}
{!["applied", "applying"].includes(selection.item.status) ? <button className="personal-secondary-action" onClick={onClose} type="button">{t("drawer.proposalClose")}</button> : null}
</>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,10 @@ const en = {
"proposal.field.initialTodos": "Initial tasks",
"proposal.field.objective": "Objective",
"proposal.field.operation": "Operation",
"proposal.field.operationState": "Operation state",
"proposal.field.resultDelivery": "Result delivery",
"proposal.field.confirmationBoundary": "Confirmation boundary",
"proposal.field.expiresAt": "Expires at",
"proposal.field.permission": "Permission",
"proposal.field.reason": "Reason",
"proposal.field.stopCondition": "Stop condition",
Expand All @@ -648,6 +652,8 @@ const en = {
"actionReview.apply_pending": "Execution is in progress. Wait for its result before retrying.",
"actionReview.readback_verified": "The action completed and its resulting state was verified.",
"actionReview.readback_unverified": "The action returned without verified readback. Completion is not confirmed; recheck the state.",
"actionReview.operation_group_confirmation": "This exact request can only be confirmed on its original card in the bound Feishu group. The Dashboard does not expose a local execution control.",
"actionReview.operation_result_delivery_pending": "The operation outcome was recorded, but the original group result card has not passed readback verification yet.",
"actionReview.apply_failed": "Execution did not complete. Check the failure and regenerate the preview before retrying.",
"actionReview.inactive_proposal": "This proposal is no longer ready to execute. Recheck it before continuing.",
"proposal.gate.default": "Host confirmation required",
Expand All @@ -657,12 +663,18 @@ const en = {
"proposal.impact.lifecycleResume": "After confirmation, the Goal becomes eligible for automatic scheduling and returns to Active Goals. Actual execution still follows quota, Gate, and Todo constraints.",
"proposal.impact.lifecycleStop": "After confirmation, automatic progress stops and the Goal moves to the collapsed Stopped list. History, Todos, and evidence remain available for resuming.",
"proposal.impact.protected": "This action must be completed through the protected LoopX write service.",
"proposal.impact.operation": "The exact terms are read-only here. Confirm or reject the same immutable request in the bound Feishu group; confirmation consumes one canonical claim.",
"proposal.primary.apply": "Confirm and apply",
"proposal.primary.goalCreate": "Create Goal and start first run",
"proposal.primary.lifecycleDelete": "Delete Goal",
"proposal.primary.lifecycleResume": "Resume Goal",
"proposal.primary.lifecycleStop": "Stop Goal",
"proposal.primary.todoStart": "Create task and start execution",
"proposal.primary.operationGroup": "Confirm in Feishu group",
"proposal.primary.operationResultPending": "Result card delivery pending",
"proposal.primary.operationResultVerified": "Verified result",
"proposal.resultDelivery.verified": "Verified in the original group card",
"proposal.resultDelivery.pending": "Pending verified return to the original group card",
"proposal.summary.goalCreate": "Create Goal: {title}",
"proposal.summary.heartbeat": "Set a Heartbeat for the current Goal",
"proposal.summary.lifecycleDelete": "Delete Goal: {title}",
Expand Down Expand Up @@ -1601,6 +1613,10 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"proposal.field.initialTodos": "首个任务",
"proposal.field.objective": "目标",
"proposal.field.operation": "操作",
"proposal.field.operationState": "操作状态",
"proposal.field.resultDelivery": "结果回传",
"proposal.field.confirmationBoundary": "确认边界",
"proposal.field.expiresAt": "过期时间",
"proposal.field.permission": "权限",
"proposal.field.reason": "原因",
"proposal.field.stopCondition": "停止条件",
Expand All @@ -1622,6 +1638,8 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"actionReview.apply_pending": "正在执行,请等待读回结果后再重试。",
"actionReview.readback_verified": "操作已完成,结果状态已通过读回验证。",
"actionReview.readback_unverified": "操作返回但未通过读回验证。尚不能确认完成,请重新检查状态。",
"actionReview.operation_group_confirmation": "这份精确请求只能在已绑定飞书群的原始卡片确认;Dashboard 不提供本地执行入口。",
"actionReview.operation_result_delivery_pending": "操作结果已经记录,但原群结果卡尚未通过回读核验。",
"actionReview.apply_failed": "执行未完成。请检查失败原因并重新生成预览后再试。",
"actionReview.inactive_proposal": "此提案当前不可执行。请重新检查后再继续。",
"proposal.gate.default": "需要宿主确认",
Expand All @@ -1631,12 +1649,18 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"proposal.impact.lifecycleResume": "确认后会恢复 Goal 的自动调度资格并移回 Active Goals;实际执行仍受 quota、Gate 和 Todo 约束。",
"proposal.impact.lifecycleStop": "确认后会停止自动推进,并将 Goal 移入折叠的「已停止」列表;历史、Todo 和证据都会保留,可随时恢复。",
"proposal.impact.protected": "该操作需要通过受保护的 LoopX 写入服务完成。",
"proposal.impact.operation": "这里仅展示同一份不可变条款。请在已绑定的飞书群确认或拒绝;确认只会消费一个规范 claim。",
"proposal.primary.apply": "确认并应用",
"proposal.primary.goalCreate": "创建 Goal 并开始首轮",
"proposal.primary.lifecycleDelete": "删除 Goal",
"proposal.primary.lifecycleResume": "恢复 Goal",
"proposal.primary.lifecycleStop": "停止 Goal",
"proposal.primary.todoStart": "创建任务并开始执行",
"proposal.primary.operationGroup": "前往飞书群确认",
"proposal.primary.operationResultPending": "结果卡回传待恢复",
"proposal.primary.operationResultVerified": "结果已核验",
"proposal.resultDelivery.verified": "已在原群卡片完成回读核验",
"proposal.resultDelivery.pending": "等待回传并核验原群卡片",
"proposal.summary.goalCreate": "创建 Goal:{title}",
"proposal.summary.heartbeat": "为当前 Goal 设置 Heartbeat",
"proposal.summary.lifecycleDelete": "删除 Goal:{title}",
Expand Down
Loading