agentlas 1.0.48 → 1.0.49
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 +4 -2
- package/bin/agentlas.cjs +10 -0
- package/engine/agentlas-sqlite-policy.cjs +9 -0
- package/engine/bootstrap-schema.sql +1037 -1010
- package/engine/cloud-assets/package.cjs +11 -3
- package/engine/cloud-assets/upload-scan-catalog.generated.cjs +102 -0
- package/engine/core/db.cjs +53 -1
- package/engine/core/desktop-core.cjs +54 -4
- package/engine/core/store-schema.cjs +119 -0
- package/engine/project/memory-context.cjs +7 -0
- package/engine/workforce/deps.cjs +7 -0
- package/package.json +1 -1
|
@@ -37,14 +37,22 @@ const {
|
|
|
37
37
|
normalizeCloudAssetDescriptor,
|
|
38
38
|
} = require("../hub/install.cjs");
|
|
39
39
|
const { SECRET_PATTERNS } = require("../agentlas-secret-patterns.cjs");
|
|
40
|
+
const {
|
|
41
|
+
SECRET_SCAN_TEXT_EXTENSIONS,
|
|
42
|
+
UPLOAD_AGENT_DEFINITION_FILES,
|
|
43
|
+
UPLOAD_SKIP_DIRECTORIES,
|
|
44
|
+
} = require("./upload-scan-catalog.generated.cjs");
|
|
40
45
|
const { userDataDir } = require("../core/paths.cjs");
|
|
41
46
|
const state = require("./state.cjs");
|
|
42
47
|
const { cargoSearchAgents } = require("./cargo.cjs");
|
|
43
48
|
const cas = require("./cas.cjs");
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
// 업로드/시크릿 스캔 어휘는 업로드 계약에서 온다. 세 제품이 각자 손으로 적어
|
|
51
|
+
// 두었고 이미 갈려 있었다 — .bat/.cmd/.jsx 는 데스크탑만 스캔해서 서버 스캔과
|
|
52
|
+
// 터미널 업로드가 그 파일들을 아예 열지 않았다.
|
|
53
|
+
const CLOUD_TEXT_EXTS = new Set(SECRET_SCAN_TEXT_EXTENSIONS);
|
|
54
|
+
const CLOUD_AGENT_FILES = new Set(UPLOAD_AGENT_DEFINITION_FILES);
|
|
55
|
+
const CLOUD_SKIP_DIRS = new Set(UPLOAD_SKIP_DIRECTORIES);
|
|
48
56
|
const CLOUD_BLOCKED_FILE_RE = [/^\.env(?:\..*)?$/i, /^id_rsa(?:\.pub)?$/i, /^credentials(?:\..*)?$/i, /^secrets?(?:\..*)?$/i, /^cloud-asset-state\.v1\.json$/i, /(?:^|[._-])service-account(?:[._-]|$)/i, /\.(?:key|pem|p12|pfx|mobileprovision)$/i];
|
|
49
57
|
const CLOUD_ROUTING_CARD_PATH = ".agentlas/routing-card.json";
|
|
50
58
|
const CLOUD_ROUTING_CARD_CAPABILITY_RE = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// ⚠️ GENERATED FILE — do not hand-edit; the next generation erases your change.
|
|
2
|
+
// 정본: agentlas/AgentsAtlas/app/src/lib/agentlas-cloud/upload-scan-catalog.json
|
|
3
|
+
// 생성: (agentlas/AgentsAtlas/app) node scripts/gen-upload-scan-catalog.mjs
|
|
4
|
+
//
|
|
5
|
+
// Cloud-agent upload + secret-scan catalog. Three products used to restate
|
|
6
|
+
// this by hand and drifted; the server-side scan was the one that lost.
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const SECRET_SCAN_TEXT_EXTENSIONS = Object.freeze([
|
|
11
|
+
".bat",
|
|
12
|
+
".cfg",
|
|
13
|
+
".cjs",
|
|
14
|
+
".cmd",
|
|
15
|
+
".conf",
|
|
16
|
+
".config",
|
|
17
|
+
".css",
|
|
18
|
+
".csv",
|
|
19
|
+
".env",
|
|
20
|
+
".html",
|
|
21
|
+
".ini",
|
|
22
|
+
".js",
|
|
23
|
+
".json",
|
|
24
|
+
".jsonl",
|
|
25
|
+
".jsx",
|
|
26
|
+
".md",
|
|
27
|
+
".mjs",
|
|
28
|
+
".properties",
|
|
29
|
+
".ps1",
|
|
30
|
+
".psd1",
|
|
31
|
+
".psm1",
|
|
32
|
+
".py",
|
|
33
|
+
".sh",
|
|
34
|
+
".toml",
|
|
35
|
+
".ts",
|
|
36
|
+
".tsx",
|
|
37
|
+
".txt",
|
|
38
|
+
".xml",
|
|
39
|
+
".yaml",
|
|
40
|
+
".yml",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const UPLOAD_SKIP_DIRECTORIES = Object.freeze([
|
|
44
|
+
".git",
|
|
45
|
+
".next",
|
|
46
|
+
".studio-runtime",
|
|
47
|
+
".turbo",
|
|
48
|
+
"build",
|
|
49
|
+
"coverage",
|
|
50
|
+
"dist",
|
|
51
|
+
"node_modules",
|
|
52
|
+
"out",
|
|
53
|
+
"release",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const AGENT_DEFINITION_FILES = Object.freeze([
|
|
57
|
+
"AGENT.md",
|
|
58
|
+
"AGENTS.md",
|
|
59
|
+
"CLAUDE.md",
|
|
60
|
+
"GEMINI.md",
|
|
61
|
+
"README.md",
|
|
62
|
+
"agent.md",
|
|
63
|
+
"manifest.md",
|
|
64
|
+
"system-prompt.md",
|
|
65
|
+
"system.md",
|
|
66
|
+
"soul.md",
|
|
67
|
+
"prompt.md",
|
|
68
|
+
"persona.md",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const UPLOAD_AGENT_DEFINITION_FILES = Object.freeze([
|
|
72
|
+
"AGENT.md",
|
|
73
|
+
"AGENTS.md",
|
|
74
|
+
"CLAUDE.md",
|
|
75
|
+
"GEMINI.md",
|
|
76
|
+
"README.md",
|
|
77
|
+
"agent.md",
|
|
78
|
+
"manifest.md",
|
|
79
|
+
"system-prompt.md",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const FOLDER_SCAN_AGENT_DEFINITION_FILES = Object.freeze([
|
|
83
|
+
"AGENT.md",
|
|
84
|
+
"AGENTS.md",
|
|
85
|
+
"CLAUDE.md",
|
|
86
|
+
"GEMINI.md",
|
|
87
|
+
"agent.md",
|
|
88
|
+
"manifest.md",
|
|
89
|
+
"system-prompt.md",
|
|
90
|
+
"system.md",
|
|
91
|
+
"soul.md",
|
|
92
|
+
"prompt.md",
|
|
93
|
+
"persona.md",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
SECRET_SCAN_TEXT_EXTENSIONS,
|
|
98
|
+
UPLOAD_SKIP_DIRECTORIES,
|
|
99
|
+
AGENT_DEFINITION_FILES,
|
|
100
|
+
UPLOAD_AGENT_DEFINITION_FILES,
|
|
101
|
+
FOLDER_SCAN_AGENT_DEFINITION_FILES,
|
|
102
|
+
};
|
package/engine/core/db.cjs
CHANGED
|
@@ -7,11 +7,17 @@
|
|
|
7
7
|
* 스키마 소유권: 정본 스키마는 데스크탑 앱. 터미널은 bootstrap-schema.sql로
|
|
8
8
|
* 첫 부트스트랩만 하고(런처가 수행), 이후 마이그레이션은 앱이 한다.
|
|
9
9
|
* 터미널은 "있으면 쓰는" 방어적 열 확인(columnExists)으로만 전진 호환한다.
|
|
10
|
+
*
|
|
11
|
+
* ★2026-08-18: 위 문단은 계약이었지 배선이 아니었다. 이 파일은 user_version 을 **한 번도
|
|
12
|
+
* 읽지 않으면서** seedBuiltins 로 BEGIN IMMEDIATE 쓰기 트랜잭션을 열었다 — 검사도 거절도
|
|
13
|
+
* 없이 그냥 진행. 이제 openDb() 가 core/store-schema.cjs 로 버전을 확인하고, 낮으면
|
|
14
|
+
* 정직하게 거절한다(승급은 여전히 데스크탑의 일이다).
|
|
10
15
|
*/
|
|
11
16
|
const fs = require("node:fs");
|
|
12
17
|
const { configureSqliteConnection } = require("../agentlas-sqlite-policy.cjs");
|
|
13
18
|
const { parseSemVer, compareSemVer } = require("../semver.cjs");
|
|
14
19
|
const { dbPath } = require("./paths.cjs");
|
|
20
|
+
const { assertStoreSchemaCompatible } = require("./store-schema.cjs");
|
|
15
21
|
|
|
16
22
|
function loadNodeSqliteQuietly() {
|
|
17
23
|
const originalEmitWarning = process.emitWarning;
|
|
@@ -44,16 +50,61 @@ function openRaw(file) {
|
|
|
44
50
|
return db;
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 이 프로세스가 마이그레이션 권위인가. 기본은 **아니다**(follower).
|
|
55
|
+
* `AGENTLAS_STORE_MIGRATION_ROLE=owner` 는 데스크탑이 없는 머신을 위한 명시적 탈출구다 —
|
|
56
|
+
* 사람이 일부러 켜야 하고, 그때만 벤더 코어의 사다리가 한 번 돈다.
|
|
57
|
+
*/
|
|
58
|
+
function storeMigrationRole() {
|
|
59
|
+
return String(process.env.AGENTLAS_STORE_MIGRATION_ROLE || "").trim().toLowerCase() === "owner"
|
|
60
|
+
? "owner"
|
|
61
|
+
: "follower";
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
/**
|
|
48
65
|
* DB 파일이 없으면 하드 실패한다. 부트스트랩은 런처(bin/agentlas.cjs)의 책임 —
|
|
49
66
|
* 엔진이 임의 경로에 빈 DB를 만들면 데스크탑과의 공유 계약이 조용히 깨진다.
|
|
67
|
+
*
|
|
68
|
+
* 연 다음에는 스키마 버전을 **확인**한다. 낮으면 거절 — 조용히 진행하지 않는다.
|
|
69
|
+
* owner 로 명시 지정된 경우에만, 거절 대신 벤더 코어의 사다리를 한 번 돌려 승급한다.
|
|
50
70
|
*/
|
|
51
71
|
function openDb() {
|
|
52
72
|
const file = dbPath();
|
|
53
73
|
if (!fs.existsSync(file)) {
|
|
54
74
|
throw new Error(`Agentlas database not found: ${file} (run via bin/agentlas.cjs)`);
|
|
55
75
|
}
|
|
56
|
-
|
|
76
|
+
if (storeMigrationRole() === "owner") migrateSharedStoreAsOwner(file);
|
|
77
|
+
const db = openRaw(file);
|
|
78
|
+
try {
|
|
79
|
+
assertStoreSchemaCompatible(db, file);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
try { db.close(); } catch { /* noop */ }
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
return db;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 명시적 탈출구의 실행부. 데스크탑이 없는 머신에서 사람이 일부러 켰을 때만 불린다.
|
|
89
|
+
* 승급 자체는 여전히 **데스크탑 사다리 코드 하나**가 한다 — 터미널은 그 사다리를 손으로
|
|
90
|
+
* 복제하지 않는다(두 번째 사다리 구현이 곧 세 번째 주인이다). 코어가 없으면 정직한 실패.
|
|
91
|
+
*/
|
|
92
|
+
function migrateSharedStoreAsOwner(file) {
|
|
93
|
+
let core;
|
|
94
|
+
try {
|
|
95
|
+
core = require("./desktop-core.cjs").loadDesktopCore({ migrationRole: "owner" });
|
|
96
|
+
} catch (e) {
|
|
97
|
+
throw new Error(`AGENTLAS_STORE_MIGRATION_ROLE=owner: could not load the Desktop core to migrate ${file}: ${e && e.message ? e.message : e}`);
|
|
98
|
+
}
|
|
99
|
+
if (!core) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`AGENTLAS_STORE_MIGRATION_ROLE=owner: no Desktop core is available on this machine, so ${file} cannot be migrated here.\n` +
|
|
102
|
+
"Run `agentlas doctor`, or launch the Agentlas Desktop app once.",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (core.error) {
|
|
106
|
+
throw new Error(`AGENTLAS_STORE_MIGRATION_ROLE=owner: Desktop core failed to migrate ${file}: ${core.error.message}`);
|
|
107
|
+
}
|
|
57
108
|
}
|
|
58
109
|
|
|
59
110
|
function tableExists(db, name) {
|
|
@@ -151,6 +202,7 @@ function seedBuiltins(db) {
|
|
|
151
202
|
module.exports = {
|
|
152
203
|
openDb,
|
|
153
204
|
openRaw,
|
|
205
|
+
storeMigrationRole,
|
|
154
206
|
tableExists,
|
|
155
207
|
columnExists,
|
|
156
208
|
runWriteTransaction,
|
|
@@ -141,6 +141,25 @@ function installNativeModuleHook() {
|
|
|
141
141
|
_nativeHookInstalled = true;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 코어의 initStore 를 부르기 전에, 공유 저장소의 user_version 을 **우리 드라이버로** 확인한다.
|
|
146
|
+
* 별도 커넥션을 잠깐 열고 닫을 뿐 아무것도 쓰지 않는다. 파일이 아직 없으면(첫 실행 부트스트랩
|
|
147
|
+
* 이전) 판단할 것이 없으므로 통과 — 코어가 만들게 둔다.
|
|
148
|
+
*/
|
|
149
|
+
function assertSharedStoreSchemaBeforeCoreInit() {
|
|
150
|
+
const file = process.env.AGENTLAS_STORE_PATH || dbPath();
|
|
151
|
+
if (!fs.existsSync(file)) return;
|
|
152
|
+
const { openRaw } = require("./db.cjs");
|
|
153
|
+
const { assertStoreSchemaCompatible } = require("./store-schema.cjs");
|
|
154
|
+
let probe = null;
|
|
155
|
+
try {
|
|
156
|
+
probe = openRaw(file);
|
|
157
|
+
assertStoreSchemaCompatible(probe, file);
|
|
158
|
+
} finally {
|
|
159
|
+
try { probe && probe.close(); } catch { /* noop */ }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
144
163
|
let _cache = undefined;
|
|
145
164
|
|
|
146
165
|
/**
|
|
@@ -148,8 +167,22 @@ let _cache = undefined;
|
|
|
148
167
|
* · require(rel): 코어 안의 임의 컴파일 모듈을 상대경로로 로드(예: "store/automations").
|
|
149
168
|
* · runGraph: electron/workflow/run-graph.js 의 runGraph(automation, graph, opts).
|
|
150
169
|
* 공유 DB(AGENTLAS_STORE_PATH)를 코어 로드 전에 반드시 세팅한다 — store/db 가 모듈 로드 시 읽는다.
|
|
170
|
+
*
|
|
171
|
+
* ★마이그레이션 권위 (Phase 0, docs/DAEMON-ARCHITECTURE-DESIGN-2026-08-18.md §2/§6).
|
|
172
|
+
*
|
|
173
|
+
* 여기가 **두 번째 마이그레이션 주인**이었다. 이 셰임은 isPackaged:true 를 보고해
|
|
174
|
+
* (설계상) db.ts 의 개발 샌드박스 가드를 무력화하고, 공유 라이브 파일에 데스크탑과
|
|
175
|
+
* 같은 사다리를 돌렸다. 락이 없는 파일에서 두 프로세스가 사다리를 겹쳐 도는 것이
|
|
176
|
+
* 정확히 run_events + 인덱스 4개를 malformed 로 만든 경로다.
|
|
177
|
+
*
|
|
178
|
+
* **선택: (a) 사다리를 돌리지 않는다.** initStore 를 follower 로 부른다 — 코어는 열고
|
|
179
|
+
* 확인만 하며, 낮으면 정직하게 거절한다. (b)("데스크탑이 없으면 터미널이 주인")를 고르지
|
|
180
|
+
* 않은 이유: 데스크탑의 부재는 경합 없이 관측할 수 없다. 터미널이 사다리를 도는 중에
|
|
181
|
+
* 데스크탑이 켜질 수 있고, 틀렸을 때의 비용이 비대칭이다(손상 vs 앱 한 번 실행).
|
|
182
|
+
* 데스크탑이 정말 없는 머신은 `AGENTLAS_STORE_MIGRATION_ROLE=owner` 로 **사람이 일부러**
|
|
183
|
+
* 한 번 켠다 — 사고로 되는 일과 적어서 되는 일은 달라야 한다.
|
|
151
184
|
*/
|
|
152
|
-
function loadDesktopCore() {
|
|
185
|
+
function loadDesktopCore(options = {}) {
|
|
153
186
|
if (_cache !== undefined) return _cache;
|
|
154
187
|
const root = findCoreRoot();
|
|
155
188
|
if (!root) { _cache = null; return null; }
|
|
@@ -157,13 +190,30 @@ function loadDesktopCore() {
|
|
|
157
190
|
installNativeModuleHook();
|
|
158
191
|
// 코어의 store 가 이 값을 모듈 로드 시점에 읽는다 — require 이전에 세팅해야 한다.
|
|
159
192
|
if (!process.env.AGENTLAS_STORE_PATH) process.env.AGENTLAS_STORE_PATH = dbPath();
|
|
193
|
+
const migrationRole = options.migrationRole
|
|
194
|
+
|| (String(process.env.AGENTLAS_STORE_MIGRATION_ROLE || "").trim().toLowerCase() === "owner"
|
|
195
|
+
? "owner"
|
|
196
|
+
: "follower");
|
|
160
197
|
const req = (rel) => require(path.join(root, "electron", rel.replace(/\.js$/, "") + ".js"));
|
|
161
198
|
let kernel;
|
|
162
199
|
try {
|
|
200
|
+
const store = req("store/db");
|
|
201
|
+
if (migrationRole === "follower") {
|
|
202
|
+
// 옛 벤더 번들은 이 계약을 모른다(옵션·env 를 무시하고 사다리를 돈다). 조용히 넘어가지
|
|
203
|
+
// 않고 무엇이 위험한지 말한다 — `npm run vendor:core` 로 갱신하면 사라진다.
|
|
204
|
+
if (typeof store.STORE_SCHEMA_VERSION !== "number") {
|
|
205
|
+
console.warn(
|
|
206
|
+
"[store] vendored Desktop core predates the single-migration-authority contract "
|
|
207
|
+
+ "(no STORE_SCHEMA_VERSION export); it may migrate the shared database. "
|
|
208
|
+
+ "Refresh it with `npm run vendor:core`.",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
// 코어를 부르기 **전에** 우리 쪽에서 먼저 확인한다 — 옛 코어가 옵션을 무시해도
|
|
212
|
+
// 최소한 낮은 스키마에서는 사다리에 닿지 않는다.
|
|
213
|
+
assertSharedStoreSchemaBeforeCoreInit();
|
|
214
|
+
}
|
|
163
215
|
// 데스크탑은 app.whenReady 에서 initStore() 를 부른다 — 터미널도 코어를 쓰기 전에 부른다.
|
|
164
|
-
|
|
165
|
-
// 언제나 코어가 기대하는 모양이 된다. 이것이 "재구현"이 아니라 "재사용"의 핵심.
|
|
166
|
-
req("store/db").initStore();
|
|
216
|
+
store.initStore({ migrationRole });
|
|
167
217
|
kernel = req("workflow/run-graph");
|
|
168
218
|
} catch (e) { _cache = { root, error: e }; return _cache; }
|
|
169
219
|
_cache = {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* core/store-schema — 공유 저장소의 **스키마 버전 계약**. 터미널은 읽기만 한다.
|
|
4
|
+
*
|
|
5
|
+
* ★단일 마이그레이션 권위 (Phase 0, docs/DAEMON-ARCHITECTURE-DESIGN-2026-08-18.md §2/§6).
|
|
6
|
+
*
|
|
7
|
+
* `~/Library/Application Support/Agentlas/agentlas.sqlite` 는 락 없는 다중 쓰기 파일인데
|
|
8
|
+
* 마이그레이션 주인이 **둘**이었다:
|
|
9
|
+
* 1) 데스크탑의 사다리(electron/store/db.ts SCHEMA_VERSION),
|
|
10
|
+
* 2) 그 사다리를 그대로 다시 도는 터미널의 벤더 코어 경로(core/desktop-core.cjs initStore).
|
|
11
|
+
* 그리고 터미널의 가벼운 드라이버(core/db.cjs)는 user_version 을 **읽지도 않으면서** 쓰기
|
|
12
|
+
* 트랜잭션(seedBuiltins → BEGIN IMMEDIATE)을 열었다. 즉 "검사 없음 + 거절 없음 + 쓰기 있음".
|
|
13
|
+
*
|
|
14
|
+
* 이제 규칙은 하나다: **터미널은 절대 승급하지 않는다.** 파일이 이 배포가 아는 버전보다
|
|
15
|
+
* 낮으면 조용히 진행하지도, 몰래 마이그레이션하지도 않고 **정직하게 거절**한다.
|
|
16
|
+
*
|
|
17
|
+
* 왜 "없으면 터미널이 주인" 이 아닌가: 데스크탑의 부재는 경합 없이 관측할 수 없다 —
|
|
18
|
+
* 터미널이 사다리를 도는 도중에 데스크탑이 켜질 수 있다. 잘못 추측한 비용은 117MB 저장소
|
|
19
|
+
* 손상(db.ts 의 run_events 사고 주석)이고, 거절의 비용은 데스크탑 한 번 실행이다.
|
|
20
|
+
*
|
|
21
|
+
* 기대 버전의 출처: 이 패키지가 함께 배포하는 engine/bootstrap-schema.sql 의
|
|
22
|
+
* `PRAGMA user_version=` 이 곧 "이 배포가 아는 사다리 머리"다. 그 파일은 데스크탑 사다리를
|
|
23
|
+
* 빈 DB 에 끝까지 돌려 생성한 것이므로(scripts/gen-bootstrap-schema.cjs) 손으로 맞출 숫자가
|
|
24
|
+
* 따로 없다 — 재생성하면 자동으로 같이 움직인다.
|
|
25
|
+
*/
|
|
26
|
+
const fs = require("node:fs");
|
|
27
|
+
const path = require("node:path");
|
|
28
|
+
|
|
29
|
+
const BOOTSTRAP_SCHEMA_FILE = path.join(path.dirname(__dirname), "bootstrap-schema.sql");
|
|
30
|
+
|
|
31
|
+
let _expected;
|
|
32
|
+
|
|
33
|
+
/** 이 배포가 아는 스키마 버전. 부트스트랩 SQL 헤더에서 읽는다(정본은 데스크탑 사다리). */
|
|
34
|
+
function expectedStoreSchemaVersion() {
|
|
35
|
+
if (_expected !== undefined) return _expected;
|
|
36
|
+
let header = "";
|
|
37
|
+
try {
|
|
38
|
+
const fd = fs.openSync(BOOTSTRAP_SCHEMA_FILE, "r");
|
|
39
|
+
try {
|
|
40
|
+
const buf = Buffer.alloc(4096);
|
|
41
|
+
const read = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
42
|
+
header = buf.slice(0, read).toString("utf8");
|
|
43
|
+
} finally {
|
|
44
|
+
fs.closeSync(fd);
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
header = "";
|
|
48
|
+
}
|
|
49
|
+
const match = /PRAGMA\s+user_version\s*=\s*(\d+)/i.exec(header);
|
|
50
|
+
// 부트스트랩 SQL 을 못 읽으면 기대 버전을 **모른다**. 0 은 "검사 불가"를 뜻하고,
|
|
51
|
+
// 아래 단언은 그때 통과시킨다 — 모르는 것을 근거로 사용자를 막지 않는다.
|
|
52
|
+
_expected = match ? Number(match[1]) : 0;
|
|
53
|
+
return _expected;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 열린 커넥션의 user_version. 못 읽으면 null(모름). */
|
|
57
|
+
function readStoreSchemaVersion(db) {
|
|
58
|
+
try {
|
|
59
|
+
if (typeof db.pragma === "function") {
|
|
60
|
+
const value = db.pragma("user_version", { simple: true });
|
|
61
|
+
return Number.isFinite(Number(value)) ? Number(value) : null;
|
|
62
|
+
}
|
|
63
|
+
const row = db.prepare("PRAGMA user_version").get();
|
|
64
|
+
if (!row) return null;
|
|
65
|
+
const value = row.user_version ?? Object.values(row)[0];
|
|
66
|
+
return Number.isFinite(Number(value)) ? Number(value) : null;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 정직하고 실행 가능한 거절문. 두 가지 해결책을 모두 이름으로 말한다. */
|
|
73
|
+
function storeSchemaRefusalMessage(found, expected, file) {
|
|
74
|
+
return [
|
|
75
|
+
`Agentlas store schema is v${found}, but this Agentlas CLI needs v${expected}.`,
|
|
76
|
+
`Store: ${file}`,
|
|
77
|
+
"The Agentlas CLI never migrates the shared database — the Desktop app owns the migration ladder,",
|
|
78
|
+
"and a second migrator on this lock-free file is how the store was corrupted before.",
|
|
79
|
+
"",
|
|
80
|
+
"Fix it once, either way:",
|
|
81
|
+
" • Launch (or update) the Agentlas Desktop app once, then re-run this command.",
|
|
82
|
+
" • No Desktop app on this machine? Close every Agentlas process, then run this command once with",
|
|
83
|
+
" AGENTLAS_STORE_MIGRATION_ROLE=owner set, so the upgrade is a deliberate act rather than a race.",
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class StoreSchemaTooOldError extends Error {
|
|
88
|
+
constructor(found, expected, file) {
|
|
89
|
+
super(storeSchemaRefusalMessage(found, expected, file));
|
|
90
|
+
this.name = "StoreSchemaTooOldError";
|
|
91
|
+
this.code = "AGENTLAS_STORE_SCHEMA_TOO_OLD";
|
|
92
|
+
this.found = found;
|
|
93
|
+
this.expected = expected;
|
|
94
|
+
this.file = file;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 공유 저장소를 이 프로세스가 써도 되는가.
|
|
100
|
+
* 낮으면 던진다. 같거나 높으면 통과 — 더 높은 것은 데스크탑이 앞서간 것이고, 터미널은
|
|
101
|
+
* columnExists 기반 방어적 읽기로 전진 호환한다(기존 계약 유지).
|
|
102
|
+
*/
|
|
103
|
+
function assertStoreSchemaCompatible(db, file) {
|
|
104
|
+
const expected = expectedStoreSchemaVersion();
|
|
105
|
+
if (!expected) return; // 기대 버전을 모르면 막지 않는다.
|
|
106
|
+
const found = readStoreSchemaVersion(db);
|
|
107
|
+
if (found === null) return; // 읽을 수 없으면 판단하지 않는다(가짜 거절 금지).
|
|
108
|
+
if (found >= expected) return;
|
|
109
|
+
throw new StoreSchemaTooOldError(found, expected, file);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
BOOTSTRAP_SCHEMA_FILE,
|
|
114
|
+
StoreSchemaTooOldError,
|
|
115
|
+
assertStoreSchemaCompatible,
|
|
116
|
+
expectedStoreSchemaVersion,
|
|
117
|
+
readStoreSchemaVersion,
|
|
118
|
+
storeSchemaRefusalMessage,
|
|
119
|
+
};
|
|
@@ -123,6 +123,13 @@ function cliProjectContextSlice(projectPath, task) {
|
|
|
123
123
|
"--task-stdin",
|
|
124
124
|
"--no-refresh",
|
|
125
125
|
"--render",
|
|
126
|
+
// Recall degrades to a labelled map, never to nothing. Core's passive
|
|
127
|
+
// freshness check walks the whole repository (measured 11.0s on the
|
|
128
|
+
// pilot) against this 4s timeout, and any non-zero exit is swallowed
|
|
129
|
+
// into "" below — so without a budget a large project silently lost
|
|
130
|
+
// its slice on every turn.
|
|
131
|
+
"--allow-stale",
|
|
132
|
+
"--freshness-budget", "0.4",
|
|
126
133
|
],
|
|
127
134
|
{
|
|
128
135
|
cwd: projectPath,
|
|
@@ -523,6 +523,13 @@ function projectContextSlice(projectPath, task) {
|
|
|
523
523
|
"--task-stdin",
|
|
524
524
|
"--no-refresh",
|
|
525
525
|
"--render",
|
|
526
|
+
// Recall degrades to a labelled map, never to nothing. Core's passive
|
|
527
|
+
// freshness check walks the whole repository (measured 11.0s on the
|
|
528
|
+
// pilot) against this 4s timeout, and any non-zero exit is swallowed
|
|
529
|
+
// into "" below — so without a budget a large project silently lost
|
|
530
|
+
// its slice on every turn.
|
|
531
|
+
"--allow-stale",
|
|
532
|
+
"--freshness-budget", "0.4",
|
|
526
533
|
],
|
|
527
534
|
{
|
|
528
535
|
cwd: projectPath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.49",
|
|
4
4
|
"description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|