import { twinWarn } from './log.js' /* * 「이 트윈에 계측이 들어오고 있나」 — **그 사실을 한 자리에서 묻는다.** * * ── 무엇이 보이지 않았나 (2026-08-14) ─────────────────────────────────────── * 미러 트윈은 커널이 돌기만 하면 「도는 중」이라 보고했다. 그런데 계측을 나르는 피드가 떨어져 있으면 * 그 트윈은 **아무것도 받지 않으면서 도는 중**이다 — 화면에서 두 상태가 똑같이 보였고, 사람은 * 「값이 안 변하네」로 뒤늦게 알게 됐다(재기동 뒤 실제로 그랬다). * * ── 왜 장부가 하나여야 하나 ──────────────────────────────────────────────── * 피드를 여는 곳이 둘이다: 커넥터 기반(`reference-live`)과 프리셋 기반(앱의 `provision-from-preset`). * 한쪽만 보고 「끊김」이라 말하면 **멀쩡히 흐르는 트윈을 끊겼다고 부르게 된다** — 거짓 경보는 참 경보를 * 죽인다. 그래서 각 계층이 자기 장부를 여기 **등록**하고, 묻는 쪽은 이 한 자리에만 묻는다. * * 호스트는 어떤 어댑터·프리셋이 있는지 몰라도 된다(계층을 거꾸로 잇지 않는다). */ /** 지금 피드가 붙어 있는 인스턴스 id 들 — 그 계층의 장부가 답한다. */ export type LiveFeedProbe = () => string[] const probes: LiveFeedProbe[] = [] /** 피드 장부를 등록한다(모듈 로드 시 한 번). */ export function registerLiveFeedProbe(probe: LiveFeedProbe): void { probes.push(probe) } /** * 피드가 붙어 있나 — 어느 장부든 그렇다고 하면 그렇다. * * 장부가 터지는 것을 이 판정 전체의 실패로 만들지 않는다: 한 계층이 답하지 못해도 다른 계층의 사실은 * 그대로 참이다. 다만 **알리지 않고 넘기지 않는다** — 답하지 못한 장부는 로그에 남긴다. */ export function liveFeedAttached(instanceId: string): boolean { for (const probe of probes) { try { if (probe().includes(instanceId)) return true } catch (e: any) { twinWarn('[twin-live] a feed registry could not answer —', e?.message ?? e) } } return false } /** * 이 트윈의 **피드 사실** — 화면이 그대로 말할 수 있는 세 값. * * `not-applicable` 을 `detached` 와 섞지 않는다(이 시스템이 내내 지켜 온 「없음 ≠ 해당 없음」): * · 시뮬 트윈에는 나를 피드가 **없어야 한다** — 그건 결함이 아니라 그 트윈의 성질이다. * · 멈춘 트윈에게 피드를 묻는 것은 뜻이 없다 — 사람이 멈춰 둔 것이다. * · 남는 것 하나가 우리가 보이려던 사실이다: **도는 미러인데 아무것도 오지 않는다.** */ export type LiveFeedState = 'attached' | 'detached' | 'not-applicable' export function liveFeedStateOf(args: { restartPolicy?: string; running?: boolean; instanceId: string }): LiveFeedState { if (args.restartPolicy !== 'resync') return 'not-applicable' if (!args.running) return 'not-applicable' return liveFeedAttached(args.instanceId) ? 'attached' : 'detached' }