agentlas 1.0.12 → 1.0.14
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 +39 -0
- package/README.md +4 -2
- package/bin/agentlas.cjs +17 -3
- package/engine/agentlas-config.cjs +25 -18
- package/engine/agentlas-core-harness.cjs +14 -1
- package/engine/agentlas-i18n.cjs +2 -0
- package/engine/agentlas-input.cjs +1 -1
- package/engine/agentlas-memory-governance.cjs +10 -0
- package/engine/agentlas-onboard.cjs +22 -5
- package/engine/agentlas-sqlite-policy.cjs +18 -7
- package/engine/agentlas-workforce.cjs +136 -19
- package/engine/agentlas-workload-routing.cjs +32 -4
- package/engine/agentlas.cjs +8 -0
- package/engine/automation/daemon.cjs +67 -30
- package/engine/automation/schedule.cjs +16 -0
- package/engine/automation/store.cjs +70 -12
- package/engine/bootstrap-schema.sql +788 -53
- package/engine/commands/automation.cjs +8 -0
- package/engine/commands/chats.cjs +6 -1
- package/engine/commands/firm.cjs +7 -0
- package/engine/commands/help.cjs +23 -2
- package/engine/commands/hep-cloud.cjs +31 -0
- package/engine/commands/hep-hub.cjs +30 -0
- package/engine/commands/hep-local.cjs +32 -0
- package/engine/commands/hep-network.cjs +43 -0
- package/engine/commands/index.cjs +38 -7
- package/engine/commands/open.cjs +5 -1
- package/engine/commands/run.cjs +12 -2
- package/engine/commands/setup.cjs +12 -11
- package/engine/commands/storm.cjs +5 -9
- package/engine/commands/swarm.cjs +4 -4
- package/engine/commands/uninstall.cjs +36 -2
- package/engine/commands/version.cjs +29 -0
- package/engine/commands/workforce.cjs +10 -10
- package/engine/core/schema-ensure.cjs +75 -0
- package/engine/experience/variant.cjs +46 -2
- package/engine/hephaestus/runtime.cjs +7 -0
- package/engine/memory-cli/curate.cjs +3 -7
- package/engine/project/memory-context.cjs +3 -7
- package/engine/project/state.cjs +7 -6
- package/engine/sessions/orchestrator.cjs +41 -3
- package/engine/sessions/prompt.cjs +4 -7
- package/engine/sessions/session.cjs +32 -0
- package/engine/storm/swarm.cjs +12 -0
- package/engine/ui/palette.cjs +14 -2
- package/engine/ui/renderer.cjs +37 -0
- package/engine/ui/repl.cjs +7 -1
- package/engine/workforce/concurrency.cjs +41 -0
- package/package.json +1 -1
|
@@ -17,7 +17,15 @@ const SCHEMA_VERSION = 1;
|
|
|
17
17
|
const ALLOCATION_SCHEMA = "agentlas.workload-allocation.v1";
|
|
18
18
|
const TIERS = Object.freeze(["economy", "balanced", "frontier"]);
|
|
19
19
|
const TIER_RANK = Object.freeze({ economy: 0, balanced: 1, frontier: 2 });
|
|
20
|
+
// 알려진 값은 랭크 폴백으로만 쓴다 — "유효한가" 게이트로 쓰지 않는다. 2026-07-28 라이브
|
|
21
|
+
// 실측: codex의 모델 카탈로그가 프론티어 모델 하나에서 "ultra"(자동 위임) 리즌
|
|
22
|
+
// 레벨을 광고했는데, normalizeEffort가 이 튜플로 게이트를 걸면 parent-AI의 할당
|
|
23
|
+
// 결정 전체가 통째로 null(무효) 처리된다. 새 값이 나올 때마다 이 튜플을 고치지
|
|
24
|
+
// 않고, resolveEffort가 모델 자체 목록의 순서(=능력 랭크, provider 계약)를
|
|
25
|
+
// 신뢰하도록 뒤집었다. (구체적 모델 id는 이 파일에 절대 하드코딩하지 않는다 —
|
|
26
|
+
// 아래 doesNotMatch 계약 테스트 참고.)
|
|
20
27
|
const EFFORTS = Object.freeze(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
28
|
+
const EFFORT_TOKEN_RE = /^[a-z][a-z0-9-]{0,23}$/;
|
|
21
29
|
const CAPABILITIES = new Set(["code", "image", "tools", "long-context"]);
|
|
22
30
|
const PHASES = new Set(["plan", "delegate", "synthesize"]);
|
|
23
31
|
|
|
@@ -31,8 +39,9 @@ function normalizeTier(value) {
|
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
function normalizeEffort(value) {
|
|
42
|
+
// 신택스만 검증한다 — 화이트리스트 존재는 게이트로 쓰지 않는다(위 EFFORTS 주석).
|
|
34
43
|
const v = String(value || "").toLowerCase().trim();
|
|
35
|
-
return
|
|
44
|
+
return EFFORT_TOKEN_RE.test(v) ? v : null;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
function normalizeCapabilities(value) {
|
|
@@ -251,10 +260,29 @@ function resolveEffort(provider, requested, supported = []) {
|
|
|
251
260
|
const available = Array.isArray(supported) ? supported : [];
|
|
252
261
|
if (!available.length) return null;
|
|
253
262
|
if (available.includes(requested)) return requested;
|
|
254
|
-
|
|
263
|
+
// 이 모델이 정확히 그 값을 광고하지 않을 때만 여기로 온다. 랭크는 모델 자체
|
|
264
|
+
// 목록의 순서(=능력 랭크, provider 계약)를 최우선으로 쓰고, 알려진 7단계
|
|
265
|
+
// 표는 순서 정보가 없는 값의 폴백일 뿐이다 — 화이트리스트 게이트가 아니다.
|
|
266
|
+
// 미광고 known 값은 available 안에서 known-rank가 자기 이하인 마지막 항목
|
|
267
|
+
// 바로 뒤(소수 위치)에 끼워 넣는다 — 무조건 "목록 끝"으로 밀면 사실 available의
|
|
268
|
+
// 상위 항목들보다 낮은 값(예: low/xhigh/max만 있을 때의 "medium")이 전부 통과돼
|
|
269
|
+
// 요청보다 위로 에스컬레이션된다. 표에도 없는 완전 미지의 값만 +Infinity.
|
|
270
|
+
const rank = (value) => {
|
|
271
|
+
const own = available.indexOf(value);
|
|
272
|
+
if (own !== -1) return own;
|
|
273
|
+
const known = EFFORTS.indexOf(value);
|
|
274
|
+
if (known === -1) return Infinity;
|
|
275
|
+
let insertAfter = -1;
|
|
276
|
+
available.forEach((item, index) => {
|
|
277
|
+
const itemKnown = EFFORTS.indexOf(item);
|
|
278
|
+
if (itemKnown !== -1 && itemKnown <= known) insertAfter = index;
|
|
279
|
+
});
|
|
280
|
+
return insertAfter + 0.5;
|
|
281
|
+
};
|
|
282
|
+
const requestedRank = rank(requested);
|
|
255
283
|
const lower = available
|
|
256
|
-
.filter((item) =>
|
|
257
|
-
.sort((a, b) =>
|
|
284
|
+
.filter((item) => rank(item) <= requestedRank)
|
|
285
|
+
.sort((a, b) => rank(b) - rank(a))[0];
|
|
258
286
|
return lower || available[0] || null;
|
|
259
287
|
}
|
|
260
288
|
return null;
|
package/engine/agentlas.cjs
CHANGED
|
@@ -102,6 +102,14 @@ function nearestCommands(token, names) {
|
|
|
102
102
|
|
|
103
103
|
function main() {
|
|
104
104
|
const argv = process.argv.slice(2);
|
|
105
|
+
const helpRequested = argv.some((arg) => arg === "--help" || arg === "-h");
|
|
106
|
+
const helpCommand = argv.find((arg) => arg !== "--help" && arg !== "-h" && !arg.startsWith("-"));
|
|
107
|
+
if (helpRequested && helpCommand) {
|
|
108
|
+
const ctx = buildCtx();
|
|
109
|
+
const command = commands.resolveCommandName(helpCommand);
|
|
110
|
+
const code = require("./commands/help.cjs").runForCommand(ctx, command);
|
|
111
|
+
process.exit(typeof code === "number" ? code : 0);
|
|
112
|
+
}
|
|
105
113
|
// 옵션 정규화: -h/--help/-V/--version 은 하위 명령으로 변환
|
|
106
114
|
const normalized = argv.map((a) => {
|
|
107
115
|
if (a === "--help" || a === "-h") return "help";
|
|
@@ -111,6 +111,41 @@ function resolveTargetAgent(db, row) {
|
|
|
111
111
|
return bySlug;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* 이 터미널이 실행할 수 없는 계열 판정 — 실행 게이트와 due 셀렉션의 유일한 정본.
|
|
116
|
+
* store.runnerUnsupportedSql 의 SQL 술어와 반드시 같은 조건을 본다(갈리면 굶주림이
|
|
117
|
+
* 되돌아온다: 셀렉션이 담은 행을 실행기가 거부하면 그 행이 due 창을 영구 점유한다).
|
|
118
|
+
*
|
|
119
|
+
* Hub 타깃: 데스크탑은 정확 릴리스 핀 + Hub 런타임으로 실행한다
|
|
120
|
+
* (automation-scheduler.ts:573-630 hub_version_pin 게이트) — 터미널에는 그 실행
|
|
121
|
+
* 계층이 없으므로 위장 실행 금지, 정직 스킵.
|
|
122
|
+
* tool_mode 'browser'/'computer-use': 데스크탑은 Agentlas Browser/컴퓨터유즈 러너를
|
|
123
|
+
* 배선하고 권한 프리플라이트까지 건다(automation-scheduler.ts:619-625). 터미널
|
|
124
|
+
* 세션 계층에는 그 러너가 없다 — 평문 세션으로 돌리는 조용한 다운그레이드 대신
|
|
125
|
+
* 정직 스킵으로 Desktop 실행분(회차)을 그대로 남겨 둔다.
|
|
126
|
+
* @returns {{reason:string, message:string}|null}
|
|
127
|
+
*/
|
|
128
|
+
function runnerSkip(db, row, ko) {
|
|
129
|
+
if (row.target_type === "hub") {
|
|
130
|
+
return {
|
|
131
|
+
reason: "hub-target-unsupported",
|
|
132
|
+
message: ko
|
|
133
|
+
? `Hub 타깃 자동화는 터미널 데몬이 실행하지 않습니다(정확 릴리스 핀 실행은 Desktop 스케줄러 몫): ${row.name}`
|
|
134
|
+
: `Hub-target automations are not run by the terminal daemon (exact-release Hub execution belongs to the Desktop scheduler): ${row.name}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const rowToolMode = columnExists(db, "automations", "tool_mode") ? row.tool_mode : null;
|
|
138
|
+
if (rowToolMode === "browser" || rowToolMode === "computer-use") {
|
|
139
|
+
return {
|
|
140
|
+
reason: "tool-mode-unsupported",
|
|
141
|
+
message: ko
|
|
142
|
+
? `tool_mode '${rowToolMode}' 자동화는 터미널 데몬이 실행하지 않습니다(브라우저/컴퓨터유즈 러너는 Desktop 몫): ${row.name}`
|
|
143
|
+
: `tool_mode '${rowToolMode}' automations are not run by the terminal daemon (the browser/computer-use runner belongs to Desktop): ${row.name}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
114
149
|
/**
|
|
115
150
|
* 자동화 1건 실행(헤드리스). 권한은 자동화 행의 permission 열(있으면), 없으면 "write".
|
|
116
151
|
* opts:
|
|
@@ -131,26 +166,10 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
|
|
|
131
166
|
return { ok: false, skipped: true, reason: "disabled" };
|
|
132
167
|
}
|
|
133
168
|
// ── 터미널이 충실히 실행할 수 없는 계열은 리스를 잡지 않고 스킵한다 ──
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (row.target_type === "hub") {
|
|
139
|
-
ctx.err(ko
|
|
140
|
-
? `Hub 타깃 자동화는 터미널 데몬이 실행하지 않습니다(정확 릴리스 핀 실행은 Desktop 스케줄러 몫): ${row.name}`
|
|
141
|
-
: `Hub-target automations are not run by the terminal daemon (exact-release Hub execution belongs to the Desktop scheduler): ${row.name}`);
|
|
142
|
-
return { ok: false, skipped: true, reason: "hub-target-unsupported" };
|
|
143
|
-
}
|
|
144
|
-
// tool_mode 'browser'/'computer-use': 데스크탑은 Agentlas Browser/컴퓨터유즈
|
|
145
|
-
// 러너를 배선하고 권한 프리플라이트까지 건다(automation-scheduler.ts:619-625).
|
|
146
|
-
// 터미널 세션 계층에는 그 러너가 없다 — 평문 세션으로 돌리는 조용한 다운그레이드
|
|
147
|
-
// (위장 실행) 대신 정직 스킵으로 Desktop 실행분을 남겨 둔다.
|
|
148
|
-
const rowToolMode = columnExists(db, "automations", "tool_mode") ? row.tool_mode : null;
|
|
149
|
-
if (rowToolMode === "browser" || rowToolMode === "computer-use") {
|
|
150
|
-
ctx.err(ko
|
|
151
|
-
? `tool_mode '${rowToolMode}' 자동화는 터미널 데몬이 실행하지 않습니다(브라우저/컴퓨터유즈 러너는 Desktop 몫): ${row.name}`
|
|
152
|
-
: `tool_mode '${rowToolMode}' automations are not run by the terminal daemon (the browser/computer-use runner belongs to Desktop): ${row.name}`);
|
|
153
|
-
return { ok: false, skipped: true, reason: "tool-mode-unsupported" };
|
|
169
|
+
const unsupported = runnerSkip(db, row, ko);
|
|
170
|
+
if (unsupported) {
|
|
171
|
+
ctx.err(unsupported.message);
|
|
172
|
+
return { ok: false, skipped: true, reason: unsupported.reason };
|
|
154
173
|
}
|
|
155
174
|
if (!store.leaseSupported(db)) {
|
|
156
175
|
// 리스 열이 없는 DB에서는 Desktop 과의 배타성을 증명할 수 없다 — fail-closed.
|
|
@@ -268,17 +287,31 @@ async function daemonTick(ctx, db, opts = {}) {
|
|
|
268
287
|
const nowIso = (opts.now || new Date()).toISOString();
|
|
269
288
|
let due = [];
|
|
270
289
|
try {
|
|
271
|
-
|
|
290
|
+
// 실행 창은 "이 실행기가 실제로 실행할 수 있는" 행만 담는다 (runnable: true).
|
|
291
|
+
// 미지원 계열/남의 리스 행은 스킵돼도 next_run_at 이 전진하지 않으므로, 창에
|
|
292
|
+
// 담으면 시간순 LIMIT 5 의 머리를 영구 점유해 뒤의 자동화를 전부 굶긴다.
|
|
293
|
+
due = store.dueAutomations(db, nowIso, 5, { runnable: true });
|
|
272
294
|
} catch (e) {
|
|
273
295
|
ctx.err("Failed to query due automations: " + String((e && e.message) || e));
|
|
274
296
|
return 0;
|
|
275
297
|
}
|
|
298
|
+
// 미지원 계열은 실행 창에서 빠졌어도 존재 사실은 정직하게 알린다 — Desktop 몫으로
|
|
299
|
+
// 남겨둔 회차이므로 틱마다 반복하지 않고 데몬 수명당 1회만 고지한다.
|
|
300
|
+
if (opts.skipAnnounced) {
|
|
301
|
+
let deferred = [];
|
|
302
|
+
// 고지 상한 50 — 실행 창(5)과 달리 이 목록은 실행에 쓰이지 않으므로 넉넉해도 안전하다.
|
|
303
|
+
try { deferred = store.dueAutomations(db, nowIso, 50, { runnable: false }); } catch { /* best-effort */ }
|
|
304
|
+
for (const row of deferred) {
|
|
305
|
+
if (opts.skipAnnounced.has(row.id)) continue;
|
|
306
|
+
const skip = runnerSkip(db, row, ctx.lang === "ko");
|
|
307
|
+
if (!skip) continue;
|
|
308
|
+
ctx.err(skip.message);
|
|
309
|
+
opts.skipAnnounced.add(row.id);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
276
312
|
let ran = 0;
|
|
277
313
|
for (const row of due) {
|
|
278
314
|
if (opts.shouldStop && opts.shouldStop()) break;
|
|
279
|
-
// 터미널 미지원 계열(hub 타깃/browser/computer-use)은 due 로 계속 남는다 —
|
|
280
|
-
// Desktop 몫으로 남겨둔 것이므로 틱마다 같은 안내를 반복하지 않는다.
|
|
281
|
-
if (opts.skipAnnounced && opts.skipAnnounced.has(row.id)) continue;
|
|
282
315
|
ran += 1;
|
|
283
316
|
const result = await runAutomationOnce(ctx, db, row, {
|
|
284
317
|
advanceSchedule: true,
|
|
@@ -288,12 +321,15 @@ async function daemonTick(ctx, db, opts = {}) {
|
|
|
288
321
|
spawnImpl: opts.spawnImpl,
|
|
289
322
|
timeoutConfig: opts.timeoutConfig,
|
|
290
323
|
});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
) {
|
|
295
|
-
|
|
296
|
-
|
|
324
|
+
// 스킵은 회차를 소비하지 않는다 — 1회성 비활성화조차 건드리지 않는다.
|
|
325
|
+
// (미지원 계열은 Desktop 몫이고, lease 스킵은 지금 남이 돌리는 회차다. 셀렉션과
|
|
326
|
+
// 실행 사이의 리스 레이스로 스킵된 1회성 행을 여기서 꺼 버리면 그 실행은 영영 사라진다.)
|
|
327
|
+
if (result && result.skipped) {
|
|
328
|
+
if (
|
|
329
|
+
opts.skipAnnounced &&
|
|
330
|
+
(result.reason === "hub-target-unsupported" || result.reason === "tool-mode-unsupported")
|
|
331
|
+
) opts.skipAnnounced.add(row.id);
|
|
332
|
+
continue;
|
|
297
333
|
}
|
|
298
334
|
// 스케줄이 없는(1회성) 행이 남으면 재발화 방지 (v1과 동일).
|
|
299
335
|
if (!row.schedule || !schedule.nextAutomationRun(row)) {
|
|
@@ -340,5 +376,6 @@ module.exports = {
|
|
|
340
376
|
resolveTargetAgent,
|
|
341
377
|
automationContractState,
|
|
342
378
|
automationSessionChatId,
|
|
379
|
+
runnerSkip,
|
|
343
380
|
decodeRuntimeSelection,
|
|
344
381
|
};
|
|
@@ -102,6 +102,21 @@ function nextCronRun(cron, from = new Date(), timezone = null) {
|
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* IANA 타임존으로 해석 가능한지만 본다(빈 값 = 로컬존이므로 유효).
|
|
107
|
+
* nextCronRun 은 cron 파싱 실패와 잘못된 존을 똑같이 null 로 접어버리므로,
|
|
108
|
+
* 호출부가 "어느 필드가 틀렸는지"를 구분하려면 이 검사가 따로 필요하다.
|
|
109
|
+
*/
|
|
110
|
+
function isValidTimezone(timezone) {
|
|
111
|
+
if (!timezone) return true;
|
|
112
|
+
try {
|
|
113
|
+
new Intl.DateTimeFormat("en-US", { timeZone: String(timezone) });
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
105
120
|
function localTimezone() {
|
|
106
121
|
try { return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; } catch { return "UTC"; }
|
|
107
122
|
}
|
|
@@ -175,6 +190,7 @@ module.exports = {
|
|
|
175
190
|
cronField,
|
|
176
191
|
zonedDateParts,
|
|
177
192
|
nextCronRun,
|
|
193
|
+
isValidTimezone,
|
|
178
194
|
localTimezone,
|
|
179
195
|
legacyScheduleSpec,
|
|
180
196
|
nextAutomationRun,
|
|
@@ -163,11 +163,28 @@ function listRuns(db, limit = 15) {
|
|
|
163
163
|
).all(Math.max(1, limit));
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/**
|
|
167
|
+
* 다음 회차가 없어 자동화를 끝낼 때만 쓰는 단방향(끄기 전용) write.
|
|
168
|
+
*
|
|
169
|
+
* advanceAfterRun 은 실행 시작 시점의 row 스냅샷을 들고 있는데, 그 사이 사용자가
|
|
170
|
+
* 다른 터미널이나 Desktop 토글로 `automation off` 를 할 수 있다(에이전트 실행은
|
|
171
|
+
* 수 분~수십 분). 스냅샷의 enabled 를 되쓰면 그 끄기가 조용히 되돌아가고
|
|
172
|
+
* ("Disabled: … morning" 을 보고 exit 0 인데도) 다음 예정 시각에 또 발화한다.
|
|
173
|
+
* 그래서 enabled 는 "켜기"로는 절대 쓰지 않고, 종료 조건일 때만 0 으로 내린다.
|
|
174
|
+
*/
|
|
175
|
+
function disableExhausted(db, id) {
|
|
176
|
+
db.prepare("UPDATE automations SET enabled = 0 WHERE id = ?").run(id);
|
|
177
|
+
}
|
|
178
|
+
|
|
166
179
|
/**
|
|
167
180
|
* 실행 후 스케줄 북키핑 — v1 runAutomationOnce 의 성공/실패 분기와 동일:
|
|
168
181
|
* - 성공: run_count 증가. 실패: last_run_at 만 갱신(성공 카운트 오염 금지).
|
|
169
182
|
* - advanceSchedule(데몬 경로)일 때만 next_run_at 전진; 다음 시각이 없으면 비활성화.
|
|
170
183
|
* - run-now(advanceSchedule=false)는 스케줄을 건드리지 않는다(앱 advanceSchedule=false와 동일).
|
|
184
|
+
*
|
|
185
|
+
* enabled 는 여기서 스냅샷 값으로 되쓰지 않는다 — disableExhausted 주석 참조.
|
|
186
|
+
* (같은 이유로 성공/실패 두 분기 모두 같은 규칙을 따라야 한다: 한쪽만 고치면
|
|
187
|
+
* 실패로 끝난 실행이 여전히 끈 자동화를 부활시킨다.)
|
|
171
188
|
*/
|
|
172
189
|
function advanceAfterRun(db, row, { ok, advanceSchedule, ranAt = new Date() } = {}) {
|
|
173
190
|
const hasRunCount = columnExists(db, "automations", "run_count");
|
|
@@ -177,12 +194,13 @@ function advanceAfterRun(db, row, { ok, advanceSchedule, ranAt = new Date() } =
|
|
|
177
194
|
if (shouldAdvance) {
|
|
178
195
|
if (hasRunCount) {
|
|
179
196
|
db.prepare(
|
|
180
|
-
"UPDATE automations SET last_run_at = ?, run_count = run_count + 1, next_run_at =
|
|
181
|
-
).run(ranAt.toISOString(), advance ? advance.toISOString() : null,
|
|
197
|
+
"UPDATE automations SET last_run_at = ?, run_count = run_count + 1, next_run_at = ? WHERE id = ?",
|
|
198
|
+
).run(ranAt.toISOString(), advance ? advance.toISOString() : null, row.id);
|
|
182
199
|
} else {
|
|
183
|
-
db.prepare("UPDATE automations SET last_run_at = ?, next_run_at =
|
|
184
|
-
.run(ranAt.toISOString(), advance ? advance.toISOString() : null,
|
|
200
|
+
db.prepare("UPDATE automations SET last_run_at = ?, next_run_at = ? WHERE id = ?")
|
|
201
|
+
.run(ranAt.toISOString(), advance ? advance.toISOString() : null, row.id);
|
|
185
202
|
}
|
|
203
|
+
if (!advance) disableExhausted(db, row.id);
|
|
186
204
|
} else if (hasRunCount) {
|
|
187
205
|
db.prepare("UPDATE automations SET last_run_at = ?, run_count = run_count + 1 WHERE id = ?")
|
|
188
206
|
.run(ranAt.toISOString(), row.id);
|
|
@@ -191,25 +209,64 @@ function advanceAfterRun(db, row, { ok, advanceSchedule, ranAt = new Date() } =
|
|
|
191
209
|
}
|
|
192
210
|
// max_runs 도달 시 비활성화 (앱과 동일한 종료 조건).
|
|
193
211
|
if (row.max_runs && (row.run_count || 0) + 1 >= row.max_runs) {
|
|
194
|
-
db
|
|
212
|
+
disableExhausted(db, row.id);
|
|
195
213
|
return { advance, maxRunsReached: true };
|
|
196
214
|
}
|
|
197
215
|
} else if (shouldAdvance) {
|
|
198
|
-
db.prepare("UPDATE automations SET last_run_at = ?, next_run_at =
|
|
199
|
-
.run(ranAt.toISOString(), advance ? advance.toISOString() : null,
|
|
216
|
+
db.prepare("UPDATE automations SET last_run_at = ?, next_run_at = ? WHERE id = ?")
|
|
217
|
+
.run(ranAt.toISOString(), advance ? advance.toISOString() : null, row.id);
|
|
218
|
+
if (!advance) disableExhausted(db, row.id);
|
|
200
219
|
} else {
|
|
201
220
|
db.prepare("UPDATE automations SET last_run_at = ? WHERE id = ?").run(ranAt.toISOString(), row.id);
|
|
202
221
|
}
|
|
203
222
|
return { advance, maxRunsReached: false };
|
|
204
223
|
}
|
|
205
224
|
|
|
206
|
-
/**
|
|
207
|
-
|
|
225
|
+
/**
|
|
226
|
+
* 이 실행기가 실행할 수 없는 계열의 SQL 술어 — daemon.runnerSkip 게이트와 짝이다.
|
|
227
|
+
* (hub 타깃 / tool_mode browser·computer-use = Desktop 몫.)
|
|
228
|
+
* IFNULL 필수: SQLite 3치 논리에서 NULL 열이 있으면 NOT (…) 이 NULL 이 되어
|
|
229
|
+
* 멀쩡한 행까지 조용히 사라진다.
|
|
230
|
+
*/
|
|
231
|
+
function runnerUnsupportedSql(db) {
|
|
232
|
+
const parts = ["IFNULL(target_type,'') = 'hub'"];
|
|
233
|
+
if (columnExists(db, "automations", "tool_mode")) {
|
|
234
|
+
parts.push("IFNULL(tool_mode,'') IN ('browser','computer-use')");
|
|
235
|
+
}
|
|
236
|
+
return `(${parts.join(" OR ")})`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 데몬 폴링: 활성 + 스케줄 트리거 + next_run_at 도래분. trigger_type 열은 방어적.
|
|
241
|
+
*
|
|
242
|
+
* opts.runnable=true (데몬 실행 창): 이 실행기가 실행할 수 없는 행을 셀렉션에서 뺀다.
|
|
243
|
+
* 이유(굶주림 근본): 미지원 행(Desktop 몫)과 남이 리스를 쥔 행은 스킵돼도 next_run_at
|
|
244
|
+
* 이 전진하지 않는다 — 시간순 LIMIT n 창의 머리에 영구히 남아 뒤의 실행 가능한
|
|
245
|
+
* 자동화를 전부 굶긴다(데몬은 멀쩡해 보이고 아무것도 안 돈다). 실행 못 할 행은
|
|
246
|
+
* 애초에 창에 담지 않는 것이 유일한 근본 수리다 — 그 행의 회차는 그대로 보존된다.
|
|
247
|
+
* opts.runnable=false: 그 미지원 행만(고지용). 미지정: 예전 그대로 전체.
|
|
248
|
+
*/
|
|
249
|
+
function dueAutomations(db, nowIso = new Date().toISOString(), limit = 5, opts = {}) {
|
|
208
250
|
if (!tableExists(db, "automations")) return [];
|
|
209
|
-
const
|
|
251
|
+
const where = ["enabled = 1"];
|
|
252
|
+
const params = [];
|
|
253
|
+
if (columnExists(db, "automations", "trigger_type")) where.push("trigger_type = 'schedule'");
|
|
254
|
+
where.push("next_run_at IS NOT NULL", "next_run_at <= ?");
|
|
255
|
+
params.push(nowIso);
|
|
256
|
+
if (opts.runnable === true) {
|
|
257
|
+
where.push(`NOT ${runnerUnsupportedSql(db)}`);
|
|
258
|
+
if (leaseSupported(db)) {
|
|
259
|
+
// 남이 유효 리스를 쥔 행은 claimAutomation 이 반드시 실패한다 — 같은 굶주림 경로.
|
|
260
|
+
where.push("(claimed_at IS NULL OR claimed_at < ?)");
|
|
261
|
+
params.push(new Date(Date.parse(nowIso) - LEASE_TTL_MS).toISOString());
|
|
262
|
+
}
|
|
263
|
+
} else if (opts.runnable === false) {
|
|
264
|
+
where.push(runnerUnsupportedSql(db));
|
|
265
|
+
}
|
|
266
|
+
params.push(limit);
|
|
210
267
|
return db.prepare(
|
|
211
|
-
`SELECT * FROM automations WHERE
|
|
212
|
-
).all(
|
|
268
|
+
`SELECT * FROM automations WHERE ${where.join(" AND ")} ORDER BY next_run_at ASC LIMIT ?`,
|
|
269
|
+
).all(...params);
|
|
213
270
|
}
|
|
214
271
|
|
|
215
272
|
module.exports = {
|
|
@@ -228,4 +285,5 @@ module.exports = {
|
|
|
228
285
|
listRuns,
|
|
229
286
|
advanceAfterRun,
|
|
230
287
|
dueAutomations,
|
|
288
|
+
runnerUnsupportedSql,
|
|
231
289
|
};
|