@tea-agent/loop-agent 0.32.1 → 0.33.1
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/CHANGELOG.md +78 -0
- package/dist/executors/model-routing.js +14 -4
- package/dist/governance/manifest-types.js +34 -7
- package/dist/worker/console/chat/model-resolver.js +114 -34
- package/dist/worker/console/chat/workspace-landing.js +58 -22
- package/dist/worker/console/doctor.js +1 -0
- package/dist/worker/console/night-aux-ticker.js +5 -0
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/pi-readiness.js +26 -17
- package/dist/worker/console/server.js +2 -0
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
- package/dist/worker/console/static/assets/index-PzYzcuFG.js +29 -0
- package/dist/worker/console/static/index.html +3 -2
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +3 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +0 -31
- package/dist/worker/observability/read-model.js +106 -0
- package/dist/worker/observe/routes.js +22 -9
- package/dist/worker/observe/static/api.js +42 -3
- package/dist/worker/observe/static/app.js +15 -0
- package/dist/worker/observe/static/constants.js +10 -0
- package/dist/worker/observe/static/custom-select.js +567 -0
- package/dist/worker/observe/static/index.html +56 -47
- package/dist/worker/observe/static/kpi.js +2 -24
- package/dist/worker/observe/static/operator-chrome.css +480 -0
- package/dist/worker/observe/static/operator-chrome.d.ts +82 -0
- package/dist/worker/observe/static/operator-chrome.js +554 -0
- package/dist/worker/observe/static/router.js +54 -1
- package/dist/worker/observe/static/shell-chrome.js +1 -11
- package/dist/worker/observe/static/state.js +20 -0
- package/dist/worker/observe/static/styles.css +680 -299
- package/dist/worker/observe/static/views/dag-inspector.js +20 -17
- package/dist/worker/observe/static/views/dag.js +136 -59
- package/dist/worker/observe/static/views/dags.js +877 -0
- package/dist/worker/observe/static/views/dashboard.js +67 -8
- package/dist/workflows/dag/init-hybrid.js +58 -29
- package/docs/templates/harness.schema.json +5 -0
- package/harness.json +4 -4
- package/package.json +1 -1
- package/dist/worker/console/static/assets/index-Bpa2qrc-.js +0 -29
- package/dist/worker/console/static/assets/index-BqfFDdnG.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,89 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.33.1] - 2026-08-10
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- Operate 与 Inspect 合并为统一的双层导航壳,取代旧的三层堆叠栏
|
|
10
|
+
- 在「操作」与「观测」模式间切换时保持对象上下文,支持一键带参数返回恢复现场
|
|
11
|
+
- 夜间信息架构收敛,总览新增「昨夜班次」条带,移除冗余的并列 tab
|
|
12
|
+
- 全面加固窄屏适配与可访问性,优化共享资产加载失败时的降级体验
|
|
13
|
+
|
|
14
|
+
### 新增
|
|
15
|
+
|
|
16
|
+
- 新增共享壳唯一实现 operator-chrome.css/js,包含全局品牌带与工作区下划线 tab
|
|
17
|
+
- Inspect 总览新增「昨夜班次」条带,支持通过/失败/需人工/待 harvest 状态一眼可见
|
|
18
|
+
- OperatorSurfaceHealthV1 新增 repoName 字段,统一上移仓库身份展示
|
|
19
|
+
- 新增 operator-chrome 契约测试,覆盖 tab 映射、模型、对象连续与返回路径
|
|
20
|
+
|
|
21
|
+
### 改进
|
|
22
|
+
|
|
23
|
+
- 模式切换不丢对象:恢复页持有 DAG 时切「观测」直达同一 DAG 画布,并提供来源 chip 支持一键返回
|
|
24
|
+
- 夜间信息架构收敛:Inspect 顶栏移除「夜间任务」并列 tab,#/night 深链详情保留并提供「在操作台管理 →」入口
|
|
25
|
+
- 仓库身份上移:Inspect 总览移除重复的内联仓库横幅,面包屑从 site-header 移入内容区页头
|
|
26
|
+
- Console 改用 CSP 友好的原生动态导入,共享资产加载失败时保留最小可用导航与错误状态
|
|
27
|
+
- 窄屏(1100px/760px)下新鲜度与 Pi 状态收敛为紧凑图标语法,避免整页横向溢出
|
|
28
|
+
|
|
29
|
+
### 修复
|
|
30
|
+
|
|
31
|
+
- 修复 Inspect 壳重渲染时 #header-refresh 新鲜度文案被 health 回填或 hash 切换清空的问题
|
|
32
|
+
- 补齐仓库身份浮层的 aria-controls/aria-expanded/dialog 名称,支持外部点击与 Esc 关闭并恢复按钮焦点
|
|
33
|
+
- 修复关闭来源 chip 时误删 node/page 等对象页查询参数的问题,现仅移除 from 参数
|
|
34
|
+
- 移除 Console 中 new Function 的间接导入与 SVG 图标的 innerHTML 注入面,提升安全性
|
|
35
|
+
- 修复 hash 导航与 destroy 时监听与宿主 class 未完整清理的问题
|
|
36
|
+
|
|
37
|
+
## [0.33.0] - 2026-08-09
|
|
38
|
+
|
|
39
|
+
### 重点更新
|
|
40
|
+
|
|
41
|
+
- Operator Console 工作区导航升级为 hash 路由,支持页面刷新保留状态及浏览器前进后退,并新增模型与设置页面入口
|
|
42
|
+
- 夜间辅助调度器(NightAuxTicker)改为自动启动,移除手动开关,简化操作流程
|
|
43
|
+
- Observe DAG 运行历史页交互全面优化,改进分页器布局、自定义选择器与复制反馈体验
|
|
44
|
+
- 执行器支持 provider/model 显式引用格式,消除运行时模型路由的歧义
|
|
45
|
+
|
|
46
|
+
### 新增
|
|
47
|
+
|
|
48
|
+
- Operator Console 工作区导航新增 hash 虚拟路径(#/tasks、#/night、#/models、#/settings),支持刷新恢复与浏览器历史导航
|
|
49
|
+
- 新增模型(Models)与设置(Settings)页面入口
|
|
50
|
+
- 夜间辅助调度器(NightAuxTicker)现默认自动启动,无需手动开启即可维持夜间调度活跃度
|
|
51
|
+
- Observe 新增统一的自定义选择器菜单,支持键盘导航与自适应定位
|
|
52
|
+
- Observe 新增 DAG 运行历史分页功能
|
|
53
|
+
|
|
54
|
+
### 改进
|
|
55
|
+
|
|
56
|
+
- Operator Console 工作区导航顺序调整,将对话(Tasks)置于首位
|
|
57
|
+
- 夜间任务面板的健康状态栏简化为纯文本状态展示,移除旧的手动开关
|
|
58
|
+
- Observe 夜页头部操作按钮样式统一
|
|
59
|
+
- Observe DAG 历史分页器视觉布局升级为精致的分段设计,并优化窄屏下的显示
|
|
60
|
+
- 执行器 Pi 层级支持 provider/model 引用格式,规范化解析字符串与对象两种配置写法,提升模型路由准确性
|
|
61
|
+
|
|
62
|
+
### 修复
|
|
63
|
+
|
|
64
|
+
- 修复 Observe 自定义选择器键盘焦点异常的问题
|
|
65
|
+
- 修复 Observe DAG 历史分页器在窄视口下布局溢出的问题
|
|
66
|
+
- 修复 Observe 复制反馈按钮的边框样式显示异常,恢复无边框设计
|
|
67
|
+
- 优化 Observe DAG 节点错误摘要展示,将失败行备注压缩为紧凑的两行摘要,避免撑高表格
|
|
68
|
+
|
|
5
69
|
## [0.32.1] - 2026-08-09
|
|
6
70
|
|
|
71
|
+
### 新增
|
|
72
|
+
|
|
73
|
+
- Observe:新增 DAG 运行历史页 `#/dags`:分页表格、hash 恢复 page/pageSize、手动刷新;只读 `GET /api/dag-runs?page=&pageSize=` 返回轻量列表,排除 dry-run/init-only,省略 nodes/edges 等重字段
|
|
74
|
+
- Dashboard「最近 DAG 运行」卡片右上增加「查看全部」入口,进入完整历史页;卡片仍保留最近 10 条
|
|
75
|
+
|
|
76
|
+
### 修复
|
|
77
|
+
|
|
78
|
+
- Observe DAG 历史页:分页摘要与失败态使用本地固定可信文案;首次 Loading 与后续动态分页/刷新控件分离;双路径失败共用 aria-live;分页失败不污染列表;默认直连真实 `/api/dag-runs`
|
|
79
|
+
- Observe DAG 历史页 review 收口:取消在途 abort 中途仍式 GET 推进 requestSeq,防止旧响应覆盖缓存页;run-id 复制按钮独立 1.5s 成功计时;图标恢复;手动刷新替换当前页缓存;失效页码 bump prefetch generation 并拒绝陈旧预取写回;`totalPages>7` 页码窗口收紧为首页/末页/当前±1(无紧邻填补);`.dags-pagination.is-busy` 降级为上方 `opacity`
|
|
80
|
+
- Observe DAG 节点表格:失败行备注改为两行错误摘要,避免完整 errorPreview 撑高表格;完整错误仍在节点检查器输出中查看
|
|
81
|
+
|
|
7
82
|
### 改进
|
|
8
83
|
|
|
9
84
|
- Operator Console 前端重构视觉层级与代码结构:`OperatorChat.tsx`(3656→407 行)、`App.tsx`(3044→182 行)、`NightJobsWorkbench.tsx`(852→90 行)按职责拆为 `operator-chat/`、`app/`、`pages/`、`shell/`、`night/` 模块,消息流 / 工具卡 / gate 卡 / composer / 侧栏各自独立;样式统一为单一语义 token 层,人工决策 gate 卡以决策卡规格(橙色左边条 + 浮起阴影 + 状态徽标 + 未决时吸附 composer 上方)突出,工具过程卡默认退后
|
|
85
|
+
- Observe:DAG 历史 pageSize 与节点检查器「执行分次」改为统一自定义选择器(触发器 + listbox;键盘/ARIA/焦点/自适应定位);不改动 hash-only 分页、attempt raw key / 默认最新 / 节点切换重置语义
|
|
86
|
+
- Observe DAG 历史页:复制改为图标反馈(成功勾号 + 页面 aria-live;失败右下角短暂 Toast);数字分页器改为精致分段布局;hash 单一事实源;缓存 + AbortController + 相邻页低优先级预取,减少翻页双重加载与长时间等待
|
|
87
|
+
|
|
10
88
|
- Agent Worker Production Readiness v1 Phase 8:在已发布 `@0.32.0` 上完成 published-tarball deterministic campaign 与 burn-in day 0 开窗;scorecard 仍为 **`NOT_READY`**(待 burn-in 累计、live Pi canary、live Closeout、C4/C7 进程 crash)
|
|
11
89
|
|
|
12
90
|
### 修复
|
|
@@ -9,14 +9,24 @@ export const DEFAULT_DAG_MODELS = {
|
|
|
9
9
|
* DAG executor model tier keys that may carry a per-complexity override.
|
|
10
10
|
*/
|
|
11
11
|
const EXECUTOR_MODEL_TIERS = ["LOW", "MED", "HIGH"];
|
|
12
|
+
/** Format harness tier selection for DAG Pi model routing (provider/model when set). */
|
|
13
|
+
export function formatExecutorTierModelRef(selection) {
|
|
14
|
+
return selection.provider
|
|
15
|
+
? `${selection.provider}/${selection.model}`
|
|
16
|
+
: selection.model;
|
|
17
|
+
}
|
|
12
18
|
/** Resolve one tier to a model and optional explicit thinking override. */
|
|
13
19
|
export function resolveExecutorTierSelection(execConfig, tier) {
|
|
14
20
|
const tierSelection = normalizeExecutorTierValue(execConfig?.[tier]);
|
|
15
21
|
if (tierSelection)
|
|
16
22
|
return tierSelection;
|
|
17
23
|
const defaultModel = execConfig?.defaultModel;
|
|
18
|
-
if (defaultModel && defaultModel !== "default")
|
|
24
|
+
if (defaultModel && defaultModel !== "default") {
|
|
25
|
+
const parsedDefault = normalizeExecutorTierValue(defaultModel);
|
|
26
|
+
if (parsedDefault)
|
|
27
|
+
return parsedDefault;
|
|
19
28
|
return { model: defaultModel };
|
|
29
|
+
}
|
|
20
30
|
return { model: DEFAULT_DAG_EXECUTOR_MODELS.pi[tier] };
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
@@ -32,9 +42,9 @@ export function resolveExecutorTierSelection(execConfig, tier) {
|
|
|
32
42
|
*/
|
|
33
43
|
export function resolveExecutorModelMatrix(_executor, execConfig) {
|
|
34
44
|
return {
|
|
35
|
-
LOW: resolveExecutorTierSelection(execConfig, "LOW")
|
|
36
|
-
MED: resolveExecutorTierSelection(execConfig, "MED")
|
|
37
|
-
HIGH: resolveExecutorTierSelection(execConfig, "HIGH")
|
|
45
|
+
LOW: formatExecutorTierModelRef(resolveExecutorTierSelection(execConfig, "LOW")),
|
|
46
|
+
MED: formatExecutorTierModelRef(resolveExecutorTierSelection(execConfig, "MED")),
|
|
47
|
+
HIGH: formatExecutorTierModelRef(resolveExecutorTierSelection(execConfig, "HIGH")),
|
|
38
48
|
};
|
|
39
49
|
}
|
|
40
50
|
/** Resolve only explicitly configured per-tier thinking values. */
|
|
@@ -11,16 +11,27 @@ export const worktreeManifestConfigSchema = z.object({
|
|
|
11
11
|
});
|
|
12
12
|
export const taskExecutorSchema = z.enum(["pi"]);
|
|
13
13
|
export const CURSOR_TASK_EXECUTOR_REMOVED_ERROR = 'task executor "cursor" is no longer supported; governed runtime is Pi-only';
|
|
14
|
-
/** Per-tier model override: bare model id string, or `{ model, thinking? }`. */
|
|
14
|
+
/** Per-tier model override: bare model id string, or `{ provider?, model, thinking? }`. */
|
|
15
15
|
export const executorTierModelSchema = z.union([
|
|
16
16
|
z.string(),
|
|
17
17
|
z
|
|
18
18
|
.object({
|
|
19
|
+
provider: z.string().min(1).optional(),
|
|
19
20
|
model: z.string().min(1),
|
|
20
21
|
thinking: z.string().optional(),
|
|
21
22
|
})
|
|
22
23
|
.strict(),
|
|
23
24
|
]);
|
|
25
|
+
function parseProviderModelReference(value) {
|
|
26
|
+
const slash = value.indexOf("/");
|
|
27
|
+
if (slash < 0)
|
|
28
|
+
return { model: value };
|
|
29
|
+
const provider = value.slice(0, slash);
|
|
30
|
+
const model = value.slice(slash + 1);
|
|
31
|
+
if (!provider || !model)
|
|
32
|
+
return { model: value };
|
|
33
|
+
return { provider, model };
|
|
34
|
+
}
|
|
24
35
|
/** Normalize an executors.pi LOW|MED|HIGH value. */
|
|
25
36
|
export function normalizeExecutorTierValue(value) {
|
|
26
37
|
if (value === null || value === undefined)
|
|
@@ -28,18 +39,34 @@ export function normalizeExecutorTierValue(value) {
|
|
|
28
39
|
if (typeof value === "string") {
|
|
29
40
|
if (value === "default" || value.length === 0)
|
|
30
41
|
return undefined;
|
|
31
|
-
|
|
42
|
+
const parsed = parseProviderModelReference(value);
|
|
43
|
+
return parsed.provider
|
|
44
|
+
? { model: parsed.model, provider: parsed.provider }
|
|
45
|
+
: { model: parsed.model };
|
|
32
46
|
}
|
|
33
47
|
if (typeof value !== "object" || Array.isArray(value))
|
|
34
48
|
return undefined;
|
|
35
|
-
const
|
|
36
|
-
|
|
49
|
+
const record = value;
|
|
50
|
+
const modelRaw = record.model;
|
|
51
|
+
if (typeof modelRaw !== "string" ||
|
|
52
|
+
modelRaw.length === 0 ||
|
|
53
|
+
modelRaw === "default") {
|
|
37
54
|
return undefined;
|
|
38
55
|
}
|
|
39
|
-
const
|
|
56
|
+
const explicitProvider = typeof record.provider === "string" && record.provider.length > 0
|
|
57
|
+
? record.provider
|
|
58
|
+
: undefined;
|
|
59
|
+
const parsed = parseProviderModelReference(modelRaw);
|
|
60
|
+
const provider = explicitProvider ?? parsed.provider;
|
|
61
|
+
const model = explicitProvider
|
|
62
|
+
? parsed.provider && parsed.provider === explicitProvider
|
|
63
|
+
? parsed.model
|
|
64
|
+
: modelRaw
|
|
65
|
+
: parsed.model;
|
|
66
|
+
const thinking = record.thinking;
|
|
40
67
|
return typeof thinking === "string" && thinking.length > 0
|
|
41
|
-
? { model, thinking }
|
|
42
|
-
: { model };
|
|
68
|
+
? { model, ...(provider ? { provider } : {}), thinking }
|
|
69
|
+
: { model, ...(provider ? { provider } : {}) };
|
|
43
70
|
}
|
|
44
71
|
export const executorManifestSchema = z
|
|
45
72
|
.object({
|
|
@@ -6,11 +6,8 @@
|
|
|
6
6
|
* `snapshot.available` model list (preferring providers whose model entry is
|
|
7
7
|
* backed by credentials).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* we search `available` for entries whose `id` matches the modelId and pick the
|
|
12
|
-
* first whose provider resolves to a configured/credentialled source. This
|
|
13
|
-
* mirrors how the user's wizard-local / configured providers surface models.
|
|
9
|
+
* harness.json tiers may be bare model ids, `provider/model` strings, or
|
|
10
|
+
* `{ provider?, model, thinking? }` objects — see normalizeExecutorTierValue.
|
|
14
11
|
*
|
|
15
12
|
* This module is deliberately framework-light: it takes a repoRoot + an
|
|
16
13
|
* optional model-list snapshot and returns a `{provider, modelId}` descriptor.
|
|
@@ -18,44 +15,61 @@
|
|
|
18
15
|
*/
|
|
19
16
|
import { readFile } from "node:fs/promises";
|
|
20
17
|
import path from "node:path";
|
|
18
|
+
import { normalizeExecutorTierValue } from "../../../governance/manifest-types.js";
|
|
21
19
|
/** Tier keys in harness executors.pi that may override the default. */
|
|
22
20
|
const EXECUTOR_TIERS = ["LOW", "MED", "HIGH"];
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
async function readHarnessExecutorsPi(repoRoot) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await readFile(path.join(repoRoot, "harness.json"), "utf-8");
|
|
24
|
+
const manifest = JSON.parse(raw);
|
|
25
|
+
return manifest.executors?.pi;
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
catch {
|
|
28
28
|
return undefined;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read one executors.pi tier as normalized provider + modelId.
|
|
33
|
+
*/
|
|
34
|
+
export async function readHarnessPiTierSelection(repoRoot, tier = "MED") {
|
|
35
|
+
const pi = await readHarnessExecutorsPi(repoRoot);
|
|
36
|
+
if (!pi)
|
|
37
|
+
return undefined;
|
|
38
|
+
const tierSelection = normalizeExecutorTierValue(pi[tier]);
|
|
39
|
+
if (tierSelection) {
|
|
40
|
+
return {
|
|
41
|
+
modelId: tierSelection.model,
|
|
42
|
+
...(tierSelection.provider ? { provider: tierSelection.provider } : {}),
|
|
43
|
+
...(tierSelection.thinking ? { thinking: tierSelection.thinking } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const defaultSelection = normalizeExecutorTierValue(pi.defaultModel);
|
|
47
|
+
if (!defaultSelection)
|
|
48
|
+
return undefined;
|
|
49
|
+
return {
|
|
50
|
+
modelId: defaultSelection.model,
|
|
51
|
+
...(defaultSelection.provider
|
|
52
|
+
? { provider: defaultSelection.provider }
|
|
53
|
+
: {}),
|
|
54
|
+
...(defaultSelection.thinking
|
|
55
|
+
? { thinking: defaultSelection.thinking }
|
|
56
|
+
: {}),
|
|
57
|
+
};
|
|
34
58
|
}
|
|
35
59
|
/**
|
|
36
60
|
* Read executors.pi.<tier> from the repo harness.json. Returns undefined when
|
|
37
61
|
* harness.json is absent or the tier is unset / "default" sentinel.
|
|
38
62
|
*
|
|
39
63
|
* Default tier = MED (matches DAG executor model matrix convention).
|
|
64
|
+
* When provider is explicit, returns `provider/model` for DAG-compat callers.
|
|
40
65
|
*/
|
|
41
66
|
export async function readHarnessPiModelTier(repoRoot, tier = "MED") {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const manifest = JSON.parse(raw);
|
|
45
|
-
const pi = manifest.executors?.pi;
|
|
46
|
-
if (!pi)
|
|
47
|
-
return undefined;
|
|
48
|
-
const tierModel = modelIdFromTierValue(pi[tier]);
|
|
49
|
-
if (tierModel)
|
|
50
|
-
return tierModel;
|
|
51
|
-
const defaultModel = pi.defaultModel;
|
|
52
|
-
if (defaultModel && defaultModel !== "default")
|
|
53
|
-
return defaultModel;
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
67
|
+
const selection = await readHarnessPiTierSelection(repoRoot, tier);
|
|
68
|
+
if (!selection)
|
|
57
69
|
return undefined;
|
|
58
|
-
|
|
70
|
+
return selection.provider
|
|
71
|
+
? `${selection.provider}/${selection.modelId}`
|
|
72
|
+
: selection.modelId;
|
|
59
73
|
}
|
|
60
74
|
/**
|
|
61
75
|
* Search the SDK's available-model list for entries matching a modelId.
|
|
@@ -71,7 +85,16 @@ export async function readHarnessPiModelTier(repoRoot, tier = "MED") {
|
|
|
71
85
|
* in the SDK's configuredProviders set).
|
|
72
86
|
* 3. Stable list order otherwise.
|
|
73
87
|
*/
|
|
74
|
-
export function findProviderForModelId(modelId, available) {
|
|
88
|
+
export function findProviderForModelId(modelId, available, options) {
|
|
89
|
+
const preferred = options?.preferredProvider?.trim();
|
|
90
|
+
if (preferred) {
|
|
91
|
+
const exact = available.find((m) => m.provider === preferred &&
|
|
92
|
+
(m.id === modelId ||
|
|
93
|
+
m.id === `${preferred}/${modelId}` ||
|
|
94
|
+
m.id.endsWith(`/${modelId}`)));
|
|
95
|
+
if (exact)
|
|
96
|
+
return exact;
|
|
97
|
+
}
|
|
75
98
|
const matches = available.filter((m) => {
|
|
76
99
|
if (m.id === modelId)
|
|
77
100
|
return true;
|
|
@@ -95,6 +118,51 @@ export function findProviderForModelId(modelId, available) {
|
|
|
95
118
|
};
|
|
96
119
|
return [...matches].sort((a, b) => score(a) - score(b))[0];
|
|
97
120
|
}
|
|
121
|
+
function resolveTierDescriptor(selection, available) {
|
|
122
|
+
if (selection.provider) {
|
|
123
|
+
const entry = findProviderForModelId(selection.modelId, available, {
|
|
124
|
+
preferredProvider: selection.provider,
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
provider: selection.provider,
|
|
128
|
+
resolved: Boolean(entry),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const entry = findProviderForModelId(selection.modelId, available);
|
|
132
|
+
return { provider: entry?.provider, resolved: Boolean(entry) };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Resolve harness executors.pi tiers (LOW/MED/HIGH) to provider descriptors.
|
|
136
|
+
* Unresolved tiers (modelId not in available list) omit provider.
|
|
137
|
+
*/
|
|
138
|
+
export async function resolveHarnessPiExecutorMatrix(repoRoot, available) {
|
|
139
|
+
const rows = [];
|
|
140
|
+
for (const tier of EXECUTOR_TIERS) {
|
|
141
|
+
const selection = await readHarnessPiTierSelection(repoRoot, tier);
|
|
142
|
+
if (!selection)
|
|
143
|
+
continue;
|
|
144
|
+
const resolved = resolveTierDescriptor(selection, available);
|
|
145
|
+
rows.push({
|
|
146
|
+
tier,
|
|
147
|
+
modelId: selection.modelId,
|
|
148
|
+
provider: resolved.provider,
|
|
149
|
+
resolved: resolved.resolved,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return rows;
|
|
153
|
+
}
|
|
154
|
+
/** Map Pi ModelRuntime snapshot.available to resolver entries. */
|
|
155
|
+
export function mapSnapshotAvailableModels(snapshot) {
|
|
156
|
+
const available = snapshot.available ?? [];
|
|
157
|
+
const configured = snapshot.configuredProviders;
|
|
158
|
+
const isConfigured = (provider) => configured instanceof Set ? configured.has(provider) : false;
|
|
159
|
+
return available.map((m) => ({
|
|
160
|
+
id: m.id,
|
|
161
|
+
name: m.name,
|
|
162
|
+
provider: m.provider,
|
|
163
|
+
hasCredentials: isConfigured(m.provider),
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
98
166
|
/**
|
|
99
167
|
* Resolve the Chat default model descriptor. Returns undefined when no model
|
|
100
168
|
* can be resolved (harness unset + no snapshot match).
|
|
@@ -104,15 +172,27 @@ export function findProviderForModelId(modelId, available) {
|
|
|
104
172
|
* @param options.tier harness tier to read (default MED)
|
|
105
173
|
*/
|
|
106
174
|
export async function resolveDefaultChatModel(repoRoot, available, options) {
|
|
107
|
-
const
|
|
108
|
-
if (!
|
|
175
|
+
const selection = await readHarnessPiTierSelection(repoRoot, options?.tier ?? "MED");
|
|
176
|
+
if (!selection)
|
|
109
177
|
return undefined;
|
|
110
|
-
|
|
178
|
+
if (selection.provider) {
|
|
179
|
+
const entry = findProviderForModelId(selection.modelId, available, {
|
|
180
|
+
preferredProvider: selection.provider,
|
|
181
|
+
});
|
|
182
|
+
if (!entry)
|
|
183
|
+
return undefined;
|
|
184
|
+
return {
|
|
185
|
+
provider: selection.provider,
|
|
186
|
+
modelId: selection.modelId,
|
|
187
|
+
label: entry.name,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const entry = findProviderForModelId(selection.modelId, available);
|
|
111
191
|
if (!entry)
|
|
112
192
|
return undefined;
|
|
113
193
|
return {
|
|
114
194
|
provider: entry.provider,
|
|
115
|
-
modelId,
|
|
195
|
+
modelId: selection.modelId,
|
|
116
196
|
label: entry.name,
|
|
117
197
|
};
|
|
118
198
|
}
|
|
@@ -13,14 +13,58 @@ function configuredWorkspace(value) {
|
|
|
13
13
|
if (normalized === "chat" ||
|
|
14
14
|
normalized === "tasks" ||
|
|
15
15
|
normalized === "recovery" ||
|
|
16
|
-
normalized === "night"
|
|
16
|
+
normalized === "night" ||
|
|
17
|
+
normalized === "models" ||
|
|
18
|
+
normalized === "settings") {
|
|
17
19
|
return normalized;
|
|
18
20
|
}
|
|
19
21
|
return undefined;
|
|
20
22
|
}
|
|
23
|
+
function readWorkspaceParams(query) {
|
|
24
|
+
const read = (key) => query.get(key)?.trim() || undefined;
|
|
25
|
+
return {
|
|
26
|
+
...(read("taskId") ? { taskId: read("taskId") } : {}),
|
|
27
|
+
...(read("dagRunId") ? { dagRunId: read("dagRunId") } : {}),
|
|
28
|
+
...(read("fromNodeId") ? { fromNodeId: read("fromNodeId") } : {}),
|
|
29
|
+
...(read("featureId") ? { featureId: read("featureId") } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Parse `#/tasks?taskId=…` hash routes (Observe-style virtual paths). */
|
|
33
|
+
export function parseConsoleHashRoute(hashValue) {
|
|
34
|
+
const raw = String(hashValue ?? "").replace(/^#/, "");
|
|
35
|
+
if (!raw)
|
|
36
|
+
return null;
|
|
37
|
+
const qIndex = raw.indexOf("?");
|
|
38
|
+
const pathPart = qIndex >= 0 ? raw.slice(0, qIndex) : raw;
|
|
39
|
+
const queryPart = qIndex >= 0 ? raw.slice(qIndex + 1) : "";
|
|
40
|
+
const path = (pathPart || "/").split("?")[0] || "/";
|
|
41
|
+
if (path === "/" || path === "")
|
|
42
|
+
return null;
|
|
43
|
+
const segment = path.replace(/^\//, "").split("/")[0];
|
|
44
|
+
const workspace = configuredWorkspace(segment);
|
|
45
|
+
if (!workspace)
|
|
46
|
+
return null;
|
|
47
|
+
return { workspace, ...readWorkspaceParams(new URLSearchParams(queryPart)) };
|
|
48
|
+
}
|
|
49
|
+
function parseLegacyWorkspaceHref(location) {
|
|
50
|
+
const query = new URLSearchParams(location.search);
|
|
51
|
+
const hash = location.hash.startsWith("#")
|
|
52
|
+
? location.hash.slice(1)
|
|
53
|
+
: location.hash;
|
|
54
|
+
const hashQuery = new URLSearchParams(hash);
|
|
55
|
+
const workspace = configuredWorkspace(query.get("workspace") ??
|
|
56
|
+
query.get("landing") ??
|
|
57
|
+
hashQuery.get("workspace"));
|
|
58
|
+
if (!workspace)
|
|
59
|
+
return null;
|
|
60
|
+
return { workspace, ...readWorkspaceParams(query) };
|
|
61
|
+
}
|
|
21
62
|
function workspaceFromLocation(location) {
|
|
22
63
|
if (!location)
|
|
23
64
|
return undefined;
|
|
65
|
+
const hashRoute = parseConsoleHashRoute(location.hash);
|
|
66
|
+
if (hashRoute)
|
|
67
|
+
return hashRoute.workspace;
|
|
24
68
|
const query = new URLSearchParams(location.search);
|
|
25
69
|
const queryValue = configuredLanding(query.get("landing") ?? query.get("workspace"));
|
|
26
70
|
if (queryValue)
|
|
@@ -36,31 +80,23 @@ export function resolveDefaultLanding(input) {
|
|
|
36
80
|
configuredLanding(input.env) ??
|
|
37
81
|
"tasks");
|
|
38
82
|
}
|
|
39
|
-
|
|
40
|
-
|
|
83
|
+
/** Hash path segment for Console virtual routing (no leading `#`). */
|
|
84
|
+
export function buildConsoleHashPath(workspace, params = {}) {
|
|
85
|
+
const query = new URLSearchParams();
|
|
41
86
|
for (const [key, value] of Object.entries(params)) {
|
|
42
87
|
if (value?.trim())
|
|
43
88
|
query.set(key, value.trim());
|
|
44
89
|
}
|
|
45
|
-
|
|
90
|
+
const qs = query.toString();
|
|
91
|
+
const base = `/${workspace}`;
|
|
92
|
+
return qs ? `${base}?${qs}` : base;
|
|
93
|
+
}
|
|
94
|
+
export function buildWorkspaceHref(workspace, params = {}) {
|
|
95
|
+
return `#${buildConsoleHashPath(workspace, params)}`;
|
|
96
|
+
}
|
|
97
|
+
export function parseConsoleRoute(location) {
|
|
98
|
+
return (parseConsoleHashRoute(location.hash) ?? parseLegacyWorkspaceHref(location));
|
|
46
99
|
}
|
|
47
100
|
export function parseWorkspaceHref(location) {
|
|
48
|
-
|
|
49
|
-
const hash = location.hash.startsWith("#")
|
|
50
|
-
? location.hash.slice(1)
|
|
51
|
-
: location.hash;
|
|
52
|
-
const hashQuery = new URLSearchParams(hash);
|
|
53
|
-
const workspace = configuredWorkspace(query.get("workspace") ??
|
|
54
|
-
query.get("landing") ??
|
|
55
|
-
hashQuery.get("workspace"));
|
|
56
|
-
if (!workspace)
|
|
57
|
-
return null;
|
|
58
|
-
const read = (key) => query.get(key)?.trim() || undefined;
|
|
59
|
-
return {
|
|
60
|
-
workspace,
|
|
61
|
-
...(read("taskId") ? { taskId: read("taskId") } : {}),
|
|
62
|
-
...(read("dagRunId") ? { dagRunId: read("dagRunId") } : {}),
|
|
63
|
-
...(read("fromNodeId") ? { fromNodeId: read("fromNodeId") } : {}),
|
|
64
|
-
...(read("featureId") ? { featureId: read("featureId") } : {}),
|
|
65
|
-
};
|
|
101
|
+
return parseConsoleRoute(location);
|
|
66
102
|
}
|
|
@@ -178,6 +178,7 @@ export async function runConsoleDoctor(options) {
|
|
|
178
178
|
const pi = await probePiReadiness({
|
|
179
179
|
override: options.piReadinessProbe,
|
|
180
180
|
siblingControllerOk: Boolean(controllerIdentity && "packageVersion" in controllerIdentity),
|
|
181
|
+
repoRoot,
|
|
181
182
|
});
|
|
182
183
|
return {
|
|
183
184
|
ok: true,
|
|
@@ -22,6 +22,11 @@ export class NightAuxTicker {
|
|
|
22
22
|
this.deps = deps;
|
|
23
23
|
this.intervalSec = Math.max(15, Math.min(3600, deps.intervalSec ?? DEFAULT_CLOCK_INTERVAL_SEC));
|
|
24
24
|
this.now = deps.now ?? (() => new Date());
|
|
25
|
+
if (deps.autoStart !== false) {
|
|
26
|
+
this.enabled = true;
|
|
27
|
+
this.startTimer();
|
|
28
|
+
void this.fire("enable");
|
|
29
|
+
}
|
|
25
30
|
}
|
|
26
31
|
status() {
|
|
27
32
|
return {
|
|
@@ -9,6 +9,7 @@ export function buildOperatorSurfaceHealthV1(input) {
|
|
|
9
9
|
ok: true,
|
|
10
10
|
product: "loop-operator-console",
|
|
11
11
|
repoFingerprint: repoFingerprintV1(input.repoRoot),
|
|
12
|
+
repoName: input.repoRoot.split(/[\\/]/).filter(Boolean).pop() ?? "repository",
|
|
12
13
|
packageName,
|
|
13
14
|
packageVersion,
|
|
14
15
|
piReadiness: input.piReadiness,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* setup-required | ready | degraded — no pure form fallback.
|
|
4
4
|
*/
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { mapSnapshotAvailableModels, resolveHarnessPiExecutorMatrix, } from "./chat/model-resolver.js";
|
|
6
7
|
const SETUP_BLOCKED = Object.freeze([
|
|
7
8
|
"newTask",
|
|
8
9
|
"importPrd",
|
|
@@ -27,13 +28,6 @@ const DEGRADED_BLOCKED = Object.freeze([
|
|
|
27
28
|
"standaloneTaskRerun",
|
|
28
29
|
// new Interview may resume; brand-new apply/run still blocked
|
|
29
30
|
]);
|
|
30
|
-
/** Default Interview / DAG model matrix (provider, modelId). */
|
|
31
|
-
const INTERVIEW_MODEL_CANDIDATES = Object.freeze([
|
|
32
|
-
["openai", "gpt-5.5"],
|
|
33
|
-
["openai", "gpt-5.3-codex-spark"],
|
|
34
|
-
["zai", "glm-5.2"],
|
|
35
|
-
["zhipu", "glm-5.2"],
|
|
36
|
-
]);
|
|
37
31
|
export function classifyPiReadiness(probe) {
|
|
38
32
|
if (!probe.sdkAvailable ||
|
|
39
33
|
!probe.authReady ||
|
|
@@ -92,7 +86,8 @@ function hasProviderConfig(snapshot) {
|
|
|
92
86
|
* Kept inside worker/console (dynamic import) so Worker does not depend on
|
|
93
87
|
* executors; contract matches pi-sdk-executor 0.80.10.
|
|
94
88
|
*/
|
|
95
|
-
export async function discoverPiReadinessProbe() {
|
|
89
|
+
export async function discoverPiReadinessProbe(options) {
|
|
90
|
+
const repoRoot = options?.repoRoot ?? process.cwd();
|
|
96
91
|
let sdk;
|
|
97
92
|
try {
|
|
98
93
|
sdk = (await import("@earendil-works/pi-coding-agent"));
|
|
@@ -173,12 +168,16 @@ export async function discoverPiReadinessProbe() {
|
|
|
173
168
|
const snapshot = runtime.snapshot ?? {};
|
|
174
169
|
const availableCount = countAvailableModels(snapshot);
|
|
175
170
|
const providerConfigured = hasProviderConfig(snapshot);
|
|
176
|
-
const
|
|
171
|
+
const availableModels = mapSnapshotAvailableModels(snapshot);
|
|
172
|
+
const harnessMatrix = await resolveHarnessPiExecutorMatrix(repoRoot, availableModels);
|
|
173
|
+
const resolvedHarnessModels = [];
|
|
177
174
|
if (typeof runtime.getModel === "function") {
|
|
178
|
-
for (const
|
|
175
|
+
for (const row of harnessMatrix) {
|
|
176
|
+
if (!row.provider)
|
|
177
|
+
continue;
|
|
179
178
|
try {
|
|
180
|
-
if (runtime.getModel(provider, modelId)) {
|
|
181
|
-
|
|
179
|
+
if (runtime.getModel(row.provider, row.modelId)) {
|
|
180
|
+
resolvedHarnessModels.push(`${row.tier}/${row.modelId}@${row.provider}`);
|
|
182
181
|
}
|
|
183
182
|
}
|
|
184
183
|
catch {
|
|
@@ -186,8 +185,16 @@ export async function discoverPiReadinessProbe() {
|
|
|
186
185
|
}
|
|
187
186
|
}
|
|
188
187
|
}
|
|
188
|
+
const harnessMatrixSummary = harnessMatrix.length > 0
|
|
189
|
+
? harnessMatrix
|
|
190
|
+
.map((row) => {
|
|
191
|
+
const resolved = resolvedHarnessModels.some((entry) => entry.startsWith(`${row.tier}/${row.modelId}@`));
|
|
192
|
+
return `${row.tier}/${row.modelId}${resolved ? `@${row.provider}` : ":missing"}`;
|
|
193
|
+
})
|
|
194
|
+
.join(",")
|
|
195
|
+
: "none";
|
|
189
196
|
const authReady = availableCount > 0 || providerConfigured;
|
|
190
|
-
const modelsAvailable = availableCount > 0 ||
|
|
197
|
+
const modelsAvailable = availableCount > 0 || resolvedHarnessModels.length > 0;
|
|
191
198
|
let interviewSessionOk = false;
|
|
192
199
|
let sessionMessage;
|
|
193
200
|
if (!authReady || !modelsAvailable) {
|
|
@@ -200,9 +207,9 @@ export async function discoverPiReadinessProbe() {
|
|
|
200
207
|
}
|
|
201
208
|
else {
|
|
202
209
|
try {
|
|
203
|
-
sdk.SessionManager.inMemory(
|
|
210
|
+
sdk.SessionManager.inMemory(repoRoot);
|
|
204
211
|
interviewSessionOk = true;
|
|
205
|
-
sessionMessage = `Interview session preflight ok (${availableCount} available model(s);
|
|
212
|
+
sessionMessage = `Interview session preflight ok (${availableCount} available model(s); harnessPi=${harnessMatrixSummary})`;
|
|
206
213
|
}
|
|
207
214
|
catch (error) {
|
|
208
215
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -220,7 +227,7 @@ export async function discoverPiReadinessProbe() {
|
|
|
220
227
|
? `credentials/providers ready (available=${availableCount}, providerConfig=${providerConfigured})`
|
|
221
228
|
: "no available models and no stored/configured provider credentials (set Pi auth or provider API keys)",
|
|
222
229
|
modelsMessage: modelsAvailable
|
|
223
|
-
? `models ready (available=${availableCount},
|
|
230
|
+
? `models ready (available=${availableCount}, harnessPi=${harnessMatrixSummary})`
|
|
224
231
|
: "no available models for Interview",
|
|
225
232
|
sessionMessage,
|
|
226
233
|
},
|
|
@@ -235,7 +242,9 @@ export async function probePiReadiness(options) {
|
|
|
235
242
|
return buildPiReadinessReport(options.override);
|
|
236
243
|
}
|
|
237
244
|
try {
|
|
238
|
-
const probe = await discoverPiReadinessProbe(
|
|
245
|
+
const probe = await discoverPiReadinessProbe({
|
|
246
|
+
repoRoot: options?.repoRoot,
|
|
247
|
+
});
|
|
239
248
|
if (options?.siblingControllerOk && !probe.details) {
|
|
240
249
|
probe.details = {};
|
|
241
250
|
}
|
|
@@ -63,6 +63,7 @@ export async function createConsoleServer(options) {
|
|
|
63
63
|
const assessments = new AssessmentStore(appData);
|
|
64
64
|
const readiness = await probePiReadiness({
|
|
65
65
|
override: options.piReadinessProbe,
|
|
66
|
+
repoRoot,
|
|
66
67
|
});
|
|
67
68
|
let readinessCache = readiness;
|
|
68
69
|
const artifactRoot = path.join(appData.root, "client-artifacts", appData.fingerprint);
|
|
@@ -79,6 +80,7 @@ export async function createConsoleServer(options) {
|
|
|
79
80
|
const refreshReadiness = async () => {
|
|
80
81
|
readinessCache = await probePiReadiness({
|
|
81
82
|
override: options.piReadinessProbe,
|
|
83
|
+
repoRoot,
|
|
82
84
|
});
|
|
83
85
|
actionContext.readiness = readinessCache;
|
|
84
86
|
return readinessCache;
|