@tea-agent/loop-agent 0.34.3 → 0.34.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -3
- package/CHANGELOG.md +66 -22
- package/README.md +2 -2
- package/dist/shared/operator/capabilities.js +0 -7
- package/dist/worker/cli.js +21 -27
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +134 -4
- package/dist/worker/console/chat/pi-runtime.js +308 -63
- package/dist/worker/console/chat/resource-preferences-store.js +152 -0
- package/dist/worker/console/chat/routes.js +425 -19
- package/dist/worker/console/chat/shortcuts.js +208 -9
- package/dist/worker/console/chat/tool-preview.js +162 -5
- package/dist/worker/console/chat/turn-execution-registry.js +82 -0
- package/dist/worker/console/dag-execution-receipt.js +14 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/index.js +1 -0
- package/dist/worker/console/open-browser.js +134 -0
- package/dist/worker/console/recovery-cta.js +1 -1
- package/dist/worker/console/server.js +5 -1
- package/dist/worker/console/static/assets/index-qpkysQYW.css +1 -0
- package/dist/worker/console/static/assets/index-y980PqtP.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-markdown-security.js +38 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +1 -3
- package/dist/worker/console/static-src/operator-chat/format.js +2 -2
- package/dist/worker/console/static-src/operator-chat/refs.js +24 -0
- package/dist/worker/console/static-src/operator-chat/resource-auto-invocation.js +91 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +690 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +158 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +535 -86
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +11 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +81 -3
- package/dist/workflows/dag/init-hybrid.js +2 -0
- package/docs/architecture/evolution.md +1 -1
- package/docs/architecture/system-overview.md +1 -1
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/operations/local-development-environment.md +4 -2
- package/docs/templates/branch-merge-report.md +9 -0
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +2 -2
- package/docs/templates/init-managed-agents.md +2 -2
- package/package.json +6 -2
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +2 -2
- package/skills/loop-agent/references/command-reference.md +2 -3
- package/dist/worker/console/static/assets/index-BQkhJpV8.css +0 -1
- package/dist/worker/console/static/assets/index-BpuHmlSP.js +0 -29
package/AGENTS.md
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
4. 涉及测试纪律/验证声明/调试时继续读:`docs/governance/harness-methodology-*.md`。
|
|
39
39
|
5. 查看最近提交、相关 plan/progress/report;`git status --short --branch`;跑最小基线验证。
|
|
40
40
|
6. 后端/接口/pytest → `taskKind: "backend-test"`(不是 `--profile`);知识回写 `knowledge-sync`;图谱开荒 `knowledge-graph-bootstrap`。`--profile` 仅 `auto|minimal|standard|reviewed|supervised`。
|
|
41
|
-
7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790
|
|
41
|
+
7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790;listen 成功后默认打开系统浏览器,`--no-open` 禁止)(`/inspect/` 只读);`observe serve` 已下线(`OBSERVE_SERVE_REMOVED` + exit 2);`observe snapshot` 仍可用。
|
|
42
42
|
8. 分支合并 → 先读 `docs/operations/branch-merge-guideline.md`。
|
|
43
43
|
|
|
44
44
|
## 会话协议
|
|
@@ -74,7 +74,9 @@ DAG 执行期间,主 agent 不得修改 writeSet 外的任何工作区文件
|
|
|
74
74
|
|
|
75
75
|
权威源:`docs/governance/verification-matrix.md`。
|
|
76
76
|
|
|
77
|
-
默认节奏:编辑中只跑矩阵「最低验证」(typecheck + 定向 Vitest);`git commit` 的 `pre-commit` 只做 `check-repo`;`git push` 的 `pre-push`
|
|
77
|
+
默认节奏:编辑中只跑矩阵「最低验证」(typecheck + 定向 Vitest);`git commit` 的 `pre-commit` 只做 `check-repo`;`git push` 的 `pre-push` 经 `node scripts/pre-push-verify.mjs` 在 receipt 命中时复用、miss 时跑 full `bash scripts/ci.sh`(全量 typecheck + `npm test`)。每个 clone 安装一次 hooks:`bash scripts/install-git-hooks.sh`。
|
|
78
|
+
|
|
79
|
+
连续 source→target 交付遵循**单一最终树全量验证**:同一 Git tree、相同环境与命令合同在 receipt TTL 内只选一个本地 full authority(有效 receipt / target pre-push / 可选 `npm run verify:tree`),不按分支名策略化,也不机械双跑 full CI。细节见 verification-matrix 与 branch-merge-guideline。
|
|
78
80
|
|
|
79
81
|
常用:
|
|
80
82
|
|
|
@@ -83,7 +85,10 @@ DAG 执行期间,主 agent 不得修改 writeSet 外的任何工作区文件
|
|
|
83
85
|
npm run typecheck
|
|
84
86
|
npx vitest run <相关测试路径>
|
|
85
87
|
|
|
86
|
-
#
|
|
88
|
+
# 可选:提前验证当前 clean HEAD 并写 receipt(随后同 tree push 可复用)
|
|
89
|
+
npm run verify:tree
|
|
90
|
+
|
|
91
|
+
# push / 交付前(或依赖 pre-push;receipt miss 时安全回退 full)
|
|
87
92
|
bash scripts/ci.sh
|
|
88
93
|
npm run build
|
|
89
94
|
node bin/loop-agent.js --help
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.34.5] - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 优化 Operator Chat 对话页体验,包括隐藏重复标题、升级 Markdown 渲染、改进工具调用展示,并合并了 `/` 命令面板
|
|
10
|
+
- 控制台启动后默认自动打开系统浏览器看板
|
|
11
|
+
|
|
12
|
+
### 改进
|
|
13
|
+
|
|
14
|
+
- 优化 Operator Chat 对话页(UI-01 至 UI-11):隐藏重复标题、补齐品牌空态与真实注入上下文侧栏、改进工具调用摘要与结果展示、修复遮罩 hover 形变、为截断会话标题提供 tooltip、升级 Markdown/GFM 渲染,并增加用户主动打开的调用记录入口与合并后的 `/` 命令面板
|
|
15
|
+
- 会话列表操作改为按需显示 Remix Icon,同时保留键盘 focus 与触屏可访问路径
|
|
16
|
+
- 输入区将图片上传收敛为图标按钮,并避免“全部工具 · N · 状态”歧义
|
|
17
|
+
- 控制台在服务监听成功后默认打开系统浏览器
|
|
18
|
+
|
|
19
|
+
## [0.34.4] - 2026-08-12
|
|
20
|
+
|
|
21
|
+
### 重点更新
|
|
22
|
+
|
|
23
|
+
- 裸 `agent-worker console` 收敛为唯一看板入口,监听成功后默认打开系统浏览器,并删除 `console serve`
|
|
24
|
+
- 本地连续交付改为按精确 Git tree 复用验证结果,避免提交、推送和分支合并之间机械重复全量测试,同时保持不同 tree、不同环境和远程 CI 的独立质量门槛
|
|
25
|
+
- 全面加固 Operator Chat 的 Turn 所有权、停止与异常恢复流程,解决浏览器与服务端状态错位时的重复提交、停止失效和错误提示问题
|
|
26
|
+
- Operator Chat 新增按仓库保存的 Skills 自动使用偏好,用户可控制技能是否进入模型自动发现范围,同时保留手动调用能力
|
|
27
|
+
- 修复控制台任务导航无法自主执行后端测试与前端测试 DAG 的问题,现可正确识别 pytest、playwright 等多种测试框架的验证命令
|
|
28
|
+
|
|
29
|
+
### 新增
|
|
30
|
+
|
|
31
|
+
- 新增可选的 `npm run verify:tree`,对当前严格 clean HEAD 执行完整验证并写入 receipt,随后推送同一 tree 时可直接复用
|
|
32
|
+
- 新增只读的当前 tree receipt 诊断,可区分匹配、缺失、过期、损坏和工作区不合格等状态,不执行测试或写入 receipt
|
|
33
|
+
- Operator Chat 引入完整的 Turn ownership 协议和服务端真实中止接口,并在状态接口中提供 active/latest Turn 与 runtime busy 事实
|
|
34
|
+
- Operator Chat Skills 新增「允许自动使用」开关,偏好按仓库持久化;关闭后仍可通过 `/skill:<name>` 手动调用
|
|
35
|
+
|
|
36
|
+
### 改进
|
|
37
|
+
|
|
38
|
+
- 裸 `agent-worker console` 成为唯一 Console 启动入口:本地图形交互环境在服务监听成功后默认打开系统浏览器,`--no-open` 可明确禁止;删除 `console serve` 兼容子命令
|
|
39
|
+
- source→target 连续交付统一选择一个本地全量验证权威;默认 commit/push 不增加步骤,远程 CI 与发布环境不复用本地 receipt
|
|
40
|
+
- pre-push receipt 诊断和执行日志改用有界命令摘要,并严格校验 tree、环境、命令合同、有效期及 receipt 内容一致性
|
|
41
|
+
- Skills 运行时快照改为使用服务端资源标识和独立的自动调用状态,资源变更仅接受当前 inventory 中的合法条目
|
|
42
|
+
- Turn 已占用时返回可恢复的中文状态提示和真实 active Turn,不再向用户暴露裸露的 `chat turn already active` 错误
|
|
43
|
+
|
|
44
|
+
### 修复
|
|
45
|
+
|
|
46
|
+
- 修复浏览器已认为对话结束、服务端却仍持有 active Turn 时的重复提交竞态;提交现在支持幂等请求标识、boot epoch orphan 隔离和 durable Turn/runtime busy 联合对账
|
|
47
|
+
- 修复创建 Turn 的响应不确定时状态被过早释放的问题;客户端会保留 ambiguous 状态,通过 `/state` 绑定或释放真实 ownership,不会重新生成请求标识
|
|
48
|
+
- 修复停止请求在 Turn 尚未绑定时丢失的问题;绑定成功后只发送一次真实 abort,且服务端缺少 abort 能力时不会通过 dispose 会话伪造停止
|
|
49
|
+
- 修复 active bind、terminal SSE 与 React streaming 状态交错时提交锁未释放的问题,并统一 adopt、停止中和 abort-pending 为非致命提示
|
|
50
|
+
- 修复控制台任务导航执行 backend-test 和 frontend-test DAG 时,因结构化验证门禁未识别 pytest/playwright 等命令而始终被阻断的问题
|
|
51
|
+
|
|
5
52
|
## [0.34.3] - 2026-08-12
|
|
6
53
|
|
|
7
54
|
### 重点更新
|
|
@@ -85,36 +132,33 @@
|
|
|
85
132
|
|
|
86
133
|
## [0.33.6] - 2026-08-11
|
|
87
134
|
|
|
135
|
+
### 重点更新
|
|
136
|
+
|
|
137
|
+
- 加固 PRD 导入与 Semantic Intake,在结构化解析不足时提供安全回退,并增强 writeSet 审查与运行结果摘要
|
|
138
|
+
- Operator Chat 接入单一事件流、启动 epoch 隔离、实时运行时控制与开放资源加载,提升长会话恢复和自主 DAG 监督能力
|
|
139
|
+
- 统一 Windows 等环境下只读 Git 命令的有界恢复机制,同时保留用户自有的 Pi 模型路由配置
|
|
140
|
+
|
|
88
141
|
### 新增
|
|
89
142
|
|
|
90
|
-
-
|
|
91
|
-
- Operator Chat
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
- reconcile(cursor_expired) 自动恢复而非全局致命错误
|
|
96
|
-
- 接入 Operator Chat 实时运行时控制
|
|
97
|
-
- open operator chat resource loading
|
|
98
|
-
- enrich execution report and URL resolution
|
|
143
|
+
- task advance 新增 writeSet delta 与 run summary,直接展示边界扩张来源、节点结果和下一步操作
|
|
144
|
+
- Operator Chat 引入单一事件流与 boot epoch fence,支持 cursor 过期后的自动对账恢复,避免将可恢复问题升级为全局错误
|
|
145
|
+
- ChatEventStore 增加事件合并与容量上限双重压缩,控制长期会话持久化体积
|
|
146
|
+
- Operator Chat 新增实时模型、thinking 和运行时资源控制,并按开放资源加载规则解析 Skills、Extensions 与 Packages
|
|
147
|
+
- Operator Chat 可对安全的 bounded DAG 进行持续监督和恢复,执行报告同步补充运行细节与目标 URL 来源
|
|
99
148
|
|
|
100
149
|
### 改进
|
|
101
150
|
|
|
102
|
-
-
|
|
103
|
-
-
|
|
151
|
+
- 只读 Git 查询统一使用共享韧性执行器,在 Windows 启动型故障下有界恢复,不扩大 mutation 命令权限
|
|
152
|
+
- 初始化与升级流程保留用户自行配置的 Pi 模型路由,不再因默认配置变化机械覆盖
|
|
153
|
+
- Packages 面板区分配置状态与真实安装元数据,并统一使用品牌图标
|
|
104
154
|
|
|
105
155
|
### 修复
|
|
106
156
|
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
- split rerun defaults and repair selector escaping
|
|
113
|
-
- ignore delimiters inside pytest strings
|
|
114
|
-
- resolve target URL from runtime context
|
|
115
|
-
- reject priority-only module stems
|
|
116
|
-
- stop desiredSha256 mechanical checks from blocking generated-content upgrades
|
|
117
|
-
- use mobius header brand icon
|
|
157
|
+
- 修复 Operator Chat 流式状态与 pi-web 可靠性机制未完全对齐的问题,提升重连和终态收敛稳定性
|
|
158
|
+
- 修复前端测试环境预检中的反引号转义错误,以及重跑默认值和 repair 选择器转义不一致的问题
|
|
159
|
+
- 修复后端测试完整性检查误判字符串内部未配对分隔符,以及按优先级生成非法单模块文件的问题
|
|
160
|
+
- 修复前端测试目标 URL 未从运行时上下文解析的问题,执行时现使用可审计的真实地址来源
|
|
161
|
+
- 修复 generated init 内容因 `desiredSha256` 机械比较而阻断安全升级的问题
|
|
118
162
|
|
|
119
163
|
## [0.33.5] - 2026-08-11
|
|
120
164
|
|
package/README.md
CHANGED
|
@@ -104,8 +104,8 @@ loop-agent dag report --run-id <run-id>
|
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
# 终端 A:Console(任务/运行操作面;canonical mutation 只经 sibling loop-agent)
|
|
107
|
-
agent-worker console # 默认 repo=当前目录、port=8790
|
|
108
|
-
agent-worker console
|
|
107
|
+
agent-worker console # 默认 repo=当前目录、port=8790;本地图形环境默认打开浏览器
|
|
108
|
+
agent-worker console --no-open # 只启动服务,不打开浏览器
|
|
109
109
|
agent-worker console doctor --repo .
|
|
110
110
|
|
|
111
111
|
```
|
|
@@ -2299,13 +2299,6 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
|
|
|
2299
2299
|
source: "agent-worker",
|
|
2300
2300
|
exclusionReason: "Not exposed to Operator Chat in M5.",
|
|
2301
2301
|
},
|
|
2302
|
-
{
|
|
2303
|
-
command: "agent-worker console serve",
|
|
2304
|
-
coverage: "excluded",
|
|
2305
|
-
action: null,
|
|
2306
|
-
source: "agent-worker",
|
|
2307
|
-
exclusionReason: "Not exposed to Operator Chat in M5.",
|
|
2308
|
-
},
|
|
2309
2302
|
{
|
|
2310
2303
|
command: "agent-worker console doctor",
|
|
2311
2304
|
coverage: "excluded",
|
package/dist/worker/cli.js
CHANGED
|
@@ -712,37 +712,22 @@ export function buildAgentWorkerProgram() {
|
|
|
712
712
|
process.stdout.write(`${JSON.stringify(snapshot)}\n`);
|
|
713
713
|
});
|
|
714
714
|
consoleCmd
|
|
715
|
-
.description("Loop Operator Console (local); bare `agent-worker console` starts it directly (repo defaults to current working directory, port defaults to 8790)")
|
|
716
|
-
.enablePositionalOptions()
|
|
717
|
-
.passThroughOptions()
|
|
715
|
+
.description("Loop Operator Console (local); bare `agent-worker console` starts it directly (repo defaults to current working directory, port defaults to 8790). Opens the system browser after listen unless --no-open or a non-graphical environment.")
|
|
718
716
|
.option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
|
|
719
717
|
.option("--port <port>", "HTTP port", "8790")
|
|
720
718
|
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
|
|
721
719
|
.option("--debug", "Log request source/method/path/status to stderr")
|
|
722
720
|
.option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
|
|
723
721
|
.option("--default-timeout-ms <ms>", "LoopAgentClient hard timeout for Console operations (ms; default 14400000 / 4h; 0 disables)")
|
|
724
|
-
.
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
defaultTimeoutMsRaw: options.defaultTimeoutMs,
|
|
734
|
-
});
|
|
735
|
-
});
|
|
736
|
-
consoleCmd
|
|
737
|
-
.command("serve")
|
|
738
|
-
.option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
|
|
739
|
-
.option("--port <port>", "HTTP port", "8790")
|
|
740
|
-
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
|
|
741
|
-
.option("--debug", "Log request source/method/path/status to stderr")
|
|
742
|
-
.option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
|
|
743
|
-
.option("--default-timeout-ms <ms>", "LoopAgentClient hard timeout for Console operations (ms; default 14400000 / 4h; 0 disables)")
|
|
744
|
-
.description("Start Loop Operator Console HTTP server (compat entry; equivalent to bare `agent-worker console`)")
|
|
745
|
-
.action(async (options) => {
|
|
722
|
+
.option("--no-open", "Do not open the system browser after the Console listens")
|
|
723
|
+
.action(async (options, command) => {
|
|
724
|
+
// Reject unknown tokens such as legacy `console serve` instead of starting HTTP.
|
|
725
|
+
const extras = command.args.filter((arg) => arg.length > 0);
|
|
726
|
+
if (extras.length > 0) {
|
|
727
|
+
process.stderr.write(`error: unknown command '${extras[0]}' for agent-worker console\n`);
|
|
728
|
+
process.exitCode = 1;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
746
731
|
await runConsoleServe({
|
|
747
732
|
repoRoot: path.resolve(options.repo),
|
|
748
733
|
host: options.host,
|
|
@@ -752,6 +737,8 @@ export function buildAgentWorkerProgram() {
|
|
|
752
737
|
? path.resolve(options.appData)
|
|
753
738
|
: undefined,
|
|
754
739
|
defaultTimeoutMsRaw: options.defaultTimeoutMs,
|
|
740
|
+
// Commander maps --no-open to open:false; default remains open:true.
|
|
741
|
+
noOpen: options.open === false,
|
|
755
742
|
});
|
|
756
743
|
});
|
|
757
744
|
consoleCmd
|
|
@@ -850,9 +837,10 @@ function parseOptionalTimeoutMs(raw) {
|
|
|
850
837
|
}
|
|
851
838
|
return value;
|
|
852
839
|
}
|
|
853
|
-
/**
|
|
840
|
+
/** Console HTTP entry for bare `agent-worker console` (listen first, then optional browser open). */
|
|
854
841
|
async function runConsoleServe(options) {
|
|
855
842
|
const { createConsoleServer } = await import("./console/server.js");
|
|
843
|
+
const { maybeOpenConsoleBrowser } = await import("./console/open-browser.js");
|
|
856
844
|
try {
|
|
857
845
|
const server = await createConsoleServer({
|
|
858
846
|
repoRoot: options.repoRoot,
|
|
@@ -862,7 +850,13 @@ async function runConsoleServe(options) {
|
|
|
862
850
|
appDataRoot: options.appDataRoot,
|
|
863
851
|
defaultTimeoutMs: parseOptionalTimeoutMs(options.defaultTimeoutMsRaw),
|
|
864
852
|
});
|
|
865
|
-
|
|
853
|
+
// Keep stdout bind URL contract (may include 0.0.0.0 / ::); browser URL is normalized separately.
|
|
854
|
+
process.stdout.write(`${server.url}
|
|
855
|
+
`);
|
|
856
|
+
await maybeOpenConsoleBrowser({
|
|
857
|
+
noOpen: options.noOpen === true,
|
|
858
|
+
serverUrl: server.url,
|
|
859
|
+
});
|
|
866
860
|
await new Promise((resolve) => {
|
|
867
861
|
const shutdown = () => resolve();
|
|
868
862
|
process.once("SIGINT", shutdown);
|
|
@@ -43,6 +43,7 @@ export function openConsoleAppData(options) {
|
|
|
43
43
|
chats: path.join(repoNamespace, "chats"),
|
|
44
44
|
chatSessions: path.join(repoNamespace, "chat-sessions"),
|
|
45
45
|
chatEvents: path.join(repoNamespace, "chat-events"),
|
|
46
|
+
resourcePreferences: path.join(repoNamespace, "resource-preferences.json"),
|
|
46
47
|
};
|
|
47
48
|
for (const dir of [
|
|
48
49
|
paths.drafts,
|
|
@@ -55,6 +56,7 @@ export function openConsoleAppData(options) {
|
|
|
55
56
|
paths.chats,
|
|
56
57
|
paths.chatSessions,
|
|
57
58
|
paths.chatEvents,
|
|
59
|
+
path.dirname(paths.resourcePreferences),
|
|
58
60
|
]) {
|
|
59
61
|
ensureSecureDir(dir);
|
|
60
62
|
}
|
|
@@ -129,9 +129,56 @@ function boundedRedactedJson(value) {
|
|
|
129
129
|
previewHash: createHash("sha256").update(preview).digest("hex"),
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
|
+
/** Prefer readable Pi ToolResult text content; fall back to JSON shell. */
|
|
133
|
+
function projectToolResultPreview(value) {
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
const preview = boundedRedactedText(value) ?? "";
|
|
136
|
+
return {
|
|
137
|
+
preview,
|
|
138
|
+
previewHash: createHash("sha256").update(preview).digest("hex"),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
142
|
+
const record = value;
|
|
143
|
+
const content = record.content;
|
|
144
|
+
if (Array.isArray(content)) {
|
|
145
|
+
const texts = [];
|
|
146
|
+
for (const block of content) {
|
|
147
|
+
if (!block || typeof block !== "object")
|
|
148
|
+
continue;
|
|
149
|
+
const item = block;
|
|
150
|
+
if (typeof item.text === "string")
|
|
151
|
+
texts.push(item.text);
|
|
152
|
+
}
|
|
153
|
+
if (texts.length > 0) {
|
|
154
|
+
const joined = texts.join("\n");
|
|
155
|
+
const preview = boundedRedactedText(joined) ?? "";
|
|
156
|
+
return {
|
|
157
|
+
preview,
|
|
158
|
+
previewHash: createHash("sha256").update(preview).digest("hex"),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (typeof record.text === "string") {
|
|
163
|
+
const preview = boundedRedactedText(record.text) ?? "";
|
|
164
|
+
return {
|
|
165
|
+
preview,
|
|
166
|
+
previewHash: createHash("sha256").update(preview).digest("hex"),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (typeof record.output === "string") {
|
|
170
|
+
const preview = boundedRedactedText(record.output) ?? "";
|
|
171
|
+
return {
|
|
172
|
+
preview,
|
|
173
|
+
previewHash: createHash("sha256").update(preview).digest("hex"),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return boundedRedactedJson(value);
|
|
178
|
+
}
|
|
132
179
|
export function projectChatToolEventPayload(input) {
|
|
133
|
-
const { preview, previewHash } = boundedRedactedJson(input.value);
|
|
134
180
|
if (input.kind === "tool_call") {
|
|
181
|
+
const { preview, previewHash } = boundedRedactedJson(input.value);
|
|
135
182
|
return {
|
|
136
183
|
toolCallId: input.toolCallId,
|
|
137
184
|
toolName: input.toolName,
|
|
@@ -139,6 +186,7 @@ export function projectChatToolEventPayload(input) {
|
|
|
139
186
|
previewHash,
|
|
140
187
|
};
|
|
141
188
|
}
|
|
189
|
+
const { preview, previewHash } = projectToolResultPreview(input.value);
|
|
142
190
|
return {
|
|
143
191
|
toolCallId: input.toolCallId,
|
|
144
192
|
toolName: input.toolName,
|
|
@@ -221,6 +269,20 @@ function shouldCoalesce(ring, turnId, partial) {
|
|
|
221
269
|
return false;
|
|
222
270
|
return nowMs - Date.parse(tail.at) <= ring.coalesceWindowMs;
|
|
223
271
|
}
|
|
272
|
+
/** Stable hash of the normalized prompt payload for clientRequestId binding. */
|
|
273
|
+
export function hashChatTurnPayload(input) {
|
|
274
|
+
const normalized = {
|
|
275
|
+
text: input.text,
|
|
276
|
+
images: (input.images ?? []).map((image) => ({
|
|
277
|
+
type: image.type ?? "image",
|
|
278
|
+
mimeType: image.mimeType ?? "",
|
|
279
|
+
data: image.data ?? "",
|
|
280
|
+
})),
|
|
281
|
+
};
|
|
282
|
+
return createHash("sha256")
|
|
283
|
+
.update(JSON.stringify(normalized))
|
|
284
|
+
.digest("hex");
|
|
285
|
+
}
|
|
224
286
|
const DEFAULT_MAX_EVENTS = 2000;
|
|
225
287
|
/** Mirrors pi-web's bounded-ring orientation (2000 events / 10 MiB). */
|
|
226
288
|
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
@@ -399,6 +461,24 @@ export function createChatEventStore(options) {
|
|
|
399
461
|
eventId: eventIdFor(sessionId, event.seq),
|
|
400
462
|
}));
|
|
401
463
|
const turns = Array.isArray(parsed.turns) ? parsed.turns : [];
|
|
464
|
+
// New Console epoch: orphan any queued/running turn whose ownerEpoch is
|
|
465
|
+
// missing or mismatched. Releases the active lease without pretending the
|
|
466
|
+
// user aborted (CHAT_TURN_OWNER_LOST fence).
|
|
467
|
+
let orphaned = false;
|
|
468
|
+
const now = new Date().toISOString();
|
|
469
|
+
for (const turn of turns) {
|
|
470
|
+
if (turn.state !== "queued" && turn.state !== "running")
|
|
471
|
+
continue;
|
|
472
|
+
if (turn.ownerEpoch === eventEpoch)
|
|
473
|
+
continue;
|
|
474
|
+
turn.state = "failed";
|
|
475
|
+
turn.finishedAt = now;
|
|
476
|
+
turn.error = {
|
|
477
|
+
code: "CHAT_TURN_OWNER_LOST",
|
|
478
|
+
message: "turn ownership lost after Console restart; previous execution is no longer owned",
|
|
479
|
+
};
|
|
480
|
+
orphaned = true;
|
|
481
|
+
}
|
|
402
482
|
turnCounts.set(sessionId, parsed.turnCount ?? 0);
|
|
403
483
|
const ring = {
|
|
404
484
|
events,
|
|
@@ -415,6 +495,8 @@ export function createChatEventStore(options) {
|
|
|
415
495
|
// R6: re-check the bounds after a restart — a persisted file may exceed the
|
|
416
496
|
// current limits if it was written by an older config or hand-edited.
|
|
417
497
|
enforceBounds(sessionId, ring);
|
|
498
|
+
if (orphaned)
|
|
499
|
+
persist(sessionId, ring);
|
|
418
500
|
return ring;
|
|
419
501
|
}
|
|
420
502
|
catch (error) {
|
|
@@ -521,11 +603,35 @@ export function createChatEventStore(options) {
|
|
|
521
603
|
}
|
|
522
604
|
return { copiedCount };
|
|
523
605
|
},
|
|
524
|
-
createTurn(sessionId) {
|
|
606
|
+
createTurn(sessionId, input) {
|
|
525
607
|
const ring = ringFor(sessionId);
|
|
608
|
+
const clientRequestId = input?.clientRequestId?.trim() || undefined;
|
|
609
|
+
const payloadHash = input?.payloadHash?.trim() || undefined;
|
|
610
|
+
if (clientRequestId) {
|
|
611
|
+
const existing = ring.turns.find((turn) => turn.clientRequestId === clientRequestId);
|
|
612
|
+
if (existing) {
|
|
613
|
+
if (payloadHash &&
|
|
614
|
+
existing.payloadHash &&
|
|
615
|
+
existing.payloadHash !== payloadHash) {
|
|
616
|
+
return {
|
|
617
|
+
ok: false,
|
|
618
|
+
code: "REQUEST_ID_REUSE_CONFLICT",
|
|
619
|
+
message: `clientRequestId ${clientRequestId} was reused with a different payload`,
|
|
620
|
+
existingTurn: { ...existing },
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
// Same id + same (or missing) payload: idempotent return, no new lease.
|
|
624
|
+
return {
|
|
625
|
+
ok: true,
|
|
626
|
+
turn: { ...existing },
|
|
627
|
+
created: false,
|
|
628
|
+
idempotent: true,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
526
632
|
const active = ring.turns.find((turn) => turn.state === "queued" || turn.state === "running");
|
|
527
633
|
if (active)
|
|
528
|
-
return { ok: false, code: "TURN_ACTIVE", activeTurn: active };
|
|
634
|
+
return { ok: false, code: "TURN_ACTIVE", activeTurn: { ...active } };
|
|
529
635
|
const ordinal = (turnCounts.get(sessionId) ?? 0) + 1;
|
|
530
636
|
turnCounts.set(sessionId, ordinal);
|
|
531
637
|
const now = new Date().toISOString();
|
|
@@ -536,11 +642,14 @@ export function createChatEventStore(options) {
|
|
|
536
642
|
ordinal,
|
|
537
643
|
state: "queued",
|
|
538
644
|
createdAt: now,
|
|
645
|
+
ownerEpoch: eventEpoch,
|
|
646
|
+
...(clientRequestId ? { clientRequestId } : {}),
|
|
647
|
+
...(payloadHash ? { payloadHash } : {}),
|
|
539
648
|
};
|
|
540
649
|
ring.turns.push(turn);
|
|
541
650
|
ring.approxBytes += byteHint(turn);
|
|
542
651
|
persist(sessionId, ring);
|
|
543
|
-
return { ok: true, turn };
|
|
652
|
+
return { ok: true, turn: { ...turn }, created: true, idempotent: false };
|
|
544
653
|
},
|
|
545
654
|
setTurnState(sessionId, turnId, state, error) {
|
|
546
655
|
const ring = ringFor(sessionId);
|
|
@@ -561,6 +670,27 @@ export function createChatEventStore(options) {
|
|
|
561
670
|
persist(sessionId, ring);
|
|
562
671
|
return { ...turn };
|
|
563
672
|
},
|
|
673
|
+
markAbortRequested(sessionId, turnId, at) {
|
|
674
|
+
const ring = ringFor(sessionId);
|
|
675
|
+
const turn = ring.turns.find((candidate) => candidate.turnId === turnId);
|
|
676
|
+
if (!turn)
|
|
677
|
+
return undefined;
|
|
678
|
+
const before = JSON.stringify(turn).length;
|
|
679
|
+
if (!turn.abortRequestedAt)
|
|
680
|
+
turn.abortRequestedAt = at ?? new Date().toISOString();
|
|
681
|
+
ring.approxBytes +=
|
|
682
|
+
(JSON.stringify(turn).length - before) * BYTE_GATE_SCALE;
|
|
683
|
+
persist(sessionId, ring);
|
|
684
|
+
return { ...turn };
|
|
685
|
+
},
|
|
686
|
+
findTurnByClientRequestId(sessionId, clientRequestId) {
|
|
687
|
+
const id = clientRequestId.trim();
|
|
688
|
+
if (!id)
|
|
689
|
+
return undefined;
|
|
690
|
+
const ring = ringFor(sessionId);
|
|
691
|
+
const turn = ring.turns.find((candidate) => candidate.clientRequestId === id);
|
|
692
|
+
return turn ? { ...turn } : undefined;
|
|
693
|
+
},
|
|
564
694
|
getTurn(sessionId, turnId) {
|
|
565
695
|
const ring = ringFor(sessionId);
|
|
566
696
|
const turn = ring.turns.find((candidate) => candidate.turnId === turnId);
|