agentlas 1.0.64 → 1.0.66
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 +87 -0
- package/README.md +8 -6
- package/bin/agentlas.cjs +341 -16
- package/engine/acp/server.cjs +27 -1
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +29 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +10 -3
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/bootstrap-schema.sql +4 -4
- package/engine/browser/cdp.cjs +315 -32
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +80 -28
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +308 -113
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +67 -34
- package/engine/commands/build.cjs +42 -12
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +40 -12
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +8 -2
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +29 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +230 -61
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +15 -5
- package/engine/mcp/plan.cjs +5 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +212 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +187 -24
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- package/engine/commands/career-graph.cjs +0 -51
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.66 — 2026-08-30
|
|
4
|
+
|
|
5
|
+
- API 스트리밍 요청이 연결·본문 읽기 중 실패하거나 취소돼도 부모 취소 리스너와 idle 타이머를 즉시 정리해, 오래 실행한 Terminal 세션에 실패한 요청의 리스너가 누적되지 않게 했습니다.
|
|
6
|
+
- MCP 정책·동의·Experience 상태 JSON은 이제 no-follow 파일 디스크립터와 읽기 전후 identity 검증을 사용하며, private-state 잠금은 살아 있는 소유자를 시간만으로 탈취하거나 이전 소유자가 후속 잠금을 지울 수 없게 했습니다.
|
|
7
|
+
- `read` 권한의 Memory Event는 One 영구 원장에 기록되지 않으며, 쓰기 권한에서도 큐레이션을 통과한 제한된 후보만 안전한 원장 경로와 소유자 검증 잠금 안에서 중복 없이 전달됩니다.
|
|
8
|
+
- CLI 환경설정도 같은 bounded no-follow JSON 및 live-owner 잠금을 사용해, 오래 실행 중인 설정 저장을 시간만으로 탈취하거나 링크 경로에서 설정을 읽지 않습니다.
|
|
9
|
+
- Graph 인터뷰는 명시 또는 역할 기본값으로 선택한 Ollama를 이제 로컬 API 루프로 실제 실행하며, `bin`이 없다는 이유로 선택을 버리거나 다른 CLI로 바꾸지 않습니다.
|
|
10
|
+
- ACP client 연결이 닫히면 진행 중 child뿐 아니라 대화 유지를 위해 남아 있던 idle Session과 Orchestrator도 모두 종료해 편집기 재연결 뒤 좀비 런타임이 쌓이지 않게 했습니다.
|
|
11
|
+
- Workforce·Storm이 쓰는 Core JSON 캡처는 stdout을 16MiB로 제한하고 기본 120초 안에 끝나지 않는 Python 호출을 종료해, 손상된 Core 응답이 Terminal 메모리나 실행을 무기한 점유하지 않습니다.
|
|
12
|
+
- 자동화 on/off/run/remove의 ID 접두사는 `%`·`_`를 와일드카드로 해석하지 않고, 여러 자동화와 겹치면 임의 행을 조작하는 대신 전체 ID를 요구합니다.
|
|
13
|
+
- 직접 생성한 Operational RunReceipt도 반환된 실제 생성 시각을 fallback run identity에 사용해, `createdAt`을 생략한 호출이 모두 같은 `undefined` 기반 ID로 합쳐지지 않습니다.
|
|
14
|
+
- Desktop이 저장한 자동화의 `antigravity`/backend/source/model/effort 핀을 Terminal도 같은 실행 정체성으로 해석하고, 정확 핀 실패 시 Dashboard 역할 풀의 다른 공급자로 넘어가지 않습니다.
|
|
15
|
+
- Memory Event 큐레이션은 후보 수·본문·증거를 제한하고 단일 원자 SQL로 중복을 막으며, 기존 private 프로젝트 디렉터리의 regular 단일-link JSONL에만 bounded append합니다.
|
|
16
|
+
- Experience 적립은 누락·잘못된 권한을 read로 막고, 부분적으로만 전달된 exact agent binding을 설치본이나 로컬 해시 정체성으로 조용히 대체하지 않습니다.
|
|
17
|
+
- Provider resume ID 저장은 시작 시 읽은 ID를 compare-and-swap 조건으로 사용해, 같은 채팅의 늦게 끝난 이전 턴이 더 최신 세션 연속성을 덮어쓰지 않습니다.
|
|
18
|
+
- Hub 응답은 stream-readable 본문만 16MiB 안에서 읽고, CLI 세션 파일도 bounded no-follow identity 검증을 통과한 private JSON만 인증 증거로 사용합니다.
|
|
19
|
+
- Project 초기 시드는 실제 private `.agentlas` 디렉터리와 single-link regular 파일만 만들거나 재사용해, 미리 놓인 symlink/hardlink를 따라 외부 파일을 쓰지 않습니다.
|
|
20
|
+
- Project 팀 연결은 Desktop의 현재 agent/team 정본과 레거시 행을 명시적으로 구분하고, 정확 릴리스가 로컬에 증명된 컨트롤러만 최대 64개 bounded 팀에서 실행합니다.
|
|
21
|
+
- Cloud 설치·복구는 private 실제 설치 루트와 bounded single-link 저널만 신뢰하고, 심링크·하드링크·특수 엔트리로 바뀐 destination/staging/backup을 재귀 삭제하거나 교체하지 않습니다.
|
|
22
|
+
- Project 활성화는 canonical 루트와 현재 private-state 무결성을 매번 확인하고, Desktop의 `antigravity` 선택을 일반 Terminal 실행에서도 `agy`로 보존하며, 능력 승인 프리픽스는 토큰 경계를 넘어 비슷한 명령을 허용하지 않습니다.
|
|
23
|
+
- Workforce는 누락·손상 권한을 read로 막고 `--parallel`의 실제 8개 상한을 지키며, Dashboard 역할 풀에서 첫 가용 런타임을 우선순위대로 선택하고 빈 Worker 풀은 Orchestrator 풀을 상속합니다.
|
|
24
|
+
- 설치 에이전트 라우팅은 BYOK·Ollama 판정의 인증·한도·연결 오류를 빈 응답으로 지우지 않고, CLI 런타임과 같이 실제 실패 사유를 보존합니다.
|
|
25
|
+
- 명시값·환경설정의 Desktop `antigravity` 런타임도 Terminal `agy`로 일관되게 실행하며, 정상 답변의 기술 용어 `rate-limit`을 한도 거절로 오인하지 않습니다.
|
|
26
|
+
- Native 모델 stdout을 16MiB로 제한하고, 생성·재사용하는 MCP 설정은 실제 private 디렉터리와 bounded single-link 파일만 허용해 비정상 스트림·링크 경로를 따라가지 않습니다.
|
|
27
|
+
- node:sqlite fallback도 실제 `BEGIN IMMEDIATE`/rollback과 중첩 savepoint를 사용해, 예외가 난 다중 쓰기를 부분 커밋하지 않습니다.
|
|
28
|
+
- Runtime capability 변환은 Desktop `antigravity`를 `agy`로 보존하고 ACP CLI를 API backend로 오분류하거나 알 수 없는 runtime spec을 제조하지 않습니다.
|
|
29
|
+
- Local Core Workforce 어댑터는 search→validate→prepare의 WorkOrder·Selection을 정확히 묶고, 과도하게 깊거나 큰 응답 및 prototype key를 경계에서 거절합니다.
|
|
30
|
+
- 로컬 모델의 파일 도구는 8MiB를 넘는 텍스트를 전체 메모리에 읽거나 쓰지 않으며, Terminal 설정 화면은 이미 제공되는 멀티모달 설정 명령을 Desktop 전용으로 잘못 안내하지 않습니다.
|
|
31
|
+
- 희소 cron 탐색은 긴 분 단위 루프 대신 로컬 달력의 불일치 날짜를 건너뛰며 8년 범위를 확인해, 비윤년 직후 등록한 2월 29일 일정도 다음 윤년 실행 시각을 잃지 않습니다.
|
|
32
|
+
- 저장 권한이 없을 때의 제품 기본값과 손상된 권한 값을 구분해 후자는 read로 막고, CLI 런타임 탐지는 PATH의 `which` 프로그램을 실행하지 않고 실제 실행 파일을 직접 확인합니다.
|
|
33
|
+
- Hephaestus 캡처는 시간·출력 상한이나 사용자 중단 뒤 응답 없는 child를 강제 종료하고 부분 JSON을 결과처럼 렌더링하지 않으며, bounded no-follow CLI history와 그 잠금은 살아 있는 소유자를 시간만으로 탈취하거나 이전 소유자가 후속 잠금을 지우지 않습니다.
|
|
34
|
+
- Graph 패키지는 중첩 비밀값까지 재귀 치환하고 그래프·매니페스트 지문을 모두 검증하며, 같은 MCP 서버를 쓰는 모든 노드를 의존성에 보존합니다.
|
|
35
|
+
- 모델 카탈로그와 라우팅 영수증은 bounded no-follow 단일-link 파일로만 읽고 쓰며, `route`·`call`·`hep` 요청 안의 일반 단어 `help`를 명령 도움말로 가로채지 않습니다.
|
|
36
|
+
- Desktop Core 캐시는 live-owner 버전 잠금과 streaming 크기·해시 검증을 사용하고, 경로 이탈·링크·특수 엔트리가 든 archive를 풀기 전에 거절합니다.
|
|
37
|
+
|
|
38
|
+
- CLI output and argument contracts now fail closed across list, run, build,
|
|
39
|
+
One, roles, variants, graphs, firms, search, automations, credentials, MCP,
|
|
40
|
+
Experience, Ontology, Storm, Swarm, Workforce, Cloud/upload, doctor,
|
|
41
|
+
install/update/import/uninstall, login/logout/setup, project/billing,
|
|
42
|
+
ACP/multimodal/memory, browser/document/plugin, environment/account, and
|
|
43
|
+
connection commands instead
|
|
44
|
+
of silently accepting unknown or contradictory input. Invalid commands stop
|
|
45
|
+
before model, network, project, or credential mutations. JSON, YAML, quiet,
|
|
46
|
+
no-header, and no-color modes now match their documented behavior.
|
|
47
|
+
- Help now rejects undocumented flags and trailing arguments, and the compact
|
|
48
|
+
startup banner stays within the actual terminal width on narrow screens.
|
|
49
|
+
- Research now exposes the complete Agentlas OS 1.2.37 diagnostic, proof,
|
|
50
|
+
browser-hardpoint, module, loadout, planning, read, search, and gather command
|
|
51
|
+
set instead of rejecting every subcommand outside the legacy five.
|
|
52
|
+
- Async native help now waits for its child output, and the Local Core MCP
|
|
53
|
+
transport bounds unterminated stdout responses instead of growing memory
|
|
54
|
+
without limit.
|
|
55
|
+
- MCP connection probes now refuse malformed runtime arguments and handle
|
|
56
|
+
asynchronous child-stdin failures as a failed preflight instead of risking
|
|
57
|
+
an unhandled stream error.
|
|
58
|
+
- REPL shell commands now participate in shutdown tracking, stop on Ctrl-C or
|
|
59
|
+
quit, escalate stubborn process groups to SIGKILL, and cannot survive the
|
|
60
|
+
parent process as detached zombies.
|
|
61
|
+
- Persistent model-role changes now cover multimodal, keep Dashboard pools and
|
|
62
|
+
the resolved selection in sync, and bind resumed provider sessions to the
|
|
63
|
+
selected model (plus ACP permission identity where the protocol requires it).
|
|
64
|
+
An explicit stop no longer launches the next fallback model, while a genuine
|
|
65
|
+
ACP recovery receives its own current fingerprint and cancellation signal.
|
|
66
|
+
- Project credential/env files, Telegram token files, MCP consent/inventory,
|
|
67
|
+
child-process shutdown, session event sinks, and generated output paths now
|
|
68
|
+
enforce bounded, no-follow, project-scoped storage and cleanup contracts.
|
|
69
|
+
Native context preparation also refuses broad, missing, or symbolic-link
|
|
70
|
+
agent source folders instead of recreating or writing through them.
|
|
71
|
+
- Cloud packaging, agent evolution, route persistence, Storm/Swarm fan-out, and
|
|
72
|
+
Oberon rendering now reject ambiguous or stale state rather than reporting a
|
|
73
|
+
partial operation as complete.
|
|
74
|
+
- Concurrent Cloud list/save/delete/bind and source-marker updates now preserve
|
|
75
|
+
every observed revision under private locks, reject corrupted tombstones, and
|
|
76
|
+
compare receipt times chronologically instead of as text.
|
|
77
|
+
- Terminal palettes no longer advertise the retired `career-graph` command. Its
|
|
78
|
+
project data remains preserved, while users are directed to `ontology` for
|
|
79
|
+
sources and the Agentlas OS runtime for derived indexing.
|
|
80
|
+
|
|
81
|
+
## 1.0.65 — 2026-08-29
|
|
82
|
+
|
|
83
|
+
- Browser, document, and Telegram CDP flows now create and track an Agentlas-owned
|
|
84
|
+
tab instead of attaching to the first page exposed by the debugging port. Existing
|
|
85
|
+
pages require explicit target selection, owned temporary pages are cleaned up, and
|
|
86
|
+
WebSocket attachment has a bounded open timeout.
|
|
87
|
+
- First-run SQLite bootstrap now starts at schema `user_version=106`, generated from
|
|
88
|
+
the Desktop migration ladder, including the multimodal model-role constraint.
|
|
89
|
+
|
|
3
90
|
## 1.0.64 — 2026-08-29
|
|
4
91
|
|
|
5
92
|
- First-run database bootstrap now refuses symbolic links and non-file entries
|
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ agentlas cd <agent> # Print folder path of an agent (cd "$(agentlas
|
|
|
79
79
|
agentlas search "<query>" # Search Hub agents (no login required)
|
|
80
80
|
agentlas install <slug> # Install Hub agent locally (gated by security trust checks)
|
|
81
81
|
agentlas plugin add <slug> # Add Hub plugin (MCP server)
|
|
82
|
-
agentlas plugin list # List active plugins
|
|
82
|
+
agentlas plugin list # List active plugins
|
|
83
83
|
agentlas build "<prompt>" # Build, repair, and package agents or multi-agent teams
|
|
84
84
|
agentlas upload <path> # Upload package to private Agent Cloud (default)
|
|
85
85
|
agentlas upload <path> --visibility marketplace # Explicitly publish to public Agentlas Hub
|
|
@@ -104,7 +104,7 @@ agentlas hep-local "<prompt>" # Same staffing, registered Local agents only
|
|
|
104
104
|
agentlas hep-cloud "<prompt>" # Same staffing, owner Agent Cloud only
|
|
105
105
|
agentlas hep-hub "<prompt>" # Same staffing, public Hub only
|
|
106
106
|
agentlas call "a,b" "<context>" # Call explicitly named Hub or Cloud agents
|
|
107
|
-
agentlas browser [...] # Real browser hardpoint launcher
|
|
107
|
+
agentlas browser [...] # Real browser hardpoint launcher (opens an owned tab)
|
|
108
108
|
agentlas route "<prompt>" [--json] # Preview routing decisions without execution
|
|
109
109
|
agentlas research <sub> # Research loadout (status|gather|search|read|plan)
|
|
110
110
|
```
|
|
@@ -114,8 +114,10 @@ agentlas research <sub> # Research loadout (status|gather|search|read|p
|
|
|
114
114
|
agentlas memory import <path> --agent <id> [--apply]
|
|
115
115
|
agentlas evolve [list|apply <id>|revert <id>]
|
|
116
116
|
agentlas ontology <sub> # Manage project ontology (status|list|add)
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
# Derived career-graph indexing is owned by the installed Hephaestus runtime;
|
|
118
|
+
# use `agentlas ontology` for source registration.
|
|
119
|
+
# Run-journal diagnosis is an expert Hephaestus path:
|
|
120
|
+
# agentlas hep stormbreaker journal --run-id <id> --journal <path>
|
|
119
121
|
agentlas project [status|init] # Explicit entry point to initialize `.agentlas/` context
|
|
120
122
|
agentlas context <sub> # Context slice operations (refresh|locate|refs|slice|impact|verify)
|
|
121
123
|
```
|
|
@@ -216,7 +218,7 @@ Runtime Search Order (`engine/agentlas-core-harness.cjs`, `engine/hephaestus/run
|
|
|
216
218
|
2. `~/.agentlas/runtime/current`
|
|
217
219
|
3. Packaged Core (`<resources>/Hephaestus`, macOS: `/Applications/Agentlas.app/Contents/Resources/Hephaestus`)
|
|
218
220
|
|
|
219
|
-
Commands including `storm`, `swarm`, `workforce`/`network`, `route`, `research`, `context`, `
|
|
221
|
+
Commands including `storm`, `swarm`, `workforce`/`network`, `route`, `research`, `context`, `netadmin`, `hep`, `build`, `call`, `browser`, and `connect` pass directly through to this core runtime. Derived career-graph indexing and run-journal diagnosis are also owned by the installed Hephaestus runtime; Terminal keeps existing project data intact but no longer exposes separate `agentlas career-graph` or `agentlas journal` commands. If no core runtime is found, Agentlas Terminal halts with an explicit error and exit code 1 (no silent fallback).
|
|
220
222
|
|
|
221
223
|
The terminal package owns REPL orchestration, session trees, agent registries, Hub/Cloud HTTP client surfaces, credential storage, MCP preflight probing, automation scheduling, and SQLite schema management.
|
|
222
224
|
|
|
@@ -230,7 +232,7 @@ The launcher (`bin/agentlas.cjs`) runs system Node against `engine/`. The defaul
|
|
|
230
232
|
| Windows | `%APPDATA%\Agentlas` |
|
|
231
233
|
| Linux | `$XDG_CONFIG_HOME/Agentlas` (default `~/.config/Agentlas`) |
|
|
232
234
|
|
|
233
|
-
The SQLite database file is `agentlas.sqlite` (`user_version=
|
|
235
|
+
The SQLite database file is `agentlas.sqlite` (`user_version=106`). When launched for the first time without an existing database, it bootstraps schemas using `engine/bootstrap-schema.sql`. Consequently, **projects, installed agents, task history, automation sessions, and MCP registrations are shared across Desktop and Terminal**. The first ordered project agent remains the controller; additional agents are task-scoped and are stored only as execution ledgers, never as global conversations or durable owners.
|
|
234
236
|
|
|
235
237
|
**Single migration authority.** The Desktop app owns the schema migration ladder; the CLI never migrates the shared database. `engine/bootstrap-schema.sql` is generated by running that ladder to completion against an empty database, so a CLI-created store already sits at the ladder head and has nothing left for Desktop to upgrade. If the CLI opens a store older than the version it knows, it refuses with an actionable message instead of migrating or silently proceeding — a second migrator on this lock-free file is what corrupted the store once before. On a machine with no Desktop app, an operator can set `AGENTLAS_STORE_MIGRATION_ROLE=owner` for a single deliberate upgrade run with every other Agentlas process closed.
|
|
236
238
|
|
package/bin/agentlas.cjs
CHANGED
|
@@ -36,8 +36,14 @@ for (const stream of [process.stdout, process.stderr]) {
|
|
|
36
36
|
const fs = require("node:fs");
|
|
37
37
|
const os = require("node:os");
|
|
38
38
|
const path = require("node:path");
|
|
39
|
+
const crypto = require("node:crypto");
|
|
39
40
|
const { spawn } = require("node:child_process");
|
|
40
41
|
const { configureSqliteConnection } = require("../engine/agentlas-sqlite-policy.cjs");
|
|
42
|
+
const { parseOutputFlags, renderError } = require("../engine/cli-output.cjs");
|
|
43
|
+
const { resolveCommandName, SELF_HELP_COMMANDS } = require("../engine/commands/index.cjs");
|
|
44
|
+
|
|
45
|
+
const SQLITE_HEADER = Buffer.from("SQLite format 3\0", "ascii");
|
|
46
|
+
const SQLITE_MIN_BYTES = 512;
|
|
41
47
|
|
|
42
48
|
const REAL_SELF = (() => {
|
|
43
49
|
try { return fs.realpathSync(__filename); } catch { return __filename; }
|
|
@@ -63,11 +69,61 @@ function dbPath() {
|
|
|
63
69
|
return path.join(userDataDir(), "agentlas.sqlite");
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
// Keep the launcher's no-database path identical to the engine's CLI grammar.
|
|
73
|
+
// A plain word "help" is valid task content (`agentlas run -p help`); only the
|
|
74
|
+
// actual help/version command forms may skip first-run database bootstrap.
|
|
75
|
+
function isMetadataOnlyInvocation(argv) {
|
|
76
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
77
|
+
const helpRequested = args.some((arg) => arg === "--help" || arg === "-h");
|
|
78
|
+
const helpCommand = args.find((arg) => arg !== "--help" && arg !== "-h" && !String(arg).startsWith("-"));
|
|
79
|
+
if (helpRequested && helpCommand) return true;
|
|
80
|
+
|
|
81
|
+
const normalized = args.map((arg) => {
|
|
82
|
+
if (arg === "--help" || arg === "-h") return "help";
|
|
83
|
+
if (arg === "--version" || arg === "-V") return "version";
|
|
84
|
+
return arg;
|
|
85
|
+
});
|
|
86
|
+
const { rest } = parseOutputFlags(normalized);
|
|
87
|
+
const [rawCommand, ...commandArgs] = rest;
|
|
88
|
+
if (rawCommand === "help" || rawCommand === "version") return true;
|
|
89
|
+
return SELF_HELP_COMMANDS.has(resolveCommandName(rawCommand)) && commandArgs[0] === "help";
|
|
90
|
+
}
|
|
91
|
+
|
|
66
92
|
function securePrivateMode(target, mode) {
|
|
67
93
|
if (process.platform === "win32") return;
|
|
68
94
|
fs.chmodSync(target, mode);
|
|
69
95
|
}
|
|
70
96
|
|
|
97
|
+
function databaseStatsMatch(expected, actual) {
|
|
98
|
+
return Boolean(expected && actual) &&
|
|
99
|
+
expected.isFile() && actual.isFile() &&
|
|
100
|
+
!expected.isSymbolicLink() && !actual.isSymbolicLink() &&
|
|
101
|
+
expected.dev === actual.dev &&
|
|
102
|
+
expected.ino === actual.ino &&
|
|
103
|
+
expected.nlink === actual.nlink &&
|
|
104
|
+
expected.size === actual.size &&
|
|
105
|
+
expected.mtimeMs === actual.mtimeMs &&
|
|
106
|
+
expected.ctimeMs === actual.ctimeMs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function databaseContentMatch(expected, actual) {
|
|
110
|
+
return Boolean(expected && actual) &&
|
|
111
|
+
expected.isFile() && actual.isFile() &&
|
|
112
|
+
!expected.isSymbolicLink() && !actual.isSymbolicLink() &&
|
|
113
|
+
expected.dev === actual.dev &&
|
|
114
|
+
expected.ino === actual.ino &&
|
|
115
|
+
expected.nlink === actual.nlink &&
|
|
116
|
+
expected.size === actual.size &&
|
|
117
|
+
expected.mtimeMs === actual.mtimeMs;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function directoryIdentityMatch(expected, actual) {
|
|
121
|
+
return Boolean(expected && actual) &&
|
|
122
|
+
expected.isDirectory() && actual.isDirectory() &&
|
|
123
|
+
!expected.isSymbolicLink() && !actual.isSymbolicLink() &&
|
|
124
|
+
expected.dev === actual.dev && expected.ino === actual.ino;
|
|
125
|
+
}
|
|
126
|
+
|
|
71
127
|
function databaseFileStat(target) {
|
|
72
128
|
let stat;
|
|
73
129
|
try {
|
|
@@ -76,12 +132,206 @@ function databaseFileStat(target) {
|
|
|
76
132
|
if (error && error.code === "ENOENT") return null;
|
|
77
133
|
throw error;
|
|
78
134
|
}
|
|
79
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
80
|
-
|
|
135
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
|
|
136
|
+
const error = new Error(`Agentlas database path must be a regular file: ${target}`);
|
|
137
|
+
if (stat.nlink !== 1) error.code = "AGENTLAS_DATABASE_MULTILINK";
|
|
138
|
+
throw error;
|
|
81
139
|
}
|
|
82
140
|
return stat;
|
|
83
141
|
}
|
|
84
142
|
|
|
143
|
+
function quarantinePath(target) {
|
|
144
|
+
return `${target}.bootstrap-quarantine-${process.pid}-${crypto.randomUUID()}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function bootstrapRaceError(message) {
|
|
148
|
+
const error = new Error(message);
|
|
149
|
+
error.code = "AGENTLAS_BOOTSTRAP_CONCURRENT_RETRY";
|
|
150
|
+
return error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function activeBootstrapOwnerExists(target) {
|
|
154
|
+
const dir = path.dirname(target);
|
|
155
|
+
const prefix = `${path.basename(target)}.bootstrap-quarantine-`;
|
|
156
|
+
let entries;
|
|
157
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; }
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (!entry || !entry.isFile() || !entry.name.startsWith(prefix)) continue;
|
|
160
|
+
const match = entry.name.slice(prefix.length).match(/^(\d+)-[0-9a-f-]+$/i);
|
|
161
|
+
if (!match) continue;
|
|
162
|
+
const ownerPid = Number(match[1]);
|
|
163
|
+
if (!Number.isSafeInteger(ownerPid) || ownerPid <= 0 || ownerPid === process.pid) continue;
|
|
164
|
+
try {
|
|
165
|
+
process.kill(ownerPid, 0);
|
|
166
|
+
return true;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error && error.code === "EPERM") return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function activeBootstrapBuildExists(target) {
|
|
175
|
+
const dir = path.dirname(target);
|
|
176
|
+
const prefix = `${path.basename(target)}.bootstrap-`;
|
|
177
|
+
let entries;
|
|
178
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; }
|
|
179
|
+
for (const entry of entries) {
|
|
180
|
+
if (!entry || !entry.isFile() || !entry.name.startsWith(prefix)) continue;
|
|
181
|
+
const match = entry.name.slice(prefix.length).match(/^(\d+)-/);
|
|
182
|
+
if (!match) continue;
|
|
183
|
+
const ownerPid = Number(match[1]);
|
|
184
|
+
if (!Number.isSafeInteger(ownerPid) || ownerPid <= 0 || ownerPid === process.pid) continue;
|
|
185
|
+
try {
|
|
186
|
+
process.kill(ownerPid, 0);
|
|
187
|
+
return true;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error && error.code === "EPERM") return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function waitForBootstrapOwner() {
|
|
196
|
+
try {
|
|
197
|
+
const waitCell = new Int32Array(new SharedArrayBuffer(4));
|
|
198
|
+
Atomics.wait(waitCell, 0, 0, 25);
|
|
199
|
+
} catch { /* runtimes without Atomics.wait continue with the next check */ }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stableSqliteWinner(target, before) {
|
|
203
|
+
let fd;
|
|
204
|
+
try {
|
|
205
|
+
if (!before || before.size < SQLITE_MIN_BYTES) return false;
|
|
206
|
+
fd = fs.openSync(target, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
207
|
+
const opened = fs.fstatSync(fd);
|
|
208
|
+
if (!databaseStatsMatch(before, opened)) return false;
|
|
209
|
+
const header = Buffer.alloc(SQLITE_HEADER.length);
|
|
210
|
+
if (fs.readSync(fd, header, 0, header.length, 0) !== header.length || !header.equals(SQLITE_HEADER)) return false;
|
|
211
|
+
const afterRead = fs.fstatSync(fd);
|
|
212
|
+
const afterPath = databaseFileStat(target);
|
|
213
|
+
return databaseStatsMatch(opened, afterRead) && databaseStatsMatch(afterRead, afterPath);
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
} finally {
|
|
217
|
+
if (fd !== undefined) {
|
|
218
|
+
try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function quarantineEmptyDatabaseIfUnchanged(target, expected) {
|
|
224
|
+
const dir = path.dirname(target);
|
|
225
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
226
|
+
const directoryFlags = fs.constants.O_RDONLY | (fs.constants.O_DIRECTORY || 0) | noFollow;
|
|
227
|
+
let fd;
|
|
228
|
+
let directoryFd;
|
|
229
|
+
let quarantine;
|
|
230
|
+
try {
|
|
231
|
+
// Re-lstat immediately before opening the descriptor. The original lstat
|
|
232
|
+
// alone is stale as soon as another process can replace the pathname.
|
|
233
|
+
const current = databaseFileStat(target);
|
|
234
|
+
if (!current) throw bootstrapRaceError("database disappeared while another bootstrap was quarantining it");
|
|
235
|
+
if (!databaseStatsMatch(expected, current)) {
|
|
236
|
+
if (current.size > 0) throw bootstrapRaceError("database was replaced while another bootstrap was quarantining it");
|
|
237
|
+
throw new Error("database changed before empty-file removal");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const listedDirectory = fs.lstatSync(dir);
|
|
241
|
+
if (!listedDirectory.isDirectory() || listedDirectory.isSymbolicLink()) {
|
|
242
|
+
throw new Error("database parent directory is unsafe");
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
directoryFd = fs.openSync(dir, directoryFlags);
|
|
246
|
+
const openedDirectory = fs.fstatSync(directoryFd);
|
|
247
|
+
if (!directoryIdentityMatch(listedDirectory, openedDirectory)) {
|
|
248
|
+
throw new Error("database parent directory changed before empty-file removal");
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
// Windows does not permit opening a directory with the same descriptor
|
|
252
|
+
// flags. The file descriptor identity checks below remain mandatory.
|
|
253
|
+
if (process.platform !== "win32" || !["EISDIR", "EINVAL", "ENOTSUP", "EPERM"].includes(error && error.code)) {
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// O_NOFOLLOW prevents a swapped symlink from being opened on POSIX. The
|
|
259
|
+
// fstat fields are compared to both lstat observations before quarantine.
|
|
260
|
+
try {
|
|
261
|
+
fd = fs.openSync(target, fs.constants.O_RDONLY | noFollow);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error && error.code === "ENOENT") throw bootstrapRaceError("database disappeared while opening empty file");
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
const opened = fs.fstatSync(fd);
|
|
267
|
+
if (!databaseStatsMatch(expected, opened)) {
|
|
268
|
+
if (opened.size > 0) throw bootstrapRaceError("database was replaced while opening empty file");
|
|
269
|
+
throw new Error("database changed while opening empty file");
|
|
270
|
+
}
|
|
271
|
+
const beforeQuarantine = databaseFileStat(target);
|
|
272
|
+
if (!beforeQuarantine) throw bootstrapRaceError("database disappeared immediately before quarantine");
|
|
273
|
+
if (!databaseStatsMatch(expected, beforeQuarantine) || !databaseStatsMatch(opened, beforeQuarantine)) {
|
|
274
|
+
if (beforeQuarantine.size > 0) throw bootstrapRaceError("database was replaced immediately before quarantine");
|
|
275
|
+
throw new Error("database changed immediately before empty-file removal");
|
|
276
|
+
}
|
|
277
|
+
const currentDirectory = fs.lstatSync(dir);
|
|
278
|
+
if (!directoryIdentityMatch(listedDirectory, currentDirectory)) {
|
|
279
|
+
throw new Error("database parent directory changed immediately before empty-file removal");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Never unlink the pathname after a check: a concurrent replacement could
|
|
283
|
+
// make that path name a different file. Move the checked candidate to a
|
|
284
|
+
// same-directory, recoverable quarantine name instead. A UUID plus an
|
|
285
|
+
// O_EXCL reservation makes accidental destination collisions negligible;
|
|
286
|
+
// the moved inode is verified again before bootstrap can continue.
|
|
287
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
288
|
+
const candidate = quarantinePath(target);
|
|
289
|
+
try {
|
|
290
|
+
const reservation = fs.openSync(candidate, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
|
|
291
|
+
fs.closeSync(reservation);
|
|
292
|
+
quarantine = candidate;
|
|
293
|
+
break;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (!error || error.code !== "EEXIST") throw error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!quarantine) throw new Error("could not allocate a unique database quarantine path");
|
|
299
|
+
try {
|
|
300
|
+
fs.renameSync(target, quarantine);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (error && error.code === "ENOENT") throw bootstrapRaceError("database disappeared while quarantining empty file");
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// rename updates ctime on POSIX, so compare the pre-rename inode/content
|
|
307
|
+
// fields against the post-rename descriptor and verify the post-rename
|
|
308
|
+
// ctime/other stat fields against a fresh pathname observation.
|
|
309
|
+
const afterQuarantine = fs.fstatSync(fd);
|
|
310
|
+
const quarantined = databaseFileStat(quarantine);
|
|
311
|
+
if (!databaseContentMatch(expected, afterQuarantine) || !databaseStatsMatch(afterQuarantine, quarantined)) {
|
|
312
|
+
throw new Error(`database changed while quarantining empty file; preserved at ${quarantine}`);
|
|
313
|
+
}
|
|
314
|
+
const afterDirectory = fs.lstatSync(dir);
|
|
315
|
+
if (!directoryIdentityMatch(listedDirectory, afterDirectory)) {
|
|
316
|
+
throw new Error(`database parent directory changed while quarantining empty file; preserved at ${quarantine}`);
|
|
317
|
+
}
|
|
318
|
+
let targetAfter;
|
|
319
|
+
try { targetAfter = fs.lstatSync(target); } catch (error) {
|
|
320
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
321
|
+
}
|
|
322
|
+
if (targetAfter) {
|
|
323
|
+
throw new Error(`database path was replaced while quarantining empty file; preserved at ${quarantine}`);
|
|
324
|
+
}
|
|
325
|
+
} finally {
|
|
326
|
+
if (fd !== undefined) {
|
|
327
|
+
try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
328
|
+
}
|
|
329
|
+
if (directoryFd !== undefined) {
|
|
330
|
+
try { fs.closeSync(directoryFd); } catch { /* best effort */ }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
85
335
|
// ── SQLite 로더 (부트스트랩용): better-sqlite3 → node:sqlite ──
|
|
86
336
|
// require.resolve가 아니라 실제 로드로 판별한다 — ABI가 깨진 better-sqlite3나
|
|
87
337
|
// node:sqlite가 아직 없는/플래그가 필요한 Node(≤22.4 등)를 정확히 걸러낸다.
|
|
@@ -155,18 +405,72 @@ function bootstrapDbIfMissing() {
|
|
|
155
405
|
// 잡힌 파일이므로 없는 것으로 취급해 정상 부트스트랩 경로를 태운다. 내용이 있는데
|
|
156
406
|
// 손상된 경우는 여기서 판단하지 않는다 — 그건 복구지 부트스트랩이 아니고, 멀쩡한
|
|
157
407
|
// DB 를 빈 것으로 오판해 덮어쓰는 위험이 훨씬 크다.
|
|
158
|
-
|
|
159
|
-
|
|
408
|
+
let waitingForConcurrentBootstrap = false;
|
|
409
|
+
let emptyCandidateResolved = false;
|
|
410
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
411
|
+
let existing;
|
|
412
|
+
try {
|
|
413
|
+
existing = databaseFileStat(p);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
// A winning bootstrap briefly has two hard links (its temp inode and the
|
|
416
|
+
// final name). Treat only that observable window as retryable when the
|
|
417
|
+
// owner process is still alive; a user-created hard link remains a hard
|
|
418
|
+
// failure.
|
|
419
|
+
if (error && error.code === "AGENTLAS_DATABASE_MULTILINK" && activeBootstrapBuildExists(p)) {
|
|
420
|
+
waitingForConcurrentBootstrap = true;
|
|
421
|
+
waitForBootstrapOwner();
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
if (!existing) {
|
|
427
|
+
if (!waitingForConcurrentBootstrap) {
|
|
428
|
+
emptyCandidateResolved = true;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
if (!activeBootstrapOwnerExists(p)) {
|
|
432
|
+
throw new Error("Empty Agentlas database could not be replaced: concurrent bootstrap owner disappeared before publishing a database");
|
|
433
|
+
}
|
|
434
|
+
waitForBootstrapOwner();
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
160
437
|
if (existing.size > 0) {
|
|
161
438
|
securePrivateMode(p, 0o600);
|
|
162
439
|
return { created: false, path: p };
|
|
163
440
|
}
|
|
164
441
|
try {
|
|
165
|
-
|
|
442
|
+
quarantineEmptyDatabaseIfUnchanged(p, existing);
|
|
443
|
+
emptyCandidateResolved = true;
|
|
444
|
+
break;
|
|
166
445
|
} catch (error) {
|
|
446
|
+
if (error && error.code === "AGENTLAS_BOOTSTRAP_CONCURRENT_RETRY") {
|
|
447
|
+
let winner;
|
|
448
|
+
try {
|
|
449
|
+
winner = databaseFileStat(p);
|
|
450
|
+
} catch (winnerError) {
|
|
451
|
+
if (!(winnerError && winnerError.code === "AGENTLAS_DATABASE_MULTILINK" && activeBootstrapBuildExists(p))) {
|
|
452
|
+
throw new Error(`Empty Agentlas database could not be replaced: ${winnerError && winnerError.message ? winnerError.message : winnerError}`);
|
|
453
|
+
}
|
|
454
|
+
waitingForConcurrentBootstrap = true;
|
|
455
|
+
waitForBootstrapOwner();
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (winner && winner.size >= SQLITE_MIN_BYTES && stableSqliteWinner(p, winner)) {
|
|
459
|
+
securePrivateMode(p, 0o600);
|
|
460
|
+
return { created: false, path: p };
|
|
461
|
+
}
|
|
462
|
+
if (activeBootstrapOwnerExists(p)) {
|
|
463
|
+
waitingForConcurrentBootstrap = true;
|
|
464
|
+
waitForBootstrapOwner();
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
167
468
|
throw new Error(`Empty Agentlas database could not be replaced: ${error && error.message ? error.message : error}`);
|
|
168
469
|
}
|
|
169
470
|
}
|
|
471
|
+
if (!emptyCandidateResolved) {
|
|
472
|
+
throw new Error("Empty Agentlas database could not be replaced: concurrent bootstrap did not publish a database");
|
|
473
|
+
}
|
|
170
474
|
const schemaFile = path.join(PKG_ROOT, "engine", "bootstrap-schema.sql");
|
|
171
475
|
if (!exists(schemaFile)) {
|
|
172
476
|
throw new Error(`Bootstrap schema not found: ${schemaFile}`);
|
|
@@ -209,9 +513,32 @@ function bootstrapDbIfMissing() {
|
|
|
209
513
|
// EEXIST only means another path entry won the name. It is a successful
|
|
210
514
|
// concurrent bootstrap only if that winner is already a real, non-empty
|
|
211
515
|
// database file; a symlink/directory/empty placeholder must fail closed.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
516
|
+
let winnerAccepted = false;
|
|
517
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
518
|
+
let winner;
|
|
519
|
+
try {
|
|
520
|
+
winner = databaseFileStat(p);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
if (error && error.code === "AGENTLAS_DATABASE_MULTILINK" && activeBootstrapBuildExists(p)) {
|
|
523
|
+
waitForBootstrapOwner();
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
if (stableSqliteWinner(p, winner)) {
|
|
529
|
+
winnerAccepted = true;
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
if (winner.size < SQLITE_MIN_BYTES) {
|
|
533
|
+
throw new Error(`Concurrent Agentlas database bootstrap produced an invalid or changed file: ${p}`);
|
|
534
|
+
}
|
|
535
|
+
// The winning process can unlink its temporary hard link between the
|
|
536
|
+
// winner stat and the descriptor's final stat. Retry the bounded identity
|
|
537
|
+
// check once the link count settles; never accept an unstable winner.
|
|
538
|
+
waitForBootstrapOwner();
|
|
539
|
+
}
|
|
540
|
+
if (!winnerAccepted) {
|
|
541
|
+
throw new Error(`Concurrent Agentlas database bootstrap did not publish a stable database: ${p}`);
|
|
215
542
|
}
|
|
216
543
|
} finally {
|
|
217
544
|
try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
|
|
@@ -229,10 +556,8 @@ function bootstrapDbIfMissing() {
|
|
|
229
556
|
|
|
230
557
|
function main() {
|
|
231
558
|
const args = process.argv.slice(2);
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|| args[0] === "--version"
|
|
235
|
-
|| args[0] === "-V";
|
|
559
|
+
const { options: outputOptions } = parseOutputFlags(args);
|
|
560
|
+
const metadataOnly = isMetadataOnlyInvocation(args);
|
|
236
561
|
const sqliteDriver = probeSqliteDriver();
|
|
237
562
|
const engineFound = exists(ENGINE);
|
|
238
563
|
|
|
@@ -259,18 +584,18 @@ function main() {
|
|
|
259
584
|
}
|
|
260
585
|
|
|
261
586
|
if (error) {
|
|
262
|
-
process.stderr.write(error + "\n");
|
|
587
|
+
process.stderr.write(renderError(new Error(error), outputOptions) + "\n");
|
|
263
588
|
process.exit(1);
|
|
264
589
|
}
|
|
265
590
|
|
|
266
591
|
if (!metadataOnly) {
|
|
267
592
|
try {
|
|
268
593
|
const boot = bootstrapDbIfMissing();
|
|
269
|
-
if (boot.created) {
|
|
594
|
+
if (boot.created && outputOptions.format === "table" && !outputOptions.quiet) {
|
|
270
595
|
process.stderr.write("First run: Agentlas data initialized.\n");
|
|
271
596
|
}
|
|
272
597
|
} catch (e) {
|
|
273
|
-
process.stderr.write(
|
|
598
|
+
process.stderr.write(renderError(e, outputOptions) + "\n");
|
|
274
599
|
process.exit(1);
|
|
275
600
|
}
|
|
276
601
|
}
|
|
@@ -310,4 +635,4 @@ function main() {
|
|
|
310
635
|
|
|
311
636
|
if (require.main === module) main();
|
|
312
637
|
|
|
313
|
-
module.exports = { bootstrapDbIfMissing, dbPath, userDataDir, probeSqliteDriver };
|
|
638
|
+
module.exports = { bootstrapDbIfMissing, dbPath, userDataDir, probeSqliteDriver, isMetadataOnlyInvocation };
|
package/engine/acp/server.cjs
CHANGED
|
@@ -131,6 +131,21 @@ function productionTurnRunner(options = {}) {
|
|
|
131
131
|
const s = sessions.get(acpSessionId);
|
|
132
132
|
if (s) { try { s.kill(); } catch { /* best effort */ } }
|
|
133
133
|
},
|
|
134
|
+
closeSession(spec) {
|
|
135
|
+
if (!spec) return;
|
|
136
|
+
const session = spec.session;
|
|
137
|
+
if (session) {
|
|
138
|
+
try { session.kill(); } catch { /* best effort */ }
|
|
139
|
+
}
|
|
140
|
+
if (spec.acpSessionId && sessions.get(spec.acpSessionId) === session) {
|
|
141
|
+
sessions.delete(spec.acpSessionId);
|
|
142
|
+
}
|
|
143
|
+
if (spec.orch && typeof spec.orch.shutdown === "function") {
|
|
144
|
+
try { spec.orch.shutdown(); } catch { /* best effort */ }
|
|
145
|
+
}
|
|
146
|
+
spec.session = null;
|
|
147
|
+
spec.orch = null;
|
|
148
|
+
},
|
|
134
149
|
};
|
|
135
150
|
}
|
|
136
151
|
|
|
@@ -168,7 +183,18 @@ class AcpAgentServer {
|
|
|
168
183
|
if (!msg || typeof msg !== "object" || !msg.method) return;
|
|
169
184
|
void this.handle(msg);
|
|
170
185
|
});
|
|
171
|
-
rl.on("close", () => {
|
|
186
|
+
rl.on("close", () => {
|
|
187
|
+
this.closed = true;
|
|
188
|
+
// stdin을 닫은 ACP client는 이 서버의 모든 session 소유권을 내려놓았다.
|
|
189
|
+
// idle conversation의 Orchestrator도, 진행 중 native child도 남기지 않는다.
|
|
190
|
+
for (const spec of this.sessions.values()) {
|
|
191
|
+
try {
|
|
192
|
+
if (typeof this.runner.closeSession === "function") this.runner.closeSession(spec);
|
|
193
|
+
else this.runner.cancel(spec.acpSessionId);
|
|
194
|
+
} catch { /* connection close must still settle */ }
|
|
195
|
+
}
|
|
196
|
+
this.sessions.clear();
|
|
197
|
+
});
|
|
172
198
|
return new Promise((resolve) => rl.on("close", resolve));
|
|
173
199
|
}
|
|
174
200
|
|