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
8 changes: 4 additions & 4 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2380,9 +2380,9 @@ export async function updateGoalOwnership(body: { goal_id: string; mode: Executi


const privateConversationSchema = z.object({
binding_id: z.string(), app_ref: z.string(), context_kind: z.literal("project"),
binding_id: z.string(), app_ref: z.string(), context_kind: z.enum(["project", "steward"]),
project_ref: z.string(), project_title: z.string(), context_available: z.boolean(), executor_endpoint_id: z.string(),
grant: z.literal("workspace_read"), listener_status: z.string(),
grant: z.enum(["workspace_read", "portfolio_read"]), goal_count: z.number().int().default(0), listener_status: z.string(),
pending_count: z.number().int(), recovery_count: z.number().int(),
});
const privateConversationsSchema = z.object({ok: z.literal(true), revision: z.number().int(),
Expand All @@ -2391,10 +2391,10 @@ export type PrivateConversation = z.infer<typeof privateConversationSchema>;
export async function fetchPrivateConversations() {
return privateConversationsSchema.parse(await requestJson<unknown>("/api/chat/lark/private-conversations"));
}
export async function connectPrivateConversation(appRef: string, projectRef: string, executor: string) {
export async function connectPrivateConversation(appRef: string, projectRef: string, executor: string, contextKind: "project" | "steward" = "project") {
return privateConversationsSchema.parse(await requestJson<unknown>("/api/chat/lark/private-conversations", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({app_ref: appRef, project_ref: projectRef, executor_endpoint_id: executor}),
body: JSON.stringify({app_ref: appRef, project_ref: projectRef, executor_endpoint_id: executor, context_kind: contextKind}),
}));
}
export async function disconnectPrivateConversation(bindingId: string, revision: number) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function PrivateConversationPanel() {
const [app, setApp] = useState("");
const [project, setProject] = useState("");
const [executor, setExecutor] = useState("");
const [role, setRole] = useState<"project" | "steward">("project");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);

Expand Down Expand Up @@ -54,10 +55,11 @@ export function PrivateConversationPanel() {
finally {setBusy(false);}
}
return <section className="personal-detail-card personal-private-conversation" aria-label={zh ? "本人飞书私聊" : "Owner private Chat"}>
<h3>{zh ? "本人私聊 · 项目对话" : "Owner private Chat · Project conversation"}</h3>
<p>{zh ? "每个 App 单独核验登录本人,只读讨论所选工作区。普通私聊不会创建 Goal。" : "Verify the logged-in owner independently for each App. Discuss the selected workspace with a read grant; ordinary private Chat creates no Goal."}</p>
<h3>{zh ? "本人私聊 · 项目助手与管家" : "Owner private Chat · Project assistant and steward"}</h3>
<p>{zh ? "每个 App 单独核验本人。项目助手只读讨论工作区,不创建隐藏 Goal。管家从空 portfolio 开始,只管理在此入口明确确认的新委托。" : "Verify the owner independently for each App. Project Chat discusses the workspace without hidden Goals. A steward starts with an empty portfolio and manages only new commissions explicitly confirmed here."}</p>
{rows.map(row => <article key={row.binding_id}>
<strong>{row.app_ref} · {row.context_available ? row.project_title : (zh ? "工作区不可用" : "Workspace unavailable")}</strong>
<p>{row.context_kind === "steward" ? (zh ? `LoopX 管家 · ${row.goal_count === 0 ? "暂无已授权的新委托;没有继承旧目标。" : `${row.goal_count} 个已确认的新委托`}` : `LoopX steward · ${row.goal_count} new confirmed commissions; no inherited Goals.`) : (zh ? "普通项目助手 · 只读对话" : "Project assistant · Read-only Chat")}</p>
<p>{row.executor_endpoint_id} · {zh ? "监听状态" : "Listener"}: {listenerLabel(row.listener_status)}</p>
<p>{zh ? `待处理或回复:${row.pending_count}` : `Pending execution or reply: ${row.pending_count}`}</p>
{row.recovery_count > 0 ? <p role="status">{zh ? "存在尚未确认的发送回执。服务会读取原回执恢复;不要重新发送同一任务。检查 App 登录、权限和原会话后刷新状态。" : "A send receipt is unconfirmed. The service reads the original receipt to recover; avoid resending the same task. Check this App login, permissions and original Session, then refresh status."}</p> : null}
Expand All @@ -69,17 +71,21 @@ export function PrivateConversationPanel() {
<option value="">{zh ? "选择已验证 App" : "Select a verified App"}</option>
{apps.map(app => <option key={app.app_ref} value={app.app_ref}>{app.label} · {app.app_ref}</option>)}
</select></label>
<label>{zh ? "角色" : "Role"}<select aria-label={zh ? "私聊角色" : "Private Chat role"} value={role} disabled={busy} onChange={event => setRole(event.target.value as "project" | "steward")}>
<option value="project">{zh ? "普通项目助手" : "Project assistant"}</option><option value="steward">{zh ? "LoopX 管家(新委托)" : "LoopX steward (new commissions)"}</option>
</select></label>
<label>{zh ? "工作区" : "Workspace"}<select aria-label={zh ? "私聊工作区" : "Private Chat workspace"} value={project} disabled={busy} onChange={event => setProject(event.target.value)}>
<option value="">{zh ? "选择授权工作区" : "Select an authorized workspace"}</option>
{projects.map(project => <option key={project.project_ref} value={project.project_ref}>{project.title}</option>)}
</select></label>
<label>{zh ? "执行器" : "Executor"}<select aria-label={zh ? "私聊执行器" : "Private Chat executor"} value={executor} disabled={busy} onChange={event => setExecutor(event.target.value)}>
{executors.map(executor => <option key={executor} value={executor}>{executor}</option>)}
</select></label>
<div className="personal-detail-actions"><button disabled={busy || !app || !project || !executor} onClick={() => void act(() => connectPrivateConversation(app, project, executor))} type="button">
<div className="personal-detail-actions"><button disabled={busy || !app || !project || !executor} onClick={() => void act(() => connectPrivateConversation(app, project, executor, role))} type="button">
{busy ? (zh ? "正在核验" : "Verifying") : (zh ? "连接本人私聊" : "Connect owner private Chat")}</button>
<button disabled={busy} onClick={() => void act(refresh)} type="button">{zh ? "刷新状态" : "Refresh status"}</button></div>
<p>{zh ? "从手机发送文字开始;后续消息进入原会话队列。/status 查看状态,/stop 停止当前执行,/new 开启新会话。图片、文件会明确提示暂不支持。" : "Send text from your phone to begin; follow-ups queue in the same Session. /status checks state, /stop stops the current Turn, /new starts a new conversation. Images and files receive an explicit unsupported response."}</p>
<p>{zh ? "从手机发送文字开始;后续消息进入原会话队列。/status 查看聊天状态,/stop 停止当前聊天执行,/new 开启新会话。图片、文件会明确提示暂不支持。" : "Send text from your phone to begin; follow-ups queue in the same Session. /status checks Chat state, /stop stops the current Chat Turn, /new starts a new conversation. Images and files receive an explicit unsupported response."}</p>
<p>{zh ? "管家新委托:/delegate --tokens N 具体目标。先读预览,再用原私聊的完整 /confirm 命令确认;15 分钟过期。原生执行保持只读,总 token 上限可能被运行中的请求超过;没有默认定时调度。回执提供 /stop-commission 停止和 /resume-commission 恢复命令;恢复保留原线程及累计用量。" : "Steward commission: /delegate --tokens N objective. Read the preview, then use its full /confirm command in the original private Chat within 15 minutes. Native execution remains read-only; in-flight requests can exceed the total token allowance. No default schedule. Receipts provide /stop-commission and /resume-commission commands; recovery retains the original thread and cumulative usage."}</p>
{error ? <p role="alert">{error}</p> : null}
</section>;
}
53 changes: 52 additions & 1 deletion docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ browser coverage. A native Codex source canary separately qualifies distinct
upstream threads with independently observed real App identities. Neither is a
live Lark/model/mobile result. Installed service qualification, phone journeys,
registered Agent selection, real incremental/media/permission interactions and
a separately granted long-running steward remain open acceptance. Runtime and
broader long-running coordination remain open acceptance. Runtime and
permission-boundary changes require maintainer review before promotion.

The private setup UI composes within Settings → Lark; the App workspace scope
Expand All @@ -74,6 +74,57 @@ Synthetic product previews: [desktop](../../assets/personal-workspace/private-pr
[narrow](../../assets/personal-workspace/private-project-conversations-narrow.png),
[revoked workspace](../../assets/personal-workspace/private-project-workspace-revoked.png).

## Bound steward private Chat: explicit new commissions

Settings → Lark can now select a steward role independently of ordinary project
Chat. The existing typed conversation binding owns its App, verified owner,
source, workspace and bounded portfolio. A verified empty scope is distinct from
an unavailable authorization; it contains no inherited Goals and does not certify
global inventory coverage. Ordinary Chat still has no Goal or manager identity.

Only an explicit `/delegate --tokens N objective` prepares a `goal.create`
preview. Confirmation must arrive from that exact owner/App/source within fifteen
minutes. The immutable preview shows the read-only boundary, total native token
allowance and absence of automatic scheduling. Existing canonical Chat actions
create the Goal and return their receipt; Core adopts only that exact new creation
in the configured workspace. Existing Goal and single-workspace fallbacks cannot
redirect it. Neither normal conversation nor model prose creates a commission.

The existing service worker advances the durable Core request without blocking
inbound admission. Codex native Goal continuation performs the read-only work;
its result returns through the original private-source delivery journal.
`/stop-commission` freezes the execution target at admission, while an explicit
`/resume-commission ... --tokens N` retains the original Session, native thread,
objective and cumulative usage. Native completion is host execution evidence,
not canonical Goal/Todo acceptance. An allowance includes previous usage and
context; an in-flight request can exceed it. No default heartbeat is enabled.

Portfolio extension refreshes scoped evidence/tools in the same steward thread.
After creation commits, the existing request journal saves its exact resource
receipt before attempting portfolio adoption. An adoption or readback failure
keeps that operation queued for recovery. Recovery rechecks the original
binding and canonical receipt, adopts the same resources, and returns their
result without creating another Goal or model thread. Notification cannot
settle an operation whose adoption is pending. Fault journeys cover adoption
failure, lost adoption readback, an interrupted creation-owner call and revoked
binding; they qualify provider/Core IO recovery, not a full host restart.
App, owner, source and workspace identity remain frozen and rechecked. A silent
native event reader cannot block the control RPC receipt needed to pause work.
The provider-specific setup companion belongs to the Lark extension; the
conversation, request, scope and creation semantics remain in their typed owners.

Regression journeys cover independently verified empty versus missing scope,
wrong App/source confirmation, expiry, creation/adoption, native result return,
stop, duplicate events and same-thread recovery. A local actual Codex source
canary separately exercised a budget-limited synthetic commission and resumed it
in the same native thread to return verified fixture findings. It did not prove
real Lark inbound or phone acceptance. Multi-Agent coordination, actual media /
permission interactions, installed login recovery and mobile journeys remain
open; runtime/authority promotion still requires maintainer review.

Synthetic product previews: [empty steward and project assistant](../../assets/personal-workspace/private-steward-empty.png),
[narrow](../../assets/personal-workspace/private-steward-empty-narrow.png).

## Decision: make the App the place where work conversations continue

Users should be able to say “接着做,结果给我” / “Keep going and bring me the result”
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ provider 读回确认回复;发生没有 receipt 的不确定写入时不盲
后续消息持久排队、exact stop,以及重启后原会话恢复和已确认回复不重复发送。
这些是合成 provider/协议验收;真实原生 Codex 另行验证独立线程与上下文隔离。
真实 Lark 收发、安装候选、手机旅程、注册 Agent 选择、媒体/增量/权限回调,以及
新管家的明确长期委托仍未验收。当前私聊绑定仅支持普通只读项目会话。
更广的长期协调仍未验收。这一普通项目绑定保持只读,新增管家入口见下一检查点。

私聊配置复用设置 → Lark;App 范围仍是本机普通对话的唯一入口。未存储 App 身份
的旧群聊 profile 保留原 profile-hash 监听锁键。没有私聊绑定时不增加鉴权;有绑定
Expand All @@ -53,6 +53,45 @@ extension,typed binding owner 继续保持 provider-neutral。
![窄屏私聊设置](../../assets/personal-workspace/private-project-conversations-narrow.png)
![工作区撤权读回](../../assets/personal-workspace/private-project-workspace-revoked.png)

## 本人私聊管家:明确的新委托

设置 → Lark 可为独立 App 选择管家角色。既有 typed conversation binding
固定 App、独立核验的本人、来源、工作区和有界 portfolio。已核验的空范围与授权
不可用分开:新管家没有继承旧 Goal;空范围也不证明全局 inventory 覆盖完整。
普通项目聊天仍没有 Goal 或管家身份。

只有显式 `/delegate --tokens N 具体目标` 会准备既有 `goal.create` 预览。
确认必须在十五分钟内从原本人、App 和私聊来源进入。预览固定只读边界、原生总
token 上限和不启用默认调度的事实。既有 canonical Chat action 创建 Goal 并返回
回执,Core 仅把该确切新创建加入此管家范围。已有 Goal 或单一工作区 fallback
不能重定向委托;普通聊天和模型文字不能创建委托。

既有服务 worker 推进已持久受理的 Core 操作,入站不等终态。只读工作由 Codex
原生 Goal continuation 执行,结果经原私聊 delivery journal 返回。
`/stop-commission` 在受理时固定确切执行目标;显式
`/resume-commission ... --tokens N` 保留原 Session、原生线程、目标和累计用量。
原生完成是宿主执行证据,不是 canonical Goal/Todo 验收。总上限包含历史用量与
上下文,运行中的请求可能超过上限;没有默认 heartbeat。

新委托扩展 portfolio 时,在原管家线程刷新有界证据和工具。创建提交后,既有
请求日志先保存确切资源回执,再尝试加入管家范围。加入或读回失败时,操作保持
排队以供恢复。恢复重新核验原 binding 与 canonical 回执,采用同一批资源并
返回结果,不再创建 Goal 或模型线程。范围加入仍待恢复时,通知回执不能结算
该操作。故障旅程覆盖加入失败、加入后读回丢失、创建 owner 调用中断及 binding
撤权;验证的是 provider/Core IO 恢复,不是完整宿主重启。App、本人、来源和
工作区仍固定并重新核验。原生事件读取在没有输出时,也不能卡住停止所需的控制
RPC 回执。Lark 专属设置 companion 归 extension;会话、请求、范围和创建语义
仍由现有 typed owner 持有。

回归覆盖独立空态与无授权、另一 App/来源确认拒绝、过期、创建与 scope adoption、
原生结果返回、停止、重复事件和同线程恢复。本地真实 Codex 源码 canary 另验证
合成委托遇到额度限制后,沿用原生线程恢复并返回可核验的 fixture 结果。
这不证明真实飞书入站或手机验收。多 Agent 协调、真实媒体/权限交互、正式安装与
登录恢复、手机旅程仍开放;runtime/authority 发布继续等待维护者审核。

合成产品预览:[空管家与项目助手](../../assets/personal-workspace/private-steward-empty.png)、
[窄视口](../../assets/personal-workspace/private-steward-empty-narrow.png)。

## 决策:让 App 成为工作会话持续进行的地方

用户应能在 LoopX 中说“接着做,结果给我” / “Keep going and bring me the result”,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading