@tea-agent/loop-agent 0.32.1 → 0.33.0

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/executors/model-routing.js +14 -4
  3. package/dist/governance/manifest-types.js +34 -7
  4. package/dist/worker/console/chat/model-resolver.js +114 -34
  5. package/dist/worker/console/chat/workspace-landing.js +58 -22
  6. package/dist/worker/console/doctor.js +1 -0
  7. package/dist/worker/console/night-aux-ticker.js +5 -0
  8. package/dist/worker/console/pi-readiness.js +26 -17
  9. package/dist/worker/console/server.js +2 -0
  10. package/dist/worker/console/static/assets/index-3R-GT3a_.js +29 -0
  11. package/dist/worker/console/static/assets/index-B0EQt_yq.css +1 -0
  12. package/dist/worker/console/static/index.html +2 -2
  13. package/dist/worker/console/static-src/night/useNightBoard.js +0 -31
  14. package/dist/worker/observability/read-model.js +106 -0
  15. package/dist/worker/observe/routes.js +19 -1
  16. package/dist/worker/observe/static/api.js +42 -3
  17. package/dist/worker/observe/static/app.js +4 -0
  18. package/dist/worker/observe/static/constants.js +10 -0
  19. package/dist/worker/observe/static/custom-select.js +567 -0
  20. package/dist/worker/observe/static/index.html +47 -6
  21. package/dist/worker/observe/static/router.js +54 -1
  22. package/dist/worker/observe/static/state.js +20 -0
  23. package/dist/worker/observe/static/styles.css +618 -30
  24. package/dist/worker/observe/static/views/dag-inspector.js +20 -17
  25. package/dist/worker/observe/static/views/dag.js +136 -59
  26. package/dist/worker/observe/static/views/dags.js +877 -0
  27. package/dist/worker/observe/static/views/dashboard.js +15 -4
  28. package/dist/workflows/dag/init-hybrid.js +58 -29
  29. package/docs/templates/harness.schema.json +5 -0
  30. package/harness.json +4 -4
  31. package/package.json +1 -1
  32. package/dist/worker/console/static/assets/index-Bpa2qrc-.js +0 -29
  33. package/dist/worker/console/static/assets/index-BqfFDdnG.css +0 -1
package/CHANGELOG.md CHANGED
@@ -2,11 +2,57 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.33.0] - 2026-08-09
6
+
7
+ ### 重点更新
8
+
9
+ - Operator Console 工作区导航升级为 hash 路由,支持页面刷新保留状态及浏览器前进后退,并新增模型与设置页面入口
10
+ - 夜间辅助调度器(NightAuxTicker)改为自动启动,移除手动开关,简化操作流程
11
+ - Observe DAG 运行历史页交互全面优化,改进分页器布局、自定义选择器与复制反馈体验
12
+ - 执行器支持 provider/model 显式引用格式,消除运行时模型路由的歧义
13
+
14
+ ### 新增
15
+
16
+ - Operator Console 工作区导航新增 hash 虚拟路径(#/tasks、#/night、#/models、#/settings),支持刷新恢复与浏览器历史导航
17
+ - 新增模型(Models)与设置(Settings)页面入口
18
+ - 夜间辅助调度器(NightAuxTicker)现默认自动启动,无需手动开启即可维持夜间调度活跃度
19
+ - Observe 新增统一的自定义选择器菜单,支持键盘导航与自适应定位
20
+ - Observe 新增 DAG 运行历史分页功能
21
+
22
+ ### 改进
23
+
24
+ - Operator Console 工作区导航顺序调整,将对话(Tasks)置于首位
25
+ - 夜间任务面板的健康状态栏简化为纯文本状态展示,移除旧的手动开关
26
+ - Observe 夜页头部操作按钮样式统一
27
+ - Observe DAG 历史分页器视觉布局升级为精致的分段设计,并优化窄屏下的显示
28
+ - 执行器 Pi 层级支持 provider/model 引用格式,规范化解析字符串与对象两种配置写法,提升模型路由准确性
29
+
30
+ ### 修复
31
+
32
+ - 修复 Observe 自定义选择器键盘焦点异常的问题
33
+ - 修复 Observe DAG 历史分页器在窄视口下布局溢出的问题
34
+ - 修复 Observe 复制反馈按钮的边框样式显示异常,恢复无边框设计
35
+ - 优化 Observe DAG 节点错误摘要展示,将失败行备注压缩为紧凑的两行摘要,避免撑高表格
36
+
5
37
  ## [0.32.1] - 2026-08-09
6
38
 
39
+ ### 新增
40
+
41
+ - Observe:新增 DAG 运行历史页 `#/dags`:分页表格、hash 恢复 page/pageSize、手动刷新;只读 `GET /api/dag-runs?page=&pageSize=` 返回轻量列表,排除 dry-run/init-only,省略 nodes/edges 等重字段
42
+ - Dashboard「最近 DAG 运行」卡片右上增加「查看全部」入口,进入完整历史页;卡片仍保留最近 10 条
43
+
44
+ ### 修复
45
+
46
+ - Observe DAG 历史页:分页摘要与失败态使用本地固定可信文案;首次 Loading 与后续动态分页/刷新控件分离;双路径失败共用 aria-live;分页失败不污染列表;默认直连真实 `/api/dag-runs`
47
+ - Observe DAG 历史页 review 收口:取消在途 abort 中途仍式 GET 推进 requestSeq,防止旧响应覆盖缓存页;run-id 复制按钮独立 1.5s 成功计时;图标恢复;手动刷新替换当前页缓存;失效页码 bump prefetch generation 并拒绝陈旧预取写回;`totalPages>7` 页码窗口收紧为首页/末页/当前±1(无紧邻填补);`.dags-pagination.is-busy` 降级为上方 `opacity`
48
+ - Observe DAG 节点表格:失败行备注改为两行错误摘要,避免完整 errorPreview 撑高表格;完整错误仍在节点检查器输出中查看
49
+
7
50
  ### 改进
8
51
 
9
52
  - 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 上方)突出,工具过程卡默认退后
53
+ - Observe:DAG 历史 pageSize 与节点检查器「执行分次」改为统一自定义选择器(触发器 + listbox;键盘/ARIA/焦点/自适应定位);不改动 hash-only 分页、attempt raw key / 默认最新 / 节点切换重置语义
54
+ - Observe DAG 历史页:复制改为图标反馈(成功勾号 + 页面 aria-live;失败右下角短暂 Toast);数字分页器改为精致分段布局;hash 单一事实源;缓存 + AbortController + 相邻页低优先级预取,减少翻页双重加载与长时间等待
55
+
10
56
  - 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
57
 
12
58
  ### 修复
@@ -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").model,
36
- MED: resolveExecutorTierSelection(execConfig, "MED").model,
37
- HIGH: resolveExecutorTierSelection(execConfig, "HIGH").model,
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
- return { model: value };
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 model = value.model;
36
- if (typeof model !== "string" || model.length === 0 || model === "default") {
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 thinking = value.thinking;
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
- * Rationale: harness.json only stores a modelId per tier (e.g. "gpt-5.5"), not
10
- * a provider. The Pi SDK `getModel(provider, modelId)` requires a provider, so
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 modelIdFromTierValue(value) {
24
- if (typeof value === "string") {
25
- return value && value !== "default" ? value : undefined;
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
- if (!value || typeof value !== "object" || Array.isArray(value)) {
27
+ catch {
28
28
  return undefined;
29
29
  }
30
- const model = value.model;
31
- return typeof model === "string" && model && model !== "default"
32
- ? model
33
- : undefined;
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
- try {
43
- const raw = await readFile(path.join(repoRoot, "harness.json"), "utf-8");
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 modelId = await readHarnessPiModelTier(repoRoot, options?.tier ?? "MED");
108
- if (!modelId)
175
+ const selection = await readHarnessPiTierSelection(repoRoot, options?.tier ?? "MED");
176
+ if (!selection)
109
177
  return undefined;
110
- const entry = findProviderForModelId(modelId, available);
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
- export function buildWorkspaceHref(workspace, params = {}) {
40
- const query = new URLSearchParams({ workspace });
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
- return `/?${query.toString()}`;
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
- const query = new URLSearchParams(location.search);
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 {
@@ -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 resolvedInterviewModels = [];
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 [provider, modelId] of INTERVIEW_MODEL_CANDIDATES) {
175
+ for (const row of harnessMatrix) {
176
+ if (!row.provider)
177
+ continue;
179
178
  try {
180
- if (runtime.getModel(provider, modelId)) {
181
- resolvedInterviewModels.push(`${provider}/${modelId}`);
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 || resolvedInterviewModels.length > 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(process.cwd());
210
+ sdk.SessionManager.inMemory(repoRoot);
204
211
  interviewSessionOk = true;
205
- sessionMessage = `Interview session preflight ok (${availableCount} available model(s); matrix=${resolvedInterviewModels.join(",") || "none"})`;
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}, interviewMatrix=${resolvedInterviewModels.join(",") || "none"})`
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;