dominds 1.24.3 → 1.25.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.
- package/README.md +48 -1
- package/README.zh.md +48 -1
- package/dist/apps/runtime.js +0 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +22 -0
- package/dist/dialog-display-state.d.ts +1 -1
- package/dist/dialog-display-state.js +20 -4
- package/dist/dialog-drive-work.js +5 -0
- package/dist/dialog-global-registry.d.ts +23 -8
- package/dist/dialog-global-registry.js +107 -38
- package/dist/dialog-instance-registry.js +7 -0
- package/dist/dialog-interruption.d.ts +2 -0
- package/dist/dialog-interruption.js +6 -0
- package/dist/dialog.d.ts +9 -9
- package/dist/dialog.js +43 -37
- package/dist/docs/dialog-system.md +7 -7
- package/dist/docs/dialog-system.zh.md +4 -4
- package/dist/docs/dlg-drive-algo.zh.md +539 -0
- package/dist/llm/gen/mock.js +2 -0
- package/dist/llm/kernel-driver/drive.js +133 -103
- package/dist/llm/kernel-driver/flow.js +767 -223
- package/dist/llm/kernel-driver/loop.js +207 -98
- package/dist/llm/kernel-driver/runtime.js +1 -1
- package/dist/llm/kernel-driver/sideDialog.js +14 -12
- package/dist/llm/kernel-driver/tellask-special.js +24 -23
- package/dist/llm/kernel-driver/types.d.ts +15 -28
- package/dist/llm/kernel-driver/types.js +0 -1
- package/dist/persistence-errors.d.ts +1 -1
- package/dist/persistence.d.ts +87 -17
- package/dist/persistence.js +631 -199
- package/dist/recovery/open-generation-recovery.d.ts +1 -0
- package/dist/recovery/{proceeding-drive.js → open-generation-recovery.js} +25 -25
- package/dist/recovery/reply-delivery-recovery.d.ts +3 -0
- package/dist/recovery/{reply-special.js → reply-delivery-recovery.js} +12 -12
- package/dist/runtime/driver-messages.d.ts +1 -0
- package/dist/runtime/driver-messages.js +7 -2
- package/dist/server/websocket-handler.d.ts +14 -0
- package/dist/server/websocket-handler.js +46 -7
- package/dist/server.js +4 -4
- package/dist/tools/builtins.js +4 -2
- package/dist/tools/prompts/skills/en/errors.md +2 -0
- package/dist/tools/prompts/skills/en/index.md +1 -0
- package/dist/tools/prompts/skills/en/tools.md +6 -0
- package/dist/tools/prompts/skills/zh/errors.md +2 -0
- package/dist/tools/prompts/skills/zh/index.md +1 -0
- package/dist/tools/prompts/skills/zh/tools.md +6 -0
- package/dist/tools/ripgrep.js +386 -44
- package/dist/tools/skills.d.ts +1 -0
- package/dist/tools/skills.js +81 -1
- package/dist/tools/team_mgmt.js +38 -2
- package/package.json +4 -4
- package/dist/docs/issues/tellask-background-continuation-live-bugs-2026-05-17.zh.md +0 -101
- package/dist/docs/tellask-background-continuation-refactor.zh.md +0 -1146
- package/dist/recovery/proceeding-drive.d.ts +0 -1
- package/dist/recovery/reply-special.d.ts +0 -3
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ Dominds is an AI-powered DevOps framework that creates autonomous agentic teams
|
|
|
69
69
|
|
|
70
70
|
### Prerequisites
|
|
71
71
|
|
|
72
|
-
- **Node.js (with npm bundled)**: Version 24.
|
|
72
|
+
- **Node.js (with npm bundled)**: Version 24.5+ LTS
|
|
73
73
|
- **npm (if used directly)**: Use the npm bundled with your installed Node.js 24 LTS; current minimum in this repo is `11.9.0`, and newer versions are allowed.
|
|
74
74
|
- **LLM provider configured for your team**: Dominds ships with a built-in provider catalog [main/llm/defaults.yaml](./main/llm/defaults.yaml) including Codex (ChatGPT) and Anthropic, plus several Anthropic-compatible endpoints (e.g. MiniMax, Z.ai, BigModel). You’ll need valid credentials for at least one provider.
|
|
75
75
|
- **pnpm (optional)**: Recommended only if you’re developing Dominds itself. Prefer enabling the pinned CLI once via `npm run setup:pm`. If Corepack shims cannot be enabled in your environment, install `pnpm@10` manually.
|
|
@@ -80,6 +80,53 @@ For repo development, initialize the package manager once after `nvm use --lts`:
|
|
|
80
80
|
npm run setup:pm
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
### Proxy Support
|
|
84
|
+
|
|
85
|
+
Dominds can use Node's built-in environment proxy handling on Windows, macOS, and Linux.
|
|
86
|
+
|
|
87
|
+
Default behavior:
|
|
88
|
+
|
|
89
|
+
- `DOMINDS_USE_ENV_PROXY` is enabled by default.
|
|
90
|
+
- When enabled, Dominds calls Node's runtime env-proxy initializer early in CLI startup.
|
|
91
|
+
- Dominds does not set or clear `NODE_USE_ENV_PROXY`; leave that variable for users who intentionally want Node startup-time or child-process behavior.
|
|
92
|
+
- Set `DOMINDS_USE_ENV_PROXY=0` to stop Dominds from auto-enabling env-proxy support.
|
|
93
|
+
|
|
94
|
+
Supported proxy variables:
|
|
95
|
+
|
|
96
|
+
- `HTTP_PROXY`
|
|
97
|
+
- `HTTPS_PROXY`
|
|
98
|
+
- `NO_PROXY`
|
|
99
|
+
- `http_proxy`
|
|
100
|
+
- `https_proxy`
|
|
101
|
+
- `no_proxy`
|
|
102
|
+
|
|
103
|
+
Practical guidance:
|
|
104
|
+
|
|
105
|
+
- Set both `HTTP_PROXY` and `HTTPS_PROXY` when your network uses the same proxy for both schemes. Lowercase forms also work.
|
|
106
|
+
- Use `NO_PROXY` for localhost, loopback, and internal domains that should bypass the proxy.
|
|
107
|
+
- Values should be standard proxy URLs, for example `http://proxy.company.com:8080`.
|
|
108
|
+
- If your shell already exports proxy variables, Dominds will read them too; clear unrelated proxy vars if you want a clean test environment.
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Windows PowerShell
|
|
114
|
+
$env:HTTP_PROXY = "http://proxy.company.com:8080"
|
|
115
|
+
$env:HTTPS_PROXY = "http://proxy.company.com:8080"
|
|
116
|
+
$env:NO_PROXY = "localhost,127.0.0.1,.corp.local"
|
|
117
|
+
dominds
|
|
118
|
+
|
|
119
|
+
# macOS / Linux shell
|
|
120
|
+
export HTTP_PROXY="http://proxy.company.com:8080"
|
|
121
|
+
export HTTPS_PROXY="http://proxy.company.com:8080"
|
|
122
|
+
export NO_PROXY="localhost,127.0.0.1,.corp.local"
|
|
123
|
+
dominds
|
|
124
|
+
|
|
125
|
+
# Disable Dominds env-proxy handling
|
|
126
|
+
export DOMINDS_USE_ENV_PROXY=0
|
|
127
|
+
dominds
|
|
128
|
+
```
|
|
129
|
+
|
|
83
130
|
### Install Dominds
|
|
84
131
|
|
|
85
132
|
```bash
|
package/README.zh.md
CHANGED
|
@@ -23,7 +23,7 @@ Dominds 是一款面向开发运作(DevOps)场景的智能体框架,它将
|
|
|
23
23
|
|
|
24
24
|
### 环境要求
|
|
25
25
|
|
|
26
|
-
- **Node.js(含 npm)**:版本 24.
|
|
26
|
+
- **Node.js(含 npm)**:版本 24.5+ LTS
|
|
27
27
|
- **npm(如直接使用)**:使用当前安装的 Node.js 24 LTS 自带 npm;本仓库当前最低要求为 `11.9.0`,更高版本也允许。
|
|
28
28
|
- **至少一个可用的 LLM 服务提供商**:Dominds 内置提供商目录(路径:[main/llm/defaults.yaml](./main/llm/defaults.yaml)),需为其中至少一个提供商配置有效凭证(通过环境变量设置)。
|
|
29
29
|
- **pnpm(可选)**:仅在开发 Dominds 本体时推荐使用。优先执行一次 `npm run setup:pm` 启用并缓存仓库固定的 pnpm;若你的环境无法启用 Corepack shim,再手动安装 `pnpm@10`。
|
|
@@ -34,6 +34,53 @@ Dominds 是一款面向开发运作(DevOps)场景的智能体框架,它将
|
|
|
34
34
|
npm run setup:pm
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
### 代理支持
|
|
38
|
+
|
|
39
|
+
Dominds 在 Windows、macOS、Linux 上都可以使用 Node 内建的环境变量代理能力。
|
|
40
|
+
|
|
41
|
+
默认行为:
|
|
42
|
+
|
|
43
|
+
- `DOMINDS_USE_ENV_PROXY` 默认启用。
|
|
44
|
+
- 启用后,Dominds 会在 CLI 启动早期调用 Node 的运行时环境代理初始化。
|
|
45
|
+
- Dominds 不会设置或清理 `NODE_USE_ENV_PROXY`;这个变量留给确实需要 Node 启动期行为或子进程继承语义的用户自己管理。
|
|
46
|
+
- 若要让 Dominds 不再自动开启环境代理支持,设置 `DOMINDS_USE_ENV_PROXY=0`。
|
|
47
|
+
|
|
48
|
+
支持的代理变量:
|
|
49
|
+
|
|
50
|
+
- `HTTP_PROXY`
|
|
51
|
+
- `HTTPS_PROXY`
|
|
52
|
+
- `NO_PROXY`
|
|
53
|
+
- `http_proxy`
|
|
54
|
+
- `https_proxy`
|
|
55
|
+
- `no_proxy`
|
|
56
|
+
|
|
57
|
+
实践建议:
|
|
58
|
+
|
|
59
|
+
- 若同一网络对 HTTP/HTTPS 都走同一代理,建议 `HTTP_PROXY` 和 `HTTPS_PROXY` 一起设置。小写变量名同样有效。
|
|
60
|
+
- `NO_PROXY` 用于排除 localhost、回环地址和内网域名。
|
|
61
|
+
- 代理值应使用标准 URL 形式,例如 `http://proxy.company.com:8080`。
|
|
62
|
+
- 如果你的 shell 已经导出了代理变量,Dominds 也会读取它们;想要干净的测试环境时,请清掉无关的代理变量。
|
|
63
|
+
|
|
64
|
+
示例:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Windows PowerShell
|
|
68
|
+
$env:HTTP_PROXY = "http://proxy.company.com:8080"
|
|
69
|
+
$env:HTTPS_PROXY = "http://proxy.company.com:8080"
|
|
70
|
+
$env:NO_PROXY = "localhost,127.0.0.1,.corp.local"
|
|
71
|
+
dominds
|
|
72
|
+
|
|
73
|
+
# macOS / Linux shell
|
|
74
|
+
export HTTP_PROXY="http://proxy.company.com:8080"
|
|
75
|
+
export HTTPS_PROXY="http://proxy.company.com:8080"
|
|
76
|
+
export NO_PROXY="localhost,127.0.0.1,.corp.local"
|
|
77
|
+
dominds
|
|
78
|
+
|
|
79
|
+
# 关闭 Dominds 的环境代理支持
|
|
80
|
+
export DOMINDS_USE_ENV_PROXY=0
|
|
81
|
+
dominds
|
|
82
|
+
```
|
|
83
|
+
|
|
37
84
|
### 安装 Dominds
|
|
38
85
|
|
|
39
86
|
```bash
|
package/dist/apps/runtime.js
CHANGED
|
@@ -98,9 +98,6 @@ async function resolveTargetDialog(sourceDlg, target) {
|
|
|
98
98
|
matches.push(dialog);
|
|
99
99
|
};
|
|
100
100
|
pushMatch(await ensureDialogLoadedBySelfId(sourceRoot, target.dialogId));
|
|
101
|
-
for (const loadedRoot of dialog_global_registry_1.globalDialogRegistry.getAll()) {
|
|
102
|
-
pushMatch(await ensureDialogLoadedBySelfId(loadedRoot, target.dialogId));
|
|
103
|
-
}
|
|
104
101
|
if (target.dialogId !== sourceRoot.id.rootId) {
|
|
105
102
|
pushMatch(await ensureMainDialogLoaded(target.dialogId));
|
|
106
103
|
}
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -62,8 +62,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
62
62
|
};
|
|
63
63
|
})();
|
|
64
64
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
65
|
+
exports.configureEnvProxySupport = configureEnvProxySupport;
|
|
65
66
|
exports.main = main;
|
|
66
67
|
const fs = __importStar(require("fs"));
|
|
68
|
+
const http = __importStar(require("node:http"));
|
|
67
69
|
const path = __importStar(require("path"));
|
|
68
70
|
const runtime_1 = require("./apps/runtime");
|
|
69
71
|
const dotenv_1 = require("./bootstrap/dotenv");
|
|
@@ -81,6 +83,24 @@ const update_1 = require("./cli/update");
|
|
|
81
83
|
const validate_team_def_1 = require("./cli/validate-team-def");
|
|
82
84
|
const webui_1 = require("./cli/webui");
|
|
83
85
|
require("./tools/builtins");
|
|
86
|
+
function configureEnvProxySupport() {
|
|
87
|
+
const domindsUseEnvProxy = process.env.DOMINDS_USE_ENV_PROXY?.trim();
|
|
88
|
+
if (domindsUseEnvProxy === '0') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const setGlobalProxyFromEnv = http.setGlobalProxyFromEnv;
|
|
93
|
+
if (typeof setGlobalProxyFromEnv !== 'function') {
|
|
94
|
+
console.error('Error: DOMINDS_USE_ENV_PROXY requires Node.js 24.5+ because http.setGlobalProxyFromEnv() is unavailable.');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
setGlobalProxyFromEnv(process.env);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
console.error('Error: invalid proxy environment configuration:', err instanceof Error ? err.message : String(err));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
84
104
|
function printHelp() {
|
|
85
105
|
console.log(`
|
|
86
106
|
Dominds CLI - AI-driven DevOps framework with persistent memory
|
|
@@ -163,6 +183,7 @@ async function main() {
|
|
|
163
183
|
}
|
|
164
184
|
}
|
|
165
185
|
(0, dotenv_1.loadRtwsDotenv)({ cwd: process.cwd() });
|
|
186
|
+
configureEnvProxySupport();
|
|
166
187
|
await runSubcommand('webui', []);
|
|
167
188
|
return;
|
|
168
189
|
}
|
|
@@ -214,6 +235,7 @@ async function main() {
|
|
|
214
235
|
// Precedence: `.env` then `.env.local` (later overwrites earlier), and both
|
|
215
236
|
// overwrite any existing process.env values.
|
|
216
237
|
(0, dotenv_1.loadRtwsDotenv)({ cwd: process.cwd() });
|
|
238
|
+
configureEnvProxySupport();
|
|
217
239
|
}
|
|
218
240
|
const shouldLoadApps = subcommand !== 'webui' &&
|
|
219
241
|
subcommand !== 'create' &&
|
|
@@ -51,7 +51,7 @@ export declare function broadcastDisplayStateMarker(dialogId: DialogID, marker:
|
|
|
51
51
|
reason?: DialogInterruptionReason;
|
|
52
52
|
}): void;
|
|
53
53
|
export declare function computeIdleDisplayState(dlg: Dialog): Promise<DialogDisplayState>;
|
|
54
|
-
export declare function refreshRunControlProjectionFromPersistenceFacts(dialogId: DialogID, trigger: 'resume_dialog' | 'resume_all' | 'run_control_snapshot' | 'active_callee_dispatches_changed' | 'q4h_changed'): Promise<DialogLatestFile | null>;
|
|
54
|
+
export declare function refreshRunControlProjectionFromPersistenceFacts(dialogId: DialogID, trigger: 'resume_dialog' | 'resume_all' | 'run_control_snapshot' | 'active_callee_dispatches_changed' | 'q4h_changed' | 'restart_reconciliation'): Promise<DialogLatestFile | null>;
|
|
55
55
|
export declare function reconcileDisplayStatesAfterRestart(): Promise<void>;
|
|
56
56
|
export declare function requestInterruptDialog(dialogId: DialogID, reason: StopRequestedReason): Promise<{
|
|
57
57
|
applied: boolean;
|
|
@@ -255,10 +255,12 @@ function clearActiveRun(dialogId, options) {
|
|
|
255
255
|
return;
|
|
256
256
|
}
|
|
257
257
|
clearQuarantiningMainDialogIfIdle(dialogId.rootId);
|
|
258
|
-
if (
|
|
258
|
+
if (options?.notifyBackendLoop !== false) {
|
|
259
259
|
dialog_global_registry_1.globalDialogRegistry.notifyActiveRunCleared(dialogId.rootId, {
|
|
260
260
|
source: 'dialog_display_state_active_run_clear',
|
|
261
|
-
reason:
|
|
261
|
+
reason: dialogId.selfId === dialogId.rootId
|
|
262
|
+
? 'root_active_run_cleared'
|
|
263
|
+
: 'sideDialog_active_run_cleared',
|
|
262
264
|
});
|
|
263
265
|
}
|
|
264
266
|
syncRunControlCountsAfterActiveRunChange('clear_active_run', dialogId);
|
|
@@ -774,7 +776,7 @@ async function reconcileDisplayStatesAfterRestart() {
|
|
|
774
776
|
}));
|
|
775
777
|
}
|
|
776
778
|
catch (err) {
|
|
777
|
-
log.warn('Failed to preserve
|
|
779
|
+
log.warn('Failed to preserve open-generation dialog for auto-drive after restart', err, {
|
|
778
780
|
dialogId: dialogId.valueOf(),
|
|
779
781
|
});
|
|
780
782
|
}
|
|
@@ -803,7 +805,21 @@ async function reconcileDisplayStatesAfterRestart() {
|
|
|
803
805
|
}));
|
|
804
806
|
}
|
|
805
807
|
catch (err) {
|
|
806
|
-
log.warn('Failed to reconcile
|
|
808
|
+
log.warn('Failed to reconcile open-generation dialog after restart', err, {
|
|
809
|
+
dialogId: dialogId.valueOf(),
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (latest.userWait?.kind === 'awaiting_user_answer' ||
|
|
815
|
+
latest.pendingRuntimePrompt !== undefined ||
|
|
816
|
+
latest.executionMarker?.kind === 'interrupted' ||
|
|
817
|
+
isNonIdleDisplayProjection(latest.displayState)) {
|
|
818
|
+
try {
|
|
819
|
+
await refreshRunControlProjectionFromPersistenceFacts(dialogId, 'restart_reconciliation');
|
|
820
|
+
}
|
|
821
|
+
catch (err) {
|
|
822
|
+
log.warn('Failed to refresh stale run-control projection after restart', err, {
|
|
807
823
|
dialogId: dialogId.valueOf(),
|
|
808
824
|
});
|
|
809
825
|
}
|
|
@@ -3,6 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.hasRecoverableGenerationBeyondFinalResponse = hasRecoverableGenerationBeyondFinalResponse;
|
|
4
4
|
exports.hasDurableDriveWork = hasDurableDriveWork;
|
|
5
5
|
const dialog_generation_run_1 = require("./dialog-generation-run");
|
|
6
|
+
// Backend drive work is a wake routing filter only. It must stay deliberately shallow: this module
|
|
7
|
+
// says "some persisted continuation source may need a handler", not "the dialog has business value
|
|
8
|
+
// to drive". Each concrete continuation handler must locally re-read and claim its own business
|
|
9
|
+
// facts before starting or continuing a generation. Do not add generic de-dup/fingerprint/cleanup
|
|
10
|
+
// policy here; that would merge unrelated business continuations into one spaghetti gate.
|
|
6
11
|
function hasResultArrivalTrigger(latest) {
|
|
7
12
|
return latest.nextStep.triggers.some((trigger) => trigger.kind === 'result_arrival');
|
|
8
13
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type MainDialog } from './dialog';
|
|
2
2
|
export type DriveTriggerEvent = Readonly<{
|
|
3
3
|
type: 'drive_trigger_evt';
|
|
4
|
-
action: '
|
|
4
|
+
action: 'queue_root_drive' | 'clear_root_drive_queue' | 'active_run_cleared';
|
|
5
5
|
rootId: string;
|
|
6
6
|
entryFound: boolean;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
previousDriveQueued: boolean | null;
|
|
8
|
+
nextDriveQueued: boolean;
|
|
9
9
|
source: string;
|
|
10
10
|
reason: string;
|
|
11
11
|
emittedAtMs: number;
|
|
@@ -16,7 +16,19 @@ export type DriveTriggerMeta = Readonly<{
|
|
|
16
16
|
}>;
|
|
17
17
|
declare class GlobalDialogRegistry {
|
|
18
18
|
private static instance;
|
|
19
|
+
/**
|
|
20
|
+
* Runtime-owned roots keyed by rootId.
|
|
21
|
+
*
|
|
22
|
+
* Do not add an API that enumerates this map for backend driving. A dialog becomes driveable only
|
|
23
|
+
* through an explicit business/runtime scheduling event (`queueRootDrive`, result arrival,
|
|
24
|
+
* active-run clear, etc.).
|
|
25
|
+
* Reintroducing "scan every loaded root and see what happens" makes hidden polling loops easy to
|
|
26
|
+
* create and obscures the business event that should own the next action.
|
|
27
|
+
*/
|
|
19
28
|
private readonly entries;
|
|
29
|
+
private readonly queuedRootIds;
|
|
30
|
+
private readonly queuedRootIdSet;
|
|
31
|
+
private queuedRootIdReadIndex;
|
|
20
32
|
private readonly lastDriveTriggerByRootId;
|
|
21
33
|
private readonly driveTriggerPubChan;
|
|
22
34
|
private driveTriggerSubChan;
|
|
@@ -24,16 +36,19 @@ declare class GlobalDialogRegistry {
|
|
|
24
36
|
get(rootId: string): MainDialog | undefined;
|
|
25
37
|
register(mainDialog: MainDialog): void;
|
|
26
38
|
unregister(rootId: string): void;
|
|
39
|
+
private enqueueRoot;
|
|
40
|
+
private compactQueuedRootsIfNeeded;
|
|
41
|
+
private compactSparseQueuedRootsIfNeeded;
|
|
27
42
|
private publishDriveTrigger;
|
|
28
43
|
waitForDriveTrigger(): Promise<DriveTriggerEvent>;
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
queueRootDrive(rootId: string, meta?: DriveTriggerMeta): void;
|
|
45
|
+
clearRootDriveQueue(rootId: string, meta?: DriveTriggerMeta): void;
|
|
31
46
|
notifyActiveRunCleared(rootId: string, meta?: DriveTriggerMeta): void;
|
|
32
47
|
noteActiveRunBlockedQueuedDrive(rootId: string): void;
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
hasPendingActiveRunClearedDrive(rootId: string): boolean;
|
|
49
|
+
isRootDriveQueued(rootId: string): boolean;
|
|
35
50
|
getLastDriveTrigger(rootId: string): DriveTriggerEvent | undefined;
|
|
36
|
-
|
|
51
|
+
consumeQueuedMainDialogs(): MainDialog[];
|
|
37
52
|
get size(): number;
|
|
38
53
|
}
|
|
39
54
|
export declare const globalDialogRegistry: GlobalDialogRegistry;
|
|
@@ -8,7 +8,19 @@ const persistence_1 = require("./persistence");
|
|
|
8
8
|
const log = (0, log_1.createLogger)('dialog-global-registry');
|
|
9
9
|
class GlobalDialogRegistry {
|
|
10
10
|
constructor() {
|
|
11
|
+
/**
|
|
12
|
+
* Runtime-owned roots keyed by rootId.
|
|
13
|
+
*
|
|
14
|
+
* Do not add an API that enumerates this map for backend driving. A dialog becomes driveable only
|
|
15
|
+
* through an explicit business/runtime scheduling event (`queueRootDrive`, result arrival,
|
|
16
|
+
* active-run clear, etc.).
|
|
17
|
+
* Reintroducing "scan every loaded root and see what happens" makes hidden polling loops easy to
|
|
18
|
+
* create and obscures the business event that should own the next action.
|
|
19
|
+
*/
|
|
11
20
|
this.entries = new Map();
|
|
21
|
+
this.queuedRootIds = [];
|
|
22
|
+
this.queuedRootIdSet = new Set();
|
|
23
|
+
this.queuedRootIdReadIndex = 0;
|
|
12
24
|
this.lastDriveTriggerByRootId = new Map();
|
|
13
25
|
this.driveTriggerPubChan = (0, evt_1.createPubChan)();
|
|
14
26
|
this.driveTriggerSubChan = (0, evt_1.createSubChan)(this.driveTriggerPubChan);
|
|
@@ -34,24 +46,24 @@ class GlobalDialogRegistry {
|
|
|
34
46
|
}
|
|
35
47
|
this.entries.set(mainDialog.id.rootId, {
|
|
36
48
|
mainDialog,
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
driveQueued: false,
|
|
50
|
+
activeRunClearedDrivePending: false,
|
|
39
51
|
});
|
|
40
52
|
void (async () => {
|
|
41
53
|
try {
|
|
42
54
|
const hasPendingNextStepTriggers = await persistence_1.DialogPersistence.hasPendingNextStepTriggers(mainDialog.id);
|
|
43
|
-
const
|
|
44
|
-
if (hasPendingNextStepTriggers ||
|
|
45
|
-
this.
|
|
55
|
+
const wakeQueueEntries = await persistence_1.DialogPersistence.loadWakeQueueEntries(mainDialog.id);
|
|
56
|
+
if (hasPendingNextStepTriggers || wakeQueueEntries.length > 0) {
|
|
57
|
+
this.queueRootDrive(mainDialog.id.rootId, {
|
|
46
58
|
source: 'dialog_registry_hydration',
|
|
47
59
|
reason: hasPendingNextStepTriggers
|
|
48
60
|
? 'persisted_next_step_triggers'
|
|
49
|
-
: '
|
|
61
|
+
: 'persisted_wake_queue',
|
|
50
62
|
});
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
catch (error) {
|
|
54
|
-
log.warn('Failed to hydrate persisted drive
|
|
66
|
+
log.warn('Failed to hydrate persisted root drive queue for registered main dialog', error, {
|
|
55
67
|
rootId: mainDialog.id.rootId,
|
|
56
68
|
selfId: mainDialog.id.selfId,
|
|
57
69
|
});
|
|
@@ -60,17 +72,53 @@ class GlobalDialogRegistry {
|
|
|
60
72
|
}
|
|
61
73
|
unregister(rootId) {
|
|
62
74
|
this.entries.delete(rootId);
|
|
75
|
+
this.queuedRootIdSet.delete(rootId);
|
|
63
76
|
this.lastDriveTriggerByRootId.delete(rootId);
|
|
64
77
|
(0, dialog_1.scheduleGlobalDialogMutexCleanupForRoot)(rootId);
|
|
65
78
|
}
|
|
79
|
+
enqueueRoot(rootId) {
|
|
80
|
+
if (this.queuedRootIdSet.has(rootId)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.queuedRootIdSet.add(rootId);
|
|
84
|
+
this.queuedRootIds.push(rootId);
|
|
85
|
+
}
|
|
86
|
+
compactQueuedRootsIfNeeded() {
|
|
87
|
+
if (this.queuedRootIdReadIndex === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (this.queuedRootIdReadIndex < 256 &&
|
|
91
|
+
this.queuedRootIdReadIndex * 2 < this.queuedRootIds.length) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.queuedRootIds.splice(0, this.queuedRootIdReadIndex);
|
|
95
|
+
this.queuedRootIdReadIndex = 0;
|
|
96
|
+
}
|
|
97
|
+
compactSparseQueuedRootsIfNeeded() {
|
|
98
|
+
if (this.queuedRootIds.length < 512) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (this.queuedRootIdSet.size * 2 >= this.queuedRootIds.length - this.queuedRootIdReadIndex) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const compacted = [];
|
|
105
|
+
for (let index = this.queuedRootIdReadIndex; index < this.queuedRootIds.length; index += 1) {
|
|
106
|
+
const rootId = this.queuedRootIds[index];
|
|
107
|
+
if (rootId !== undefined && this.queuedRootIdSet.has(rootId)) {
|
|
108
|
+
compacted.push(rootId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.queuedRootIds.splice(0, this.queuedRootIds.length, ...compacted);
|
|
112
|
+
this.queuedRootIdReadIndex = 0;
|
|
113
|
+
}
|
|
66
114
|
publishDriveTrigger(args) {
|
|
67
115
|
const trigger = {
|
|
68
116
|
type: 'drive_trigger_evt',
|
|
69
117
|
action: args.action,
|
|
70
118
|
rootId: args.rootId,
|
|
71
119
|
entryFound: args.entryFound,
|
|
72
|
-
|
|
73
|
-
|
|
120
|
+
previousDriveQueued: args.previousDriveQueued,
|
|
121
|
+
nextDriveQueued: args.nextDriveQueued,
|
|
74
122
|
source: args.meta.source,
|
|
75
123
|
reason: args.meta.reason,
|
|
76
124
|
emittedAtMs: Date.now(),
|
|
@@ -88,44 +136,47 @@ class GlobalDialogRegistry {
|
|
|
88
136
|
this.driveTriggerSubChan = (0, evt_1.createSubChan)(this.driveTriggerPubChan);
|
|
89
137
|
}
|
|
90
138
|
}
|
|
91
|
-
|
|
139
|
+
queueRootDrive(rootId, meta) {
|
|
92
140
|
const triggerMeta = meta ?? {
|
|
93
141
|
source: 'unknown',
|
|
94
142
|
reason: 'unspecified',
|
|
95
143
|
};
|
|
96
144
|
const entry = this.entries.get(rootId);
|
|
97
|
-
const
|
|
145
|
+
const previousDriveQueued = entry ? entry.driveQueued : null;
|
|
98
146
|
if (entry) {
|
|
99
|
-
entry.
|
|
100
|
-
// A fresh queueing
|
|
101
|
-
entry.
|
|
147
|
+
entry.driveQueued = true;
|
|
148
|
+
// A fresh root drive queueing supersedes any earlier "drive once active run clears" debt.
|
|
149
|
+
entry.activeRunClearedDrivePending = false;
|
|
150
|
+
this.enqueueRoot(rootId);
|
|
102
151
|
}
|
|
103
152
|
this.publishDriveTrigger({
|
|
104
|
-
action: '
|
|
153
|
+
action: 'queue_root_drive',
|
|
105
154
|
rootId,
|
|
106
155
|
entryFound: entry !== undefined,
|
|
107
|
-
|
|
108
|
-
|
|
156
|
+
previousDriveQueued,
|
|
157
|
+
nextDriveQueued: true,
|
|
109
158
|
meta: triggerMeta,
|
|
110
159
|
});
|
|
111
160
|
}
|
|
112
|
-
|
|
161
|
+
clearRootDriveQueue(rootId, meta) {
|
|
113
162
|
const triggerMeta = meta ?? {
|
|
114
163
|
source: 'unknown',
|
|
115
164
|
reason: 'unspecified',
|
|
116
165
|
};
|
|
117
166
|
const entry = this.entries.get(rootId);
|
|
118
|
-
const
|
|
167
|
+
const previousDriveQueued = entry ? entry.driveQueued : null;
|
|
119
168
|
if (entry) {
|
|
120
|
-
entry.
|
|
121
|
-
entry.
|
|
169
|
+
entry.driveQueued = false;
|
|
170
|
+
entry.activeRunClearedDrivePending = false;
|
|
171
|
+
this.queuedRootIdSet.delete(rootId);
|
|
172
|
+
this.compactSparseQueuedRootsIfNeeded();
|
|
122
173
|
}
|
|
123
174
|
this.publishDriveTrigger({
|
|
124
|
-
action: '
|
|
175
|
+
action: 'clear_root_drive_queue',
|
|
125
176
|
rootId,
|
|
126
177
|
entryFound: entry !== undefined,
|
|
127
|
-
|
|
128
|
-
|
|
178
|
+
previousDriveQueued,
|
|
179
|
+
nextDriveQueued: false,
|
|
129
180
|
meta: triggerMeta,
|
|
130
181
|
});
|
|
131
182
|
}
|
|
@@ -138,39 +189,57 @@ class GlobalDialogRegistry {
|
|
|
138
189
|
if (!entry) {
|
|
139
190
|
return;
|
|
140
191
|
}
|
|
141
|
-
if (!entry.
|
|
142
|
-
entry.
|
|
192
|
+
if (!entry.activeRunClearedDrivePending) {
|
|
193
|
+
entry.activeRunClearedDrivePending = false;
|
|
143
194
|
return;
|
|
144
195
|
}
|
|
145
|
-
const
|
|
146
|
-
entry.
|
|
196
|
+
const currentDriveQueued = entry.driveQueued;
|
|
197
|
+
entry.activeRunClearedDrivePending = false;
|
|
198
|
+
entry.driveQueued = true;
|
|
199
|
+
this.enqueueRoot(rootId);
|
|
147
200
|
this.publishDriveTrigger({
|
|
148
201
|
action: 'active_run_cleared',
|
|
149
202
|
rootId,
|
|
150
203
|
entryFound: true,
|
|
151
|
-
|
|
152
|
-
|
|
204
|
+
previousDriveQueued: currentDriveQueued,
|
|
205
|
+
nextDriveQueued: entry.driveQueued,
|
|
153
206
|
meta: triggerMeta,
|
|
154
207
|
});
|
|
155
208
|
}
|
|
156
209
|
noteActiveRunBlockedQueuedDrive(rootId) {
|
|
157
210
|
const entry = this.entries.get(rootId);
|
|
158
|
-
if (!entry
|
|
211
|
+
if (!entry) {
|
|
159
212
|
return;
|
|
160
213
|
}
|
|
161
|
-
entry.
|
|
214
|
+
entry.activeRunClearedDrivePending = true;
|
|
162
215
|
}
|
|
163
|
-
|
|
164
|
-
return this.entries.get(rootId)?.
|
|
216
|
+
hasPendingActiveRunClearedDrive(rootId) {
|
|
217
|
+
return this.entries.get(rootId)?.activeRunClearedDrivePending === true;
|
|
165
218
|
}
|
|
166
|
-
|
|
167
|
-
return this.entries.get(rootId)?.
|
|
219
|
+
isRootDriveQueued(rootId) {
|
|
220
|
+
return this.entries.get(rootId)?.driveQueued === true;
|
|
168
221
|
}
|
|
169
222
|
getLastDriveTrigger(rootId) {
|
|
170
223
|
return this.lastDriveTriggerByRootId.get(rootId);
|
|
171
224
|
}
|
|
172
|
-
|
|
173
|
-
|
|
225
|
+
consumeQueuedMainDialogs() {
|
|
226
|
+
const queued = [];
|
|
227
|
+
while (this.queuedRootIdReadIndex < this.queuedRootIds.length) {
|
|
228
|
+
const rootId = this.queuedRootIds[this.queuedRootIdReadIndex];
|
|
229
|
+
this.queuedRootIdReadIndex += 1;
|
|
230
|
+
if (rootId === undefined || !this.queuedRootIdSet.has(rootId)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
this.queuedRootIdSet.delete(rootId);
|
|
234
|
+
const entry = this.entries.get(rootId);
|
|
235
|
+
if (!entry) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
entry.driveQueued = false;
|
|
239
|
+
queued.push(entry.mainDialog);
|
|
240
|
+
}
|
|
241
|
+
this.compactQueuedRootsIfNeeded();
|
|
242
|
+
return queued;
|
|
174
243
|
}
|
|
175
244
|
get size() {
|
|
176
245
|
return this.entries.size;
|
|
@@ -35,6 +35,13 @@ async function resolvePendingRuntimePromptForRestore(args) {
|
|
|
35
35
|
if (alreadyPersisted) {
|
|
36
36
|
if (args.status === 'running') {
|
|
37
37
|
await persistence_1.DialogPersistence.clearPendingRuntimePrompt(args.dialogId, pending.msgId, args.status);
|
|
38
|
+
const latestAfterClear = await persistence_1.DialogPersistence.loadDialogLatest(args.dialogId, args.status);
|
|
39
|
+
if (latestAfterClear) {
|
|
40
|
+
await persistence_1.DialogPersistence.syncWakeQueueForDialogLatest(args.dialogId, latestAfterClear, args.status);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
await persistence_1.DialogPersistence.removeWakeQueueEntriesForDialog(args.dialogId, args.status);
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
return { pendingRuntimePrompt: undefined };
|
|
40
47
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DialogInterruptionReason } from '@longrun-ai/kernel/types/display-state';
|
|
2
|
+
import type { DialogExecutionMarker } from '@longrun-ai/kernel/types/storage';
|
|
2
3
|
/**
|
|
3
4
|
* Decides whether a finalized stopped dialog should expose manual Continue.
|
|
4
5
|
*
|
|
@@ -17,3 +18,4 @@ import type { DialogInterruptionReason } from '@longrun-ai/kernel/types/display-
|
|
|
17
18
|
*/
|
|
18
19
|
export declare function isInterruptionReasonManualResumeEligible(reason: DialogInterruptionReason): boolean;
|
|
19
20
|
export declare function doesInterruptionReasonRequireExplicitResume(reason: DialogInterruptionReason): boolean;
|
|
21
|
+
export declare function isInterruptedDialogBlockedWithoutExplicitResume(marker: DialogExecutionMarker | undefined, explicitResumeAuthorized: boolean): boolean;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isInterruptionReasonManualResumeEligible = isInterruptionReasonManualResumeEligible;
|
|
4
4
|
exports.doesInterruptionReasonRequireExplicitResume = doesInterruptionReasonRequireExplicitResume;
|
|
5
|
+
exports.isInterruptedDialogBlockedWithoutExplicitResume = isInterruptedDialogBlockedWithoutExplicitResume;
|
|
5
6
|
/**
|
|
6
7
|
* Decides whether a finalized stopped dialog should expose manual Continue.
|
|
7
8
|
*
|
|
@@ -53,3 +54,8 @@ function doesInterruptionReasonRequireExplicitResume(reason) {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
function isInterruptedDialogBlockedWithoutExplicitResume(marker, explicitResumeAuthorized) {
|
|
58
|
+
return (marker?.kind === 'interrupted' &&
|
|
59
|
+
doesInterruptionReasonRequireExplicitResume(marker.reason) &&
|
|
60
|
+
!explicitResumeAuthorized);
|
|
61
|
+
}
|