@quolu/lattice 0.57.0 → 0.57.1
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/docs/bridge-setup.md
CHANGED
|
@@ -98,6 +98,24 @@ bridgeが無効な間は以下すべてnullで、bridgeを有効にしている
|
|
|
98
98
|
lattice bridge reconfigure --json
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
### 公開面から自分のprojectが消えた時(`last_heartbeat`)
|
|
102
|
+
|
|
103
|
+
hubへ繋いでいる端末では、`runtime.last_heartbeat`が最後にhubへ名乗った結果を持つ。公開一覧で
|
|
104
|
+
自分のprojectがofflineになっている時、原因が端末側か配線側かはここで分かれる。
|
|
105
|
+
|
|
106
|
+
| `state` | 意味 | 打つ手 |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| `accepted` | 全件受理された。公開面に出ていないならhub側の可視性設定を見る | — |
|
|
109
|
+
| `partial` | 一部が他の生きた端末に所有されている。`rejected_projects`が名指しする | 意図した端末なら放置。奪うなら`adopt` |
|
|
110
|
+
| `rejected` | hubがrequest全体を拒否した。`detail`にtyped code | detailのcodeで分岐 |
|
|
111
|
+
| `unreachable` | hubへ届かない。配線かhubの停止 | hubのURLと生死を確認 |
|
|
112
|
+
| `skipped_no_projects` | 配信しているprojectが0件。名乗るものが無い | 正常な静止。公開したいならそのrepoで作業する |
|
|
113
|
+
| `skipped_no_dashboard` | dashboard daemonを観測できない。配信そのものが立っていない | daemonの生死を見る。`todo`系commandを1回打てば起動する |
|
|
114
|
+
| `null` | hub未設定、またはまだ1回も送っていない | — |
|
|
115
|
+
|
|
116
|
+
名乗る集合はdaemonが実際に配信している集合そのもの(ADR 0165)。登録簿の`last_seen_at`が古い
|
|
117
|
+
ことは露出に影響しない——**人がCLIを叩かなくなっただけで公開面から消えることはない**。
|
|
118
|
+
|
|
101
119
|
### 焼き込むnode pathの選び方(ADR 0163)
|
|
102
120
|
|
|
103
121
|
常駐設定へ焼くnodeのpathは、版付きの実体(homebrewの`Cellar/node/<version>/bin/node`、nvm-windowsの
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.57.
|
|
3
|
+
"version": "0.57.1",
|
|
4
4
|
"description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Quo / クオ at kitepon.dev",
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Terminal-side bridge-hub heartbeat client (bh3).
|
|
3
3
|
*
|
|
4
4
|
* Wires the pure wire contract in `bridge-hub-protocol.mjs` (bh1) around this
|
|
5
|
-
* terminal's own state: a persisted terminal identity, the
|
|
6
|
-
*
|
|
5
|
+
* terminal's own state: a persisted terminal identity, the project set this
|
|
6
|
+
* terminal is actually serving (asked of the dashboard daemon itself, ADR
|
|
7
|
+
* 0165), and the configured hub origin
|
|
7
8
|
* (`bridge-config.mjs`'s `hub` field). It sends `POST /__lattice/hub/register`
|
|
8
9
|
* on `bridge-hub-server.mjs`'s (bh2) contract — the request body is the raw
|
|
9
10
|
* `lattice.bridge_hub_registration_request.v1` object, unmodified in transit.
|
|
@@ -21,7 +22,7 @@ import path from 'node:path';
|
|
|
21
22
|
|
|
22
23
|
import { bridgeConfigPaths } from './bridge-config.mjs';
|
|
23
24
|
import { BRIDGE_HUB_HEARTBEAT_INTERVAL_MS, validateBridgeHubRegistrationRequest } from './bridge-hub-protocol.mjs';
|
|
24
|
-
import {
|
|
25
|
+
import { readServedTodoDashboardProjectIds } from './todo-dashboard-registry.mjs';
|
|
25
26
|
|
|
26
27
|
export { BRIDGE_HUB_HEARTBEAT_INTERVAL_MS };
|
|
27
28
|
|
|
@@ -180,7 +181,7 @@ export function bridgeHubHeartbeatSummary(result, at = null) {
|
|
|
180
181
|
export function createBridgeHubHeartbeatController({
|
|
181
182
|
env = process.env, fetchImpl = fetch, now = () => Date.now(),
|
|
182
183
|
intervalMs = BRIDGE_HUB_HEARTBEAT_INTERVAL_MS,
|
|
183
|
-
|
|
184
|
+
readServedProjectIds = readServedTodoDashboardProjectIds,
|
|
184
185
|
} = {}) {
|
|
185
186
|
let lastSentAt = null;
|
|
186
187
|
let lastResult = null;
|
|
@@ -198,13 +199,18 @@ export function createBridgeHubHeartbeatController({
|
|
|
198
199
|
const nowMs = now();
|
|
199
200
|
if (lastSentAt !== null && nowMs - lastSentAt < intervalMs) return lastResult;
|
|
200
201
|
lastSentAt = nowMs;
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
// 名乗る集合は、この端末が実際に配信している集合そのものを使う。登録簿から別途
|
|
203
|
+
// 数え直すと、配信している側と名乗る側が別々の基準を持ち、必ずずれる(ADR 0165)。
|
|
204
|
+
const projectIds = await readServedProjectIds({ env });
|
|
205
|
+
if (projectIds === null) {
|
|
206
|
+
return record({ schema: HEARTBEAT_RESULT_SCHEMA, state: 'skipped_no_dashboard' }, nowMs);
|
|
207
|
+
}
|
|
208
|
+
if (projectIds.length === 0) {
|
|
203
209
|
return record({ schema: HEARTBEAT_RESULT_SCHEMA, state: 'skipped_no_projects' }, nowMs);
|
|
204
210
|
}
|
|
205
211
|
const terminalId = await readOrCreateBridgeHubTerminalId({ env });
|
|
206
212
|
const request = buildBridgeHubRegistrationRequest({
|
|
207
|
-
terminalId, port: config.listen.port, projectIds
|
|
213
|
+
terminalId, port: config.listen.port, projectIds,
|
|
208
214
|
});
|
|
209
215
|
return record(
|
|
210
216
|
await sendBridgeHubHeartbeat({ hubUrl: config.hub.url, request, fetchImpl }), nowMs,
|
|
@@ -157,6 +157,34 @@ export async function readVisibleTodoDashboardProjects({
|
|
|
157
157
|
|| left.project_id.localeCompare(right.project_id, 'en'));
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* この端末がいま実際に配信しているproject id。登録簿を自分で数え直さず、配信している
|
|
162
|
+
* dashboard daemon自身へ問い、loopback healthをdescriptorと突き合わせてから答える。
|
|
163
|
+
* daemonが居ない・応答しない・descriptorと食い違う時はnull——「配信していない」であって
|
|
164
|
+
* 「0件を配信している」ではない。
|
|
165
|
+
*
|
|
166
|
+
* 公開面へ在ると名乗る集合は、実際に配信している集合と同じでなければならない。別々に
|
|
167
|
+
* 数えると必ずずれる(2026-08-11に実測)。daemonはactive runを持つprojectを配信し続ける
|
|
168
|
+
* のに、hub heartbeatは登録簿の2時間staleだけを見て0件と判断し、送信ごと省いた。中身は
|
|
169
|
+
* 配信できるのに、公開面からは端末ごとofflineになる——障害と見分けがつかない消え方をする。
|
|
170
|
+
*/
|
|
171
|
+
export async function readServedTodoDashboardProjectIds({
|
|
172
|
+
env = process.env, fetchImpl = fetch, timeoutMs = 2_000,
|
|
173
|
+
} = {}) {
|
|
174
|
+
let descriptor;
|
|
175
|
+
try { descriptor = await readJson(paths(env).descriptor, null); } catch { return null; }
|
|
176
|
+
if (!validDaemonDescriptor(descriptor)) return null;
|
|
177
|
+
let body;
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetchImpl(`http://127.0.0.1:${descriptor.port}/__lattice/health`,
|
|
180
|
+
{ signal: AbortSignal.timeout(timeoutMs) });
|
|
181
|
+
body = response.status === 200 ? await response.json() : null;
|
|
182
|
+
} catch { return null; }
|
|
183
|
+
if (body?.schema !== 'lattice.todo_dashboard_health.v1' || body.pid !== descriptor.pid
|
|
184
|
+
|| body.port !== descriptor.port || !Array.isArray(body.project_ids)) return null;
|
|
185
|
+
return body.project_ids.filter(identifier).sort((left, right) => left.localeCompare(right, 'en'));
|
|
186
|
+
}
|
|
187
|
+
|
|
160
188
|
/**
|
|
161
189
|
* 見るのはrepo_root directoryの存在だけ。`.lattice`の有無で判定すると、storeを持たない
|
|
162
190
|
* 生きたrepoまで登録簿から消える。ENOENT/ENOTDIR以外のerrorは「消えた証拠」ではないので
|