Compare commits

..

1 commit

Author SHA1 Message Date
oskar b124e54e66 feat(ai-cluster): manifest + zrodla pod SOLARIA (autoring, cutover NIE wykonany)
Stack ai-cluster (openclaw, codex/planner/service-ops workery, redis, mosquitto)
biega na VPS spoza GitOps, z /home/dockeruser/docker/ai-cluster/. Branch
feat/vps-service-migration (862c04a) probowal wciagnac go do GitOps *w miejscu*,
na VPS. Ten etap pomijamy: workloady sa compute'owe, VPS ma 4 GiB bez swapu i rolę
ingressu, SOLARIA ma 64 GiB i GPU. Migrujemy od razu na SOLARIA, VPS zostaje samym
NPM-em proxujacym po Tailscale.

Zero deployu. Kontenery na VPS nietkniete — recon byl read-only (katalog nalezy do
dockeruser, odczyt przez efemeryczny kontener z mountem :ro, bez dotykania
dzialajacych kontenerow). Plan przelaczenia: services/ai-cluster/CUTOVER.md.

Zrodla wciagniete do services/ai-cluster/src/ (openclaw, worker, mosquitto).
Pominiete: .env i mosquitto/passwd (sekrety), start-codex.sh* (smiec spoza stacku),
__pycache__.

Ustalenia z reconu, ktore zmienily manifest z 862c04a:

- codex-worker/ na VPS to NIE osobny build context ani duplikat worker/ — to
  martwy 609-bajtowy prototyp na `requests` (biblioteka nie wystepuje w zadnym
  requirements.txt), bez Dockerfile'a i bez referencji w compose. Realny
  codex-worker buduje sie z ./worker (Dockerfile default). Nie przenoszony.
- image: ai-cluster-* -> build: z src/openclaw i src/worker (3 Dockerfile'e nad
  jednym kontekstem: default/planner/service-ops).
- openclaw: usuniete publish 0.0.0.0:8000 -> "100.100.231.104:8000:8000";
  usunieta siec npm_default (nie istnieje na SOLARIA).
- mosquitto: bind 100.95.58.48 -> 100.100.231.104.
- mem_limity urealnione (256m workery/openclaw, 64m redis/mosquitto) i przeniesione
  do hosts/solaria/runtime/. Zmierzone RSS na VPS: workery 7-10 MiB, openclaw
  24.6 MiB, redis 4.8, mosquitto 4.2 — limity to zapas ~10x, nie reakcja na presje.
- healthcheck zostaje na python/urllib: obraz openclaw (python:3.12-slim) nie ma
  ani wget, ani curl — sprawdzone na zywym obrazie.
- GATEWAY_BASE_URL http://piha:8080 -> http://100.108.208.3:8080. Tailscale DNS
  jest WYLACZONY na SOLARIA (`tailscale dns status` -> disabled), wiec zadna nazwa
  MagicDNS sie nie rozwiazuje. Na VPS bare `piha` tez nie rozwiazuje sie (rc=2),
  czyli ten default byl martwy juz tam.
- telegram-bot nie przenoszony (na VPS nie biega zaden kontener). telegram_bot.py
  zostaje w src/, bo openclaw/Dockerfile go COPY-uje — bez niego build sie wywala.
- sekrety wylacznie przez env_file /opt/homelab/config/ai-cluster/.env, zero
  interpolacji ${VAR:?} — stack nie zalezy od cwd deployu. service-ops-worker
  montuje ten katalog pod ta sama sciezka absolutna, bo compose waliduje env_file
  na etapie parsowania i inaczej `docker compose ps` w tym kontenerze padnie.
- AGENT_ID zostaja `vps-*`: to cele routingu MQTT, nie hostnamey. Zmiana nazw
  lamie producentow adresujacych konkretnego agenta — osobne, skoordynowane zadanie.

Konsumenci mosquitto do aktualizacji: w repo BRAK. chelsty (z2m, frigate,
stability-agent) uzywaja wlasnego, host-networkowego brokera i musza zostac
offline-first. Lista i sposob potwierdzenia klientow spoza repo — CUTOVER.md krok 5.

DoD (docker build + smoke + pytest) swiadomie odroczone: zadanie zabrania budowania
obrazow. Pokrywaja to kroki 1-3 CUTOVER.md, do wykonania w sesji cutoveru.
Zweryfikowane tutaj: `docker compose config` z overrideem (rc=0, bindy host_ip,
mem_limity, merge volumes), skladnia YAML, `bash -n healthcheck.sh`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:30:00 +02:00
432 changed files with 7207 additions and 27659 deletions

View file

@ -1,116 +0,0 @@
---
name: kb-authoring
description: Conventions for writing/editing source knowledge-base documents under kb/**/*.md (frontmatter schema, type taxonomy, visibility default, validation). Trigger whenever creating or editing a file under kb/ — this is about authoring the sources, not publishing kb-site (see kb-publish for that).
---
## Scope
This skill governs **writing kb/\*\*/\*.md documents themselves** — the OKF-format
knowledge base sources. It is not about publishing them: for "should I run the
publish script", see the `kb-publish` skill, referenced again at the bottom
here.
`docs/sessions/*.md` files use `type: session-log` frontmatter and are validated
by the same script, but they are **not** kb/ documents — they are a running
diary of work sessions. **Never convert a session log into a kb/ doc** and
never move/copy a kb/ doc's content into docs/sessions/. If material in a
session log deserves a permanent home (a decision, an incident writeup, a
runbook), write a **new** file under kb/ that captures it properly — don't
relocate the diary entry.
## Frontmatter schema (OKF v0.1)
Every kb/ document opens with a YAML frontmatter block (`---` ... `---`) as
the first thing in the file. Validated by `scripts/kb/check_okf.py` — read
that file if you need the exact rules; summary:
| Field | Required | Values | Notes |
|---|---|---|---|
| `okf` | yes | `"0.1"` | Pinned. Always this literal string, quoted. |
| `type` | yes | one of the taxonomy below | Determines which `kb/<type>s/` directory the file lives in. |
| `visibility` | yes | `private` \| `public` | **Default is `private`.** See below — never default to `public`. |
| `status` | yes | `active` \| `deprecated` \| `planned` | `planned` = decisions not yet made / work not yet started. `deprecated` requires `superseded_by`. |
| `updated` | yes | `YYYY-MM-DD` | Bump every time you edit the file's content. |
| `links` | yes (may be `[]`) | list of paths | Relative to **this file's own directory**, e.g. `../phases/backlog.md` or `sibling-doc.md`. Every entry must resolve to an existing file — check_okf verifies this. |
| `as_of` | only for `type: audit` | `YYYY-MM-DD` | Required exactly when `type: audit`, forbidden otherwise. See "audit" below for why this is a separate field from `updated`. |
| `superseded_by` | only when `status: deprecated` | free text or path | Required exactly when `status: deprecated`, forbidden otherwise. Points to whatever replaced this doc (a doc path, or prose describing the replacement if there's no single successor file). |
| `contradicts` | optional | list | Free-text notes about known conflicts with other sources (e.g. "CLAUDE.md says X, but the directory doesn't exist"). Entries that look like a `.md` path (contain `/` and end in `.md`) are checked for existence like `links`; plain-text entries are not. |
| `stub` | optional | bool | Marks a doc as a placeholder/incomplete. Must be a real YAML bool (`true`/`false`), not a string. |
## Type taxonomy — kb/<type>s/
| `type` | Directory | What goes here |
|---|---|---|
| `node` | `kb/nodes/` | One doc per physical/virtual host — role, configured services, runtime data paths. Mirrors `hosts/<node>/`. |
| `service` | `kb/services/` | One doc per deployed service — what it is, how it's used/configured. Mirrors `services/<svc>/`. |
| `subsystem` | `kb/subsystems/` | Cross-cutting architecture/design docs describing how something works *in general* (access model, agent system, deployment conventions) — not tied to a single node or service. Kept in sync with current reality (unlike `audit`, see below). |
| `decision` | `kb/decisions/` | A choice that was made (or is still open, `status: planned`) plus its rationale — forward-looking, governs future behavior. Gets edited in place and re-dated as the decision evolves; it is not a historical log of what happened. |
| `incident` | `kb/incidents/` | A factual account of something that broke: symptom, root cause, fix/status, at a point in time. Slug conventionally date-prefixed (`YYYY-MM-DD-short-description.md`) since incidents are anchored to when they happened. Content generally stays close to the as-happened account rather than being rewritten into "current state" prose. |
| `runbook` | `kb/runbooks/` | Step-by-step operational procedure — deploy, install, recover, troubleshoot. Imperative, command-heavy, meant to be followed live. |
| `phase` | `kb/phases/` | A project/milestone plan broken into steps, tracking progress (including backlog/roll-up index docs). |
| `audit` | `kb/audits/` | A **point-in-time snapshot** of actual/verified state (ground truth recon), never an ongoing description. Requires `as_of` — the date the finding was true — kept distinct from `updated` (the date the doc text was last edited) precisely because an audit's findings can go stale even when nobody touches the file. Slug conventionally date-suffixed (`topic-YYYY-MM-DD.md`). |
`session-log` also exists as a `type` value (for `docs/sessions/`) but is **out
of scope for `kb/`** — see Scope above.
### Decision vs incident vs runbook vs audit — how ambiguous cases got resolved
This came up repeatedly during the kb/ migration (docs that mixed genres got
split, not force-fit into one type):
- **decision vs incident**: a doc that both narrates "here's what broke" *and*
states "here's the guardrail we adopted because of it" is two documents.
Split the incident account into `kb/incidents/`, keep (or extract) the
resulting decision/guardrail into `kb/decisions/`. Example:
`home-assistant/DESIGN.md``kb/decisions/ha-configs-as-code.md` +
`kb/incidents/2026-07-22-ha-dwie-instancje.md`.
- **decision vs runbook**: if a decision doc contains a reusable operational
recipe (install steps, recovery procedure), that section is a runbook, not
part of the decision's rationale. Split it out. Example:
`deploy-runner``kb/services/job-deploy-runner.md` (how it works) +
`kb/decisions/deploy-runner-uzasadnienie.md` (why) +
`kb/runbooks/deploy-runner-install.md` (how to install/operate it).
- **subsystem vs audit**: a `subsystem` doc is the *maintained* description of
how something is designed/intended to work — you keep it in sync. An
`audit` is a *frozen* investigation result ("I checked X on this date and
found Y") — you don't rewrite it as things change, you write a new audit
or a decision/incident instead. This is why `audit` got its own `as_of`
field distinct from `updated`.
- When in doubt, prefer splitting a doc across two types over stretching one
type's frontmatter to cover mixed content — that's the pattern the
migration itself followed (see git log `feat(kb): SPLIT ...` commits for
worked examples).
## Visibility default: private
**`visibility` defaults to `private`.** Every new document must be written
`private` unless the operator has explicitly and consciously decided it
should be `public` — never infer or default to `public` on your own, even if
the content looks harmless. `visibility: public` documents are the only ones
`scripts/kb/gen_pages.py` will ever emit to the public kb-site (fail-closed:
missing/unparseable/unrecognized `visibility` is treated as private).
## After every change to kb/**/*.md
Run the validator and fix anything it flags before considering the edit done:
```bash
python3 scripts/kb/check_okf.py
```
It checks frontmatter parses, `okf` is pinned, `type`/`visibility`/`status`
are from their closed lists, dates are well-formed, `as_of`/`superseded_by`
are present exactly when required, and every `links`/path-like `contradicts`
entry resolves to a real file. A red run means something is broken — fix it,
don't skip it.
## Creating a new document
Use `scripts/kb/new-doc.sh <type> <slug> [--public]` to scaffold a
correctly-placed file with valid frontmatter, then fill in the content.
## If you just made a public change
If a file you created or edited carries `visibility: public`, don't forget
the KB site itself doesn't update on its own — see the `kb-publish` skill for
the one-line reminder to run `scripts/kb/publish.sh`.

View file

@ -1,42 +0,0 @@
---
name: kb-publish
description: At session close, asks the operator whether to publish the KB site if kb/**/*.md changed this session. Trigger whenever the session is wrapping up (save-session, "kończymy sesję", natural end of task) and this session's own edits, or a merge/diff visible in git, touched files under kb/.
---
## What this skill does
Trigger point: **session close**, not every response. Session close means the
operator signals they're wrapping up — e.g. invoking `save-session`, saying
something like "kończymy sesję" / "koniec na dziś" / "wrap up", or the task
reaching its natural end with no further work queued. Don't act on this skill
mid-session just because a `kb/` file was touched.
At that moment, if this session touched `kb/**/*.md` — either through your
own edits or through a merge/diff you observed in git — **ask the operator
directly**, as a question requiring a yes/no answer, not a passive reminder:
> Sesja dotknęła kb/. Zregenerować i zasugerować publikację (`bash scripts/kb/publish.sh`)?
- If the operator confirms (any affirmative reply): print the ready-to-paste
command block so they can copy it straight into their own shell:
```bash
bash scripts/kb/publish.sh
```
Do not run it yourself — see below.
- If the operator declines: drop it, nothing more to do.
- If the operator doesn't respond or ignores the question (moves on to
something else, ends the conversation): do **not** ask again in this
session. One ask per session, max.
## What this skill does NOT do
**Never run `scripts/kb/publish.sh` yourself**, under any circumstances, even
if the operator's task prompt says to deploy, publish, or calibrate. The
script SSHes into PIHA and overwrites the live `kb-site` content volume on
production — that is out of scope for a worktree agent and requires an
explicit, direct instruction from the operator in the current turn.
If the operator explicitly asks you to run it, that instruction stands on its
own — this skill only governs the session-close question, not that request.

View file

@ -93,7 +93,7 @@ services:
preflight fills `arch`, `ram_mb`, `docker_present`, `mm_runtime` — do NOT guess these.
Full schema: `kb/runbooks/node-onboarding-tool.md`.
Full schema: `scripts/onboard/README.md`.
---

2
.gitignore vendored
View file

@ -22,8 +22,6 @@ venv/
*.egg-info/
packages/*/build/
jobs/*/build/
# wyjscie generatorow (scripts/kb/gen_pages.py -> build/kb-site/) — artefakt, nie zrodlo
build/
# Tools
.aider*

View file

@ -1,9 +0,0 @@
{
"mcpServers": {
"ha": {
"command": "./services/ha-mcp/run.sh",
"args": [],
"env": {}
}
}
}

View file

@ -40,7 +40,7 @@ Pipeline stages: **prepare → validate → deploy → verify → diagnose (on f
## Node Onboarding
New nodes are onboarded via `scripts/onboard/` — an idempotent bash tool driven by
`hosts/<node>/node.yaml` manifests (no Ansible). See `kb/runbooks/node-onboarding-tool.md` for
`hosts/<node>/node.yaml` manifests (no Ansible). See `scripts/onboard/README.md` for
the full schema, step status table, and gotchas.
Key fields in `node.yaml`: `ssh_user`, `first_contact` (LAN IP — not `.local`),
@ -90,26 +90,14 @@ The platform uses a multi-agent model with **human-in-the-loop** for destructive
Agent → /opt/homelab/actions/pending/<id>.json
→ Telegram notification → Operator approves
→ /opt/homelab/actions/approved/<id>.json
→ Executor dispatches to the target node → completed / failed
→ Executor runs → completed / failed
```
The executor never connects to a node (deliberate — see kb/phases/backlog.md
"Remediacja floty bez SSH"). It writes a dispatch file that the node collects:
| Action type | Inbox | Executed on the node by |
|---|---|---|
| `container_restart` | `actions/dispatch/<node>/` | node-agent (own docker socket) |
| `redeploy` | `actions/deploy/<node>/` | deploy-runner, host-level systemd (`jobs/deploy-runner/`) |
Both report back with an `action_result` event, which the executor turns into
`completed` / `failed`. `disk_cleanup` and `alert_only` still resolve inside the
executor.
Agents must never execute destructive actions (restarts, deploys, config changes) without a corresponding approved action file.
## Event System
Events are append-only, one JSON file per event, flat under `/opt/homelab/events/<node>/evt-<node>-<unixts>-<type>[-<service>].json`.
Events are append-only JSON lines at `/opt/homelab/events/YYYY-MM-DD/<node>/events.jsonl`.
Emit via `scripts/lib/events.sh` (shell) or `scripts/lib/events.py` (Python).
@ -120,8 +108,8 @@ Normalized event types: `deployment_started/completed/failed`, `service_unhealth
| Event type | Source | Action generated | Cooldown |
|---|---|---|---|
| `containers_not_running` | stability-agent | `container_restart` | dedup via stable ID |
| `healthcheck_failed` | node-agent | `container_restart` | dedup via stable ID |
| `service_unhealthy` / other | stability-agent | `redeploy` → dispatched to the node's deploy-runner (`jobs/deploy-runner/`) | dedup via stable ID |
| `mqtt_unreachable` | stability-agent | `container_restart` | dedup via stable ID |
| `service_unhealthy` / other | stability-agent | `redeploy` | dedup via stable ID |
| `disk_pressure` (high) | stability-agent | `disk_cleanup` | dedup via stable ID |
| `ha_websocket_dead` | ha-diag-agent | `container_restart` (homeassistant) | 30 min after completion |
| `ha_websocket_recovered` | ha-diag-agent | cancels matching restart | — |

View file

@ -31,29 +31,27 @@ Action approval flow: `pending/` → operator approves → `approved/` → execu
## Repository Structure
- `docs/`: [Infrastructure Standards](kb/subsystems/standards.md) and [Deployment Conventions](kb/subsystems/deployment.md).
- `kb/phases/subsystem-a-naprawa.md`: [Current Maintenance Plan (Control Plane)](kb/phases/subsystem-a-naprawa.md).
- `docs/`: [Infrastructure Standards](docs/standards.md) and [Deployment Conventions](docs/deployment.md).
- `hosts/`: Host-specific configurations and service assignments.
- `services/`: Reusable Docker Compose service definitions.
- `scripts/`: Deployment and management scripts.
## Getting Started
1. **Standardization**: Follow the [Infrastructure Standards](kb/subsystems/standards.md).
2. **Deployment**: See [Deployment Conventions](kb/subsystems/deployment.md) for how to roll out changes.
1. **Standardization**: Follow the [Infrastructure Standards](docs/standards.md).
2. **Deployment**: See [Deployment Conventions](docs/deployment.md) for how to roll out changes.
3. **SATURN**: Remember that SATURN is the only node where commits should be made.
## Documentation Index
- [Current Maintenance Plan (Control Plane)](kb/phases/subsystem-a-naprawa.md)
- [Infrastructure Standards](kb/subsystems/standards.md)
- [Agent Operating Procedures](kb/subsystems/agent-operating-procedures.md) (For AI/Non-Human Agents)
- [Deployment Conventions](kb/subsystems/deployment.md)
- [Hardware](kb/nodes/legacy-hardware.md)
- [Networking](kb/subsystems/networking.md)
- [Services](kb/subsystems/legacy-services-list.md)
- [Node Capabilities](kb/subsystems/capability-model.md)
- [Action Model](kb/subsystems/action-approval-model.md)
- [Infrastructure Standards](docs/standards.md)
- [Agent Operating Procedures](docs/agents.md) (For AI/Non-Human Agents)
- [Deployment Conventions](docs/deployment.md)
- [Hardware](docs/hardware.md)
- [Networking](docs/networking.md)
- [Services](docs/services.md)
- [Node Capabilities](docs/capabilities.md)
- [Action Model](services/agent-system/action-model.md)
---
*Note: This repository documents the state of the homelab. Runtime state lives outside the repository in `/opt/homelab`.*

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "kb/subsystems/fleet-inventory.md + hosts/<node>/capabilities.yaml (stub z 2026-04-15, sprzed floty)"
---
# Access
## Description

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-05-20
links: []
---
# Agent Operating Procedures
This document defines the operating procedures, constraints, and interaction protocols for non-human agents (AI agents, autonomous scripts) within the Homelab Codex ecosystem.
@ -15,9 +6,9 @@ This document defines the operating procedures, constraints, and interaction pro
1. **Read-Only by Default**: Agents should assume read-only access to the `/opt/homelab` runtime unless explicitly executing an approved action.
2. **Git as Authority**: The repository on **SATURN** is the source of truth. Agents must not modify the runtime state on nodes directly without corresponding (or pending) Git state, unless it's an emergency mitigation.
3. **Human-in-the-Loop (HIL)**: All destructive or structural changes (restarts, deployments, config changes) must follow the [Action Approval Model](action-approval-model.md).
3. **Human-in-the-Loop (HIL)**: All destructive or structural changes (restarts, deployments, config changes) must follow the [Action Approval Model](../services/agent-system/action-model.md).
4. **Idempotency**: All scripts and actions proposed or executed by agents MUST be idempotent.
5. **Context-Awareness**: Agents MUST read the `README.md` and `kb/subsystems/agent-operating-procedures.md` at the start of every session to align with current infrastructure standards.
5. **Context-Awareness**: Agents MUST read the `README.md` and `docs/agents.md` at the start of every session to align with current infrastructure standards.
## 2. Agent Roles

1209
docs/backlog.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-05-20
links: []
---
# Node Capability Model
This document defines the capability model for the homelab infrastructure. The goal is to provide a declarative way to describe what each node can do, its constraints, and its suitability for various workloads.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: node
visibility: private
status: active
updated: 2026-05-27
links:
- ../runbooks/chelsty-deploy-recovery.md
---
# CHELSTY Runtime
This document describes the runtime environment and deployment flow for CHELSTY, an offline-capable home automation edge node split across two VMs.
@ -110,6 +100,48 @@ services:
Remove `monitor: false` once node-agent is bootstrapped on this VM.
## Deployment Flow
### Initial Bootstrap
```bash
./scripts/bootstrap/chelsty-runtime.sh
```
### Deploy services
```bash
./scripts/deploy/deploy-node.sh chelsty-infra
./scripts/deploy/deploy-node.sh chelsty-ha
```
### Manual (SSH) — chelsty-infra uses docker-compose v1
```bash
ssh oskar@100.122.201.22
cd ~/homelab-codex-ws/services/<service>
docker-compose -f docker-compose.yml \
-f ../../hosts/chelsty-infra/runtime/<service>/docker-compose.override.yml \
up -d --build --force-recreate
```
> **Note:** `docker compose` (v2) is **not** available on chelsty-infra — always use `docker-compose` (hyphenated, v1 1.29.2).
## Recovery Procedures
### Mosquitto stopped
```bash
ssh oskar@100.122.201.22 "docker start mosquitto"
# Ensure restart policy is correct:
docker update --restart unless-stopped mosquitto
```
### Zigbee2MQTT won't start
1. Check logs: `docker logs zigbee2mqtt --tail 50`
2. Verify SLZB-06U reachable from host: `nc -zv 192.168.1.105 6638`
3. Verify config is not empty: `cat /opt/homelab/data/zigbee2mqtt/data/configuration.yaml`
4. If config missing, recreate from the minimal template above
### SLZB-06U unreachable
`192.168.1.105:6638` EHOSTUNREACH means the coordinator is offline or the LAN is down. Zigbee2MQTT will keep retrying — no restart needed once the coordinator returns.
## Critical Backup Sets
| Data | Path |

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: service
visibility: private
status: active
updated: 2026-05-20
links: []
---
### CHELSTY Stability Agent
The stability-agent on CHELSTY provides local observability and health monitoring for the node's services and infrastructure.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "kb/subsystems/fleet-inventory.md + hosts/<node>/capabilities.yaml (stub z 2026-04-15, sprzed floty)"
---
# Core Stack
## Description

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: active
updated: 2026-06-25
links:
- ../incidents/deploy-sh-vps-niszczy-control-plane.md
---
# Deployment Conventions
This document describes the GitOps-lite deployment process for the homelab.
@ -20,6 +10,21 @@ This document describes the GitOps-lite deployment process for the homelab.
4. **Tailscale Mesh**: All hosts are connected via Tailscale, allowing secure communication without public port exposure.
5. **Host Autonomy**: Services that must operate during WAN or Git outages keep their runtime dependencies on the execution node or local LAN.
## ⚠️ ZNANY BUG — `deploy.sh vps` niszczy control-plane (2026-06-25)
`deploy.sh vps` uruchamia `deploy-node.sh` w pętli po wszystkich serwisach VPS, w tym
`control-plane`. Pętla używa innego `COMPOSE_PROJECT_NAME` niż `deploy-local.sh`
(który uruchamiany jest z `cwd=services/control-plane`). Niezgodność project-name powoduje
`Recreate``No such container``set -e` przerywa pętlę → **observer, supervisor,
executor i operator-ui znikają z VPS.**
**Dopóki bug nie zostanie naprawiony (backlog — Krytyczny):**
- Do deployu control-plane używać: `ssh -t vps 'cd ~/homelab-codex-ws && cd services/control-plane && bash deploy-local.sh'`
- Inne serwisy VPS deployować punktowo: `deploy-node.sh` z `--service <name>` lub przez SSH + `docker compose up -d`
- **NIE uruchamiać `deploy.sh vps` bez pełnej świadomości ryzyka.**
---
## Staged Deployment Framework
The homelab uses a modularized staged deployment framework located at `scripts/deploy/deploy.sh`. This script is designed to be resumable, stage-aware, and observable, with core logic split into maintainable libraries in `scripts/lib/`.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-05-12
links: []
---
# Homelab Event System
The homelab multi-agent platform uses a filesystem-first event architecture for observability, auditability, and agent reasoning.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: node
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "hosts/<node>/capabilities.yaml + kb/subsystems/fleet-inventory.md"
---
# Hardware
## Description

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: node
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "kb/nodes/vps.md + kb/subsystems/fleet-inventory.md (stub z 2026-04-15, sprzed floty)"
---
# Hetzner VPS
## Description

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: active
updated: 2026-07-03
links: []
---
# Inwentaryzacja floty homelab-codex — 2026-06-30
Zebrano: 2026-06-30 17:09 CEST

View file

@ -1,17 +1,8 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: active
updated: 2026-07-02
links: []
---
# Weryfikacja inwentaryzacji floty 2026-06-30 — stan na 2026-07-02
Zebrano: 2026-07-02 ~15:20 CEST (read-only recon, zero zmian na nodach).
Metoda: ssh + `docker ps -a / inspect / logs`, `free/df/nproc/lscpu/lsblk`, `git branch/log` (odczyt),
`curl` do fleet-prometheus API. Porównanie z `kb/subsystems/fleet-inventory.md` (23 rozjazdy)
`curl` do fleet-prometheus API. Porównanie z `docs/infra/inventory-2026-06-30.md` (23 rozjazdy)
oraz z repo na `master` (HEAD `22adfb1`).
Dostępność nodów podczas weryfikacji:

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: audit
visibility: private
status: active
updated: 2026-07-16
as_of: 2026-07-16
links: []
---
# Recon: lustro `event=dead prom=up` — 1507 mismatchy w shadow-liveness.log (2026-07-16)
READ-ONLY recon. Zero zmian w kodzie/serwisach. Wszystkie czasy **UTC**

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: audit
visibility: private
status: active
updated: 2026-07-14
as_of: 2026-07-14
links: []
---
# Monitoring coverage — co biega vs co jest monitorowane (recon 2026-07-14)
**Pytanie:** czy wszystkie serwisy floty są monitorowane?
@ -131,7 +121,7 @@ compose-service kontenera.
| owntracks-prometheus-exporter-prometheus-owntracks-exporter-1 | linusgroh/prometheus-owntracks-exporter | Up 2w | 0.0.0.0:8780→80 |
| own-tracks-frontend-owntracks-frontend-1 | owntracks/frontend | Up 2w | 0.0.0.0:8084→80 |
Zmiany vs audyt 2026-06-30 (`kb/subsystems/fleet-inventory.md`): **przybyły** paperless,
Zmiany vs audyt 2026-06-30 (`docs/infra/inventory-2026-06-30.md`): **przybyły** paperless,
paperless-db, paperless-broker (Deploy 1, 2026-07-10); **zniknęły** diskover i elasticsearch
(w audycie 06-30 były w 33 shadow; dziś nie biegają). 06-30: 40 kontenerów → dziś: 42.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-07
links: []
---
# Migracja okit.pl: 42.pl (FreeDNS) -> Cloudflare — plan faz
Cel: okit.pl na Cloudflare (jak kapala.org) -> wildcard *.okit.pl DNS-01 ->

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: runbook
visibility: private
status: active
updated: 2026-07-16
links: []
---
# Ollama SOLARIA: manual → declarative cutover runbook
Date: 2026-07-15
@ -97,7 +88,7 @@ bind-mount the existing model directory.**
docker exec ollama ollama ps
```
3. Embeddings endpoint + vector dimension (deferred check from
`kb/phases/kb-m5-faza2.md` §6 step 2):
`docs/kb/modules/05-faza2-plan.md` §6 step 2):
```bash
curl -s http://localhost:11434/api/embeddings -d '{"model":"bge-m3","prompt":"test"}' \
| python3 -c "import json,sys; v=json.load(sys.stdin)['embedding']; print(len(v))"
@ -145,7 +136,7 @@ SOLARIA:
- Given the missing driver, the cutover proceeded **in CPU-only mode**: the
`deploy.resources` GPU reservation was commented out in
`services/ollama/docker-compose.yml` (commit `f57a01a`), and the driver fix
was filed as a backlog item (see `kb/phases/backlog.md`) blocking the module 5
was filed as a backlog item (see `docs/backlog.md`) blocking the module 5
mail-embedding phase.
- **2026-07-16: driver fixed.** Installed `nvidia-driver-595-open` from the
distro repository — not the old `ppa:graphics-drivers/ppa` (jammy), which

View file

@ -1,16 +1,6 @@
---
okf: "0.1"
type: audit
visibility: private
status: active
updated: 2026-07-02
as_of: 2026-07-02
links: []
---
# Audyt odchudzania PIHA — 2026-07-02
> Faza 1 (READ-ONLY) modulu 0 filaru dokumentow (`kb/phases/kb-m0-piha-slim.md`).
> Faza 1 (READ-ONLY) modulu 0 filaru dokumentow (`docs/kb/modules/00-piha-slim.md`).
> Zadna akcja nie zostala wykonana — wylacznie `docker stats/inspect/logs`, `ss`, `curl` (odczyt).
> Stan w momencie audytu: **RAM 7.9Gi total, 5.0Gi used, 2.9Gi available; swap 4Gi total, 2.0Gi uzyty.**
> 41 kontenerow Up (inwentaryzacja 2026-06-30 liczyla 40; wszystkie nadal biega).

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: audit
visibility: private
status: active
updated: 2026-07-06
as_of: 2026-07-06
links: []
---
# Prometheus liveness cutover — recon starego toru (2026-07-06)
Read-only recon przed cutoverem liveności floty na Prometheus `up{}`. Mapa obecnego
@ -300,7 +290,7 @@ renderuje `health`/`status`/`last_seen`), `/unhealthy` (`:302-334`), `/summary`,
chelsty/chelsty-infra (exporter DOWN po LTE, 2026-06-26).
- Reguła `NodeDown` (`rules/liveness.yml:21-28`): `up{node=~"vps|piha"} == 0`,
`for: 5m`, severity critical. solaria/lustro świadomie wykluczone (`:12-16` —
planned power-off; docelowo anomaly detection, `kb/phases/backlog.md:390-406`).
planned power-off; docelowo anomaly detection, `docs/backlog.md:390-406`).
Bez Alertmanagera by design (`:3-7`) — delivery = brain-watchdog poll `/api/v1/alerts`.
- node_exporter w repo tylko dla VPS (`services/node_exporter/`, network_mode: host,
`hosts/vps/services.yaml:36-43`). Exportery na piha/solaria/lustro **nie mają
@ -348,7 +338,7 @@ chelsty-infra, chelsty-ha, lustro.
| lustro | TAK (`:50-52`) | NIE | TAK (bez stability-agenta) | jw. |
| **chelsty-infra** | **NIE** (`:57-60`, exporter DOWN po LTE) | NIE | **TAK** (remote TTL 900/3600, `liveness.py:43-50`) | **tylko stary tor — cutover totalny zostawiłby go bez liveności** |
| chelsty-ha | NIE | NIE | NIE (`hosts/chelsty-ha/services.yaml:6-12`, `monitor: false`) | już dziś bez liveności (pośrednio przez MQTT chelsty-infra) — cutover nic nie zmienia |
| saturn | NIE (`:55`, laptop) | NIE | NIE (brak `hosts/saturn/services.yaml`, `kb/phases/backlog.md:423`) | już dziś bez liveności — cutover nic nie zmienia |
| saturn | NIE (`:55`, laptop) | NIE | NIE (brak `hosts/saturn/services.yaml`, `docs/backlog.md:423`) | już dziś bez liveności — cutover nic nie zmienia |
**Chelsty offline ~34 dni — jak traktuje go stara rura:** eventy buforują się lokalnie
(rsync fail = non-fatal, `node_agent.py:560-566`), `last_seen` na VPS zamrożone sprzed
@ -361,7 +351,7 @@ totalnym chelsty-infra nie miałby żadnej liveności i żadnego przejścia offl
Dodatkowo docs sygnalizują konflikt IP w komentarzach `prometheus.yml:57` vs
`hosts/chelsty-infra/host.yaml:12` — do wyjaśnienia przy ewentualnym dodawaniu scrape.
*(rzeczywisty bieżący stan chelsty — do weryfikacji na żywo; ostatni zapis:
UNREACHABLE, `kb/subsystems/fleet-inventory-verify.md:17,151`)*
UNREACHABLE, `docs/infra/inventory-verify-2026-07-02.md:17,151`)*
**Wniosek twardy:** cutover NIE może być globalny. Docelowa architektura to
**hybryda per-node**: `up{}` dla scrape'owanych (vps, piha, solaria, lustro),
@ -460,7 +450,7 @@ z `last_seen` — rzadszy heartbeat przy niezmienionych TTL-ach = fałszywe degr
- chelsty-infra: zbadać exporter-over-LTE (`prometheus.yml:57-60` + konflikt IP
z `hosts/chelsty-infra/host.yaml:12`); do tego czasu zostaje na torze eventowym.
- NodeDown dla solaria/lustro: świadomie odroczone do anomaly detection
(`kb/phases/backlog.md:390-406`) — nie wciągać do cutoveru.
(`docs/backlog.md:390-406`) — nie wciągać do cutoveru.
- Watchdog na sam Prometheus (D.2 pkt 5) — mały task przy etapie 3.
- saturn / chelsty-ha: świadomie poza monitoringiem — status quo.
@ -468,7 +458,7 @@ z `last_seen` — rzadszy heartbeat przy niezmienionych TTL-ach = fałszywe degr
**Seria.** Minimalnie trzy taski implementacyjne + weryfikacje między nimi:
(1) etap 1 shadow-read; (2) etap 3 flaga per-node (po tygodniu etapu 2);
(3) watchdog-na-Prometheusa + aktualizacja `kb/subsystems/observer.md`.
(3) watchdog-na-Prometheusa + aktualizacja `docs/observer-runtime.md`.
Etap 0 to czynność operatorska (runtime, nie repo). Etap 5 to niezależny backlog.
---

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-15
links: []
---
# Prometheus cutover — Etap 2: analiza zgodności shadow-run (2026-07-15)
Analiza READ-ONLY logów `SHADOW_LIVENESS_MISMATCH` observera (parallel-run od
@ -164,7 +155,7 @@ Skutek uboczny do zaakceptowania świadomie: syntetyczne `node_stale`/
wyłączeniu **wcześniej, ale nie liczniej** — te eventy już dziś powstają co noc
(21:32/21:39 dla lustro, każdorazowo dla solaria). Cutover nie zwiększa wolumenu
alertów. Docelowe wyciszenie planowych okien off to wątek anomaly-detection
z backlogu (`kb/phases/backlog.md:390-406`) — **niezależny od cutoveru i nieblokujący**;
z backlogu (`docs/backlog.md:390-406`) — **niezależny od cutoveru i nieblokujący**;
`NodeDown` dla solaria/lustro słusznie pozostaje wyłączony.
### Rekomendowany mapping (potwierdzenie rekomendacji z recon F/Etap 2)

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: audit
visibility: private
status: active
updated: 2026-07-27
as_of: 2026-07-27
links: []
---
# Audyt niezarządzanych stacków na VPS — 2026-07-27
Recon read-only przed konsolidacją do GitOps. Zebrane przez `ssh vps` (user `oskar`,

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: service
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "brak katalogu services/joplin/ w repo — patrz contradicts w kb/subsystems/repo-operating-contract.md"
---
# Joplin Server
## Description

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-16
links: []
---
# Eval-set: pilot retrieval (faza 2, krok 7) — 2026-07-16
Stan bazy: document_chunk = 2683 chunki (bge-m3, dim 1024), 160 dokumentów z 186 kopert paperless.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: active
updated: 2026-06-26
links:
- ../decisions/kb-log-decyzji.md
---
# Baza wiedzy — przegląd i log decyzji (homelab-codex · KB)
> Master-dokument inicjatywy. Stoi ponad dokumentami per-projekt (`kb-01-email-design.md`, …).
@ -92,6 +82,21 @@ Dwa dolne tiery są per-filar i neutralne. Dwa górne są wspólne dla wszystkic
---
## Decyzje — zamknięte vs otwarte
**Zamknięte:**
- Spine: Postgres + pgvector (nie Qdrant).
- Embed: **bge-m3** (multilingual, długi kontekst — pod polski lepszy niż multilingual-e5).
- Załączniki: indeksowane w **II turze** (MVP najpierw czysty tekst).
- Warstwa 3 startuje jako **cienki graf encji**; federacja przy zapytaniu dochodzi później (docelowo hybryda).
- Dokumenty: Nextcloud + Paperless-ngx.
**Otwarte:**
- **Transakcje:** agregator vs CSV, pokrycie mBanku, Revolut, koszt (filar #4).
- **Maile §design:** sizing archiwum / node (ile waży Gmail), unifikacja adaptera (jeden IMAP dla obu vs JMAP+IMAP osobno).
---
## Tor równoległy (nie tutaj)
Hardening homelabu / stabilizacja control-plane — osobny wątek.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: active
updated: 2026-08-06
links: []
---
# Filar maili — projekt (homelab-codex · KB · projekt #1)
> Pierwszy filar. Wzorzec referencyjny dla pozostałych (archiwum, embeddingi na SOLARIA, szkielet agenta, deploy).
@ -27,22 +18,9 @@ Realna potrzeba to najpierw **archiwum**, nie „RAG nad mailami". Dwie warstwy,
Gmail **nie jest porzucany** — zostaje jako konto śmieciowe / loginy / 2FA. Stąd maile mają dwa żywe wejścia:
- **Fastmail**`source: fastmail`, adapter **IMAP** (hasło aplikacji). Primary: tu ląduje sensowna poczta na przyszłość.
- **Fastmail**`source: fastmail`, adapter **JMAP** (read-only token). Primary: tu ląduje sensowna poczta na przyszłość.
- **Gmail**`source: gmail`, adapter **IMAP** (protokół, nie Gmail API → przenośność). Ciągły sync żywej skrzynki.
> **Korekta 2026-08-06 — Fastmail przez IMAP, nie JMAP.** Do tej daty ten dokument (i §7, §9
> oraz §10 planu fazy mailowej) przewidywał dla Fastmaila **JMAP** i osobny `jobs/fastmail-poller`.
> Zapis historyczny: *„Fastmail — adapter JMAP (read-only token)"*, 2026-06-24.
>
> Decyzja z 2026-08-06 (recon `kb/audits/mail-sync-2026-08-06.md` Decyzja (b), zatwierdzona
> przez operatora) domyka otwartą od czerwca decyzję „unifikacja adaptera" z §9 na rzecz
> **jednego wspólnego IMAP-a dla obu kont**, w jednym jobie `jobs/mail-imap-sync`. Powody:
> JMAP synchronizuje po `state` — elegancko i niepotrzebnie przy jednym ticku na godzinę
> i ~37 mailach na dobę, skoro UIDVALIDITY/UIDNEXT rozwiązuje ten sam problem i tak trzeba go
> zaimplementować dla Gmaila; jeden adapter to jeden zestaw testów, jedna klasa błędów i jedna
> ścieżka hardeningu 8-bitowych nagłówków. JMAP nie jest zamknięty na zawsze — koperta
> i archiwum są protokołowo obojętne, więc wymiana transportu nie dotyka danych.
Plus jednorazowy **bulk historyczny Gmaila** (eksport „All Mail" / Takeout → surowy dump do archiwum). Operacja odwracalna i niezależna od reszty pipeline'u — robimy pierwsza. Urgency spadła (konto żyje), ale historia warta zassania od razu.
---
@ -94,9 +72,7 @@ Załączniki: **II tura** (MVP = czysty tekst + nagłówki).
## 7. Deploy
- Wszystko w `homelab-codex`, przez Git na SATURN, konwencja override `hosts/<node>/runtime/<svc>/`.
- Usługi: `mail-imap-sync` (Fastmail + Gmail, jeden job — korekta 2026-08-06; wcześniej
planowane jako osobne `jmap-poller` + `imap-poller`), `indexer`, embed (ollama na SOLARIA),
`postgres+pgvector`, `mail-agent`; bulk importer jako one-shot job.
- Usługi: `jmap-poller` (Fastmail), `imap-poller` (Gmail), `indexer`, embed (ollama na SOLARIA), `postgres+pgvector`, `mail-agent`; bulk importer jako one-shot job.
- Deploy skryptem czytającym `inventory/topology.yaml`.
---
@ -105,10 +81,8 @@ Załączniki: **II tura** (MVP = czysty tekst + nagłówki).
1. ✅ Zamroź kopertę + postaw Postgres+pgvector. *(2026-06-17)*
2. ✅ **Bulk Gmail historyczny → archiwum**`jobs/gmail-bulk-import/`**KOD GOTOWY** *(2026-06-24)*; nie uruchomiony (Takeout ~27 GB na SOLARIA, do transferu na PIHA).
3. ~~Fastmail JMAP live ingest~~**Fastmail IMAP live sync** → archiwum. *(korekta
2026-08-06; kod gotowy, pierwszy żywy run po stronie operatora —
`kb/runbooks/mail-sync-run.md`)*
4. Gmail IMAP live sync → archiwum. *(j.w. — ten sam job `jobs/mail-imap-sync`)*
3. Fastmail JMAP live ingest → archiwum.
4. Gmail IMAP live sync → archiwum.
5. Filtr archiwum→indeks.
6. Indexer (parse → chunk → embed bge-m3) → pgvector.
7. Cienki agent maili + tool dla warstwy 4.
@ -117,12 +91,8 @@ Załączniki: **II tura** (MVP = czysty tekst + nagłówki).
## 9. Decyzje otwarte (do przyklepania przed/w trakcie startu)
- ✅ **Sizing Gmaila** — ZAMKNIĘTE: 225 030 kopert, archiwum ~27 GB na PIHA (Etap B, 2026-08-06).
- ✅ **Unifikacja adaptera** — ZAMKNIĘTE 2026-08-06 na rzecz **jednego IMAP-a** dla obu kont
(Decyzja (b) reconu, uzasadnienie w §2 wyżej).
- **Sizing Fastmaila** — OTWARTE, i celowo: przesądza o tym, czy ciągniemy historię konta czy
tylko przyrost. Rozstrzyga pomiar `mail-imap-sync --measure`, nie zgadywanie —
`kb/runbooks/mail-sync-run.md` §5.
- **Sizing Gmaila** — ile realnie waży „All Mail"? (przesądza node/dysk archiwum).
- **Unifikacja adaptera** — jeden wspólny IMAP dla Fastmail + Gmail (mniej kodu) vs JMAP dla Fastmail + IMAP dla Gmail (JMAP bogatszy)?
- **Reguły filtra** — startowa lista blacklist domen/nagłówków.
- Vector store: pgvector **przyklepane** (spine).
- Embed model: bge-m3 **przyklepane**.

View file

@ -1,18 +1,9 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: active
updated: 2026-07-01
links: []
---
# KB filar #2 — Dokumenty (Nextcloud + Paperless) — design
> Dokument-master filaru dokumentow. Stoi pod `kb-00-overview.md`.
> Cel: kazda sesja / Claude Code startuje z pelnym kontekstem decyzji.
> Status: ARCHITEKTURA ZAMKNIETA (2026-07-01), implementacja modulowa czeka.
> Moduly implementacyjne: `kb/phases/kb-m*.md` — puszczane CC jeden po drugim.
> Moduly implementacyjne: `docs/kb/modules/0X-*.md` — puszczane CC jeden po drugim.
---

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-02
links: []
---
# Modul 0 — Odchudzic PIHA (prerekwizyt filaru dokumentow)
> Prerekwizyt modulow 2/4 (Paperless/Nextcloud na PIHA). Bez tego PIHA nie ma
@ -15,7 +6,7 @@ links: []
## STATUS: prerekwizyt RAM SPELNIONY (2026-07-02)
Faza 1 (audyt read-only) + faza 2 (egzekucja po review Oskara) wykonane —
szczegoly: `kb/audits/piha-slim-2026-07-02.md` (sekcja "Korekta po review
szczegoly: `docs/infra/piha-slim-audit-2026-07-02.md` (sekcja "Korekta po review
+ egzekucja").
- **Kryterium >= 1.5Gi available: SPELNIONE.** Przed egzekucja: 2.8Gi available
@ -37,7 +28,7 @@ Zwolnic RAM na PIHA (dzis: 3.1Gi available, swap 2G uzyty) tak, by lekki Paperle
serwis wszedl z zapasem, nie na styku swap.
## Wymogi
- Audyt 33 shadow-kontenerow (lista w `kb/subsystems/fleet-inventory.md`).
- Audyt 33 shadow-kontenerow (lista w `docs/infra/inventory-2026-06-30.md`).
- Zidentyfikowac kandydatow do usuniecia/przeniesienia/wylaczenia:
- **elasticsearch 1Gi** — kto tego uzywa? (wikijs? diskover?) — jesli martwy, ubic
- **diskover** — jednorazowy indekser? czy chodzi ciagle bez potrzeby?

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: decision
visibility: private
status: active
updated: 2026-07-01
links: []
---
# Modul 1 — SSO Forgejo-OIDC (decyzja + wzorzec wpiecia)
> Fundament tozsamosci dla filaru dokumentow (i szerzej homelaba). Zapisuje decyzje

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-01
links: []
---
# Modul 2 — Paperless-ngx serwis (na PIHA)
> Serwis dokumentow: UI+API+Postgres+Redis. Always-on na PIHA. OCR-worker OSOBNO

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-01
links: []
---
# Modul 3 — Paperless OCR-worker (SOLARIA + fallback PIHA)
> Ciezki OCR odseparowany od serwisu. Worker na SOLARIA (moc), fallback PIHA (wolno).

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-02
links: []
---
# Modul 4 — Nextcloud (drive/WebDAV + OIDC)
> Drugi adapter dokumentow: zamiennik Google Drive, dowolne pliki + sync. Zrodlo

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-02
links: []
---
# Modul 5 — Ingest dokumentow -> koperta KB
> Adapter obu zrodel (Paperless API + Nextcloud WebDAV) -> koperta KB. Domyka filar #2

View file

@ -1,32 +1,16 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-08-06
links: []
---
# Moduł 5, faza mailowa — treść maili w retrievalu (RECON + PLAN)
> Status (2026-08-06): Kroki 0-5 WYKONANE na żywo (chunker wydzielony, hybrid
> retrieval, Etap A apply na żywej bazie, bramka jakościowa **PASS** — patrz §8).
> **Etap B (Krok 6) ZAMKNIĘTY 2026-08-06**: pełny korpus gmail jest zchunkowany
> i zembedowany (389 012 chunków, zero nie-excluded bez wektora) — patrz §9
> „Wynik Etapu B". **Krok 7 (przyrostówka IMAP) — IN PROGRESS 2026-08-06**:
> recon `kb/audits/mail-sync-2026-08-06.md` wykonany, decyzje operatora (a)-(g)
> **zatwierdzone w całości 2026-08-06**, implementacja w repo (§10 „Zakres
> wdrożony"). **Kod nie łączył się z żywym kontem** — pierwszy sync, pomiar
> Fastmaila i aktywacja timera to kroki operatora wg
> `kb/runbooks/mail-sync-run.md`.
> Status (2026-07-23): Kroki 0-4 WYKONANE na żywo (chunker wydzielony, hybrid
> retrieval, Etap A apply na żywej bazie), Krok 5 (bramka jakościowa) **PASS**
> — patrz §8 dla liczb i werdyktu. Etap B (pełne archiwum) i Krok 7 (recon
> IMAP/JMAP) wciąż przed nami.
>
> Kontynuacja `05-faza4-plan.md` (faza 4: `packages/kb-retrieval` wydzielone,
> serwis `kb-query` z UI działa na PIHA — „KB po raz pierwszy odpowiada przez
> HTTP", 2026-07-22, `docs/sessions/2026-07-22.md`). Faza mailowa = odpowiedź na
> feedback operatora z POC wyszukiwarki: **„mało danych, brak połączeń"**.
> W punkcie wyjścia (2026-07-22) 225 030 kopert gmail miało w bazie tylko
> nagłówki — treści leżały wyłącznie w archiwum .eml na PIHA (stan zamknięty
> Etapem B, §9). Ta faza wprowadza treści maili do `document_chunk`
> 225 030 kopert gmail ma dziś w bazie tylko nagłówki — treści leżą wyłącznie
> w archiwum .eml na PIHA. Ta faza wprowadza treści maili do `document_chunk`
> i udostępnia je w retrievalu. **To nadal wyszukiwarka, nie chat** — synteza,
> Drive Takeout i backfill 70k załączników PDF pozostają poza zakresem (§11).
@ -468,8 +452,7 @@ i idempotencji (drugi przebieg = zero insertów). Definition of Done z CLAUDE.md
`hybrid_query` analogicznie do `cascade_query` (jeden embed zapytania).
- `kb-query`: `mode` pattern `^(cascade|flat|hybrid)$`; **domyślny `mode`
przełączany na `hybrid` dopiero po PASS bramki (§8)** — do tego czasu
hybrid dostępny jawnie. *(Wykonane 2026-08-06: default = `hybrid`, patrz
DoD (d) w §12.)* Wyniki gmail w UI już obsłużone (faza 4: subject/from
hybrid dostępny jawnie. Wyniki gmail w UI już obsłużone (faza 4: subject/from
z headers + „Kopiuj Message-ID").
- Testy jednostkowe na mockach (merge, pusta gałąź summary, pusta gałąź mail).
@ -584,8 +567,7 @@ płonił bramki co uruchomienie).
**`kb-query` domyślny `mode`**: przełączenie na `hybrid` jako follow-up (poza
zakresem tego zamknięcia bramki — `kb-query`'s `mode` param zmiana to osobna,
mała zmiana w serwisie, nie w `packages/kb-retrieval`). **Wykonane 2026-08-06**
po regresji na pełnym korpusie (Etap B) — szczegóły przy DoD (d) w §12.
mała zmiana w serwisie, nie w `packages/kb-retrieval`).
## 9. Krok 6 — Etap B: pełne archiwum
@ -607,139 +589,8 @@ po regresji na pełnym korpusie (Etap B) — szczegóły przy DoD (d) w §12.
**Szacunek: 1 sesja (run w tle).**
### Decyzje operatora do Etapu B (2026-08-04) — przed runem
Recon przed Etapem B (mirror archiwum na SOLARII żyje: 225 057 plików / 27 GB; RTT
SOLARIA→PIHA 0,83 ms; PIHA 140 GB wolne, baza 397 MB; M1 — `NODE_TYPE=lte_node`
na node-agencie SOLARII — zdeployowane, więc kontener Ollamy nie zniknie po
zatrzymaniu) wykazał dwie rzeczy do rozstrzygnięcia. Decyzje:
1. **Run w plastrach po 50k** (`--limit 50000 --offset 0/50k/100k/150k/200k`),
log per plaster, `nice`/`ionice`. Powód: brak checkpointu (restart = ponowny
parse od początku listy, ~1 h) + nocne wyłączanie SOLARII. Plaster ≈ 2540 min.
Tempo kolejnych plastrów po obserwacji PIHA po pierwszym.
2. **Circuit breaker w jobie: TAK**`--max-embed-failures` (domyślnie 5),
abort z kodem wyjścia 2 po N kolejnych nieudanych batchach embed. Powód:
parse jest jednowątkowy i wyprzedza GPU, więc martwa Ollama (4 incydenty)
zamieniłaby 2-godzinny przebieg w 200k+ `chunks_errors` bez ani jednego
zapisu. Licznik zeruje się po udanym batchu.
3. **Dry-run całości pomijamy** — idempotencja i odwracalność flag newsletterowych
wystarczają; ewentualna kalibracja heurystyki na dekadzie 20102015 po fakcie,
na już zapisanych flagach.
4. Przełączenie domyślnego `mode` kb-query na `hybrid` (DoD (d)) — **poza zakresem
Etapu B**, osobny task po PASS regresji. *(Wykonany 2026-08-06 — DoD (d) w §12.)*
### Hardening toru embed przed Etapem B (2026-08-05)
Recon pod kątem batchingu potwierdził, że batch `/api/embed` (Krok 1) działa zgodnie
z §1.4 — ale wykazał w torze backfillu **błąd blokujący dla Etapu B** i dwie luki:
1. **BUG (naprawiony)**: `flush_embed_buffer` łapał wyłącznie `aiohttp.ClientError`, a
wyczerpanie `ClientTimeout(total=...)` rzuca goły `builtins.TimeoutError`, który **nie**
jest jego podklasą (zweryfikowane empirycznie na aiohttp 3.14.3). Zawieszona Ollama —
czyli dokładnie jej udokumentowany failure mode, „przyjmuje połączenie i milczy", nie
„odmawia" — wywalała cały run nieobsłużonym wyjątkiem: **bez breakera i bez flushu
threadingu**. Na plastrze 50k oznaczało to utratę już zarobionej pracy. Klasy przejściowe
nazwane teraz jawnie w `kb_retrieval.embed.TRANSIENT_EMBED_ERRORS`.
2. **Brak retry** — jeden blip sieciowy spisywał na straty cały batch (64 chunki). Dodane:
`--embed-retries` (default 2) z backoffem wykładniczym.
3. **Brak obsługi błędu częściowego**`/api/embed` jest all-or-nothing, więc jeden trujący
chunk zabijał batch w kółko i mógł wywalić breaker przy **żywym** backendzie. Dodana
bisekcja po nieudanych retry, ale tylko gdy `/api/tags` potwierdza, że backend żyje;
przy martwym batch od razu „gives up" (bisekcja martwego backendu kosztowałaby 2n-1
żądań i opóźniała breaker). Porażka częściowa **nie** przesuwa już breakera.
Decyzja 2 (circuit breaker) obowiązuje w zaostrzonej formie: licznik liczy **give-upy**
(backend padł), nie dowolne nieudane batche. Fallback SOLARIA→PIHA dla backfillu **świadomie
nie powstaje** — 271k chunków × 790 ms CPU ≈ 60 h na 8 GB PIHA dzielonym z HA i Paperlessem;
właściwą odpowiedzią na martwy backend jest exit 2 i wznowienie plastra. Tor online (`kb-query`
`embed_router`) ma fallback i tak zostaje — te dwie ścieżki są rozdzielone celowo.
Doszedł też `mail-body-ingest-bench` — sweep batch size na realnych chunkach (read-only),
żeby liczby z §1.4 dało się odtworzyć po zmianie GPU albo wersji Ollamy.
### Wynik Etapu B (ZAMKNIĘTY, 2026-08-06)
Pełny korpus gmail jest zchunkowany i zembedowany: **389 012 chunków**
`document_chunk`, z czego **0 nie-excluded bez embeddingu** — weryfikacja
przebiegła idempotentnymi plastrami 0-4 (`--offset 0/50k/100k/150k/200k
--limit 50000 --batch-size 64`, wszystkie EXIT 0) plus fix bajtu NUL (`4ec0b78`).
Cross-tab na żywej bazie (kb-postgres@PIHA):
| Miara | Wartość |
|---|---|
| `document_chunk` total | **389 012** |
| nie-excluded **bez** embeddingu | **0** |
| nie-excluded z wektorem | 187 025 |
| `newsletter`-flagged bez wektora | 201 849 (odwracalne, Decyzja 4) |
| excluded **z** wektorem | 138 (artefakt kolejności flagowania, nieszkodliwy) |
**Korpus był w pełni zembedowany jeszcze przed plastrami z 2026-08-06** —
zapamiętany stan „6,4k embedded z Etapu A, ~225k kopert do backfillu" był
nieaktualny, wcześniejsze przebiegi pokryły całość. Dzisiejsze runy to pełna,
idempotentna weryfikacja. Źródło mylącego odczytu: licznik
`chunks_already_embedded` liczy **istnienie wiersza w DB** (w tym chunków
newsletter-flagged bez wektora), a nie obecność wektora.
**Bug NUL (naprawiony, `4ec0b78`)**: bajt `0x00` w treści maili z 2007 (Sony
Ericsson, 3 chunki, plaster offset 50k) wywalał insert
(`asyncpg.CharacterNotInRepertoireError` — PostgreSQL nie przyjmuje `0x00`
w `text`). Fix: strip `\x00` przed chunkowaniem i embedem + liczniki
`nul_bytes_stripped` / `mails_nul_sanitized`. Re-run plastra 1: EXIT 0,
3 chunki dobrane. Znany follow-up (osobny task): `jobs/gmail-header-backfill`
i `jobs/gmail-bulk-import` mają tę samą latentną podatność na NUL w nagłówkach
zapisywanych do `jsonb`.
Szczegóły runu: `docs/sessions/2026-08-06-kb-etapb-backfill.md`.
Uwaga do czytania wyników: na pełnym korpusie `exit 1` jest spodziewany
(pojedyncze `parse_errors` — §1.5 dokumentuje ~9 maili na fallbacku compat32).
Werdyktem jest bilans i liczniki w linii `summary`, nie kod wyjścia. `exit 2`
oznacza co innego: backend embed padł, trzeba wznowić plaster po naprawie Ollamy.
## 10. Krok 7 — IMAP/JMAP przyrostówka (zarys; szczegóły = osobny recon)
> **Stan: IN PROGRESS (2026-08-06).** Recon:
> `kb/audits/mail-sync-2026-08-06.md`; decyzje (a)-(g) zatwierdzone przez
> operatora 2026-08-06 w całości, implementacja opisana niżej. Zarys
> poniżej pochodzi z 2026-07-22 i zachowuję go jako zapis intencji. Recon
> rozstrzyga inaczej dwa jego punkty: (1) **Fastmail przez IMAP, nie JMAP**
> (unifikacja adaptera — jeden `jobs/mail-imap-sync` zamiast
> `fastmail-poller` + `gmail-imap-poller`), (2) spoiwem z torem body nie jest
> `--since`, tylko kolejka „koperty bez chunków". Reszta zarysu (poll zamiast
> IDLE, reuse `save_eml`/`insert_envelope`, sekrety w `/opt/homelab/config/`)
> się potwierdziła.
### Zakres wdrożony (2026-08-06)
| # | Element | Gdzie |
|---|---|---|
| 1 | Adapter IMAP, jeden na oba konta — EXAMINE + `BODY.PEEK[]` (nigdy nie ustawia `\Seen`), wybór folderu po atrybucie SPECIAL-USE | `packages/kb-mail/src/kb_mail/imap.py` |
| 2 | Model stanu synca: `plan_folder_sync` (pierwszy tick / przyrost / unieważnienie UIDVALIDITY) + `contiguous_last_uid` (kursor tylko po nieprzerwanym ciągu sukcesów) | `packages/kb-mail/src/kb_mail/sync_state.py` |
| 3 | Migracja `005_mail_sync_state.sql`, klucz `(account, folder)` — Decyzja (f) | `services/kb-postgres/init/` |
| 4 | Job przyrostówki: fetch → `save_eml``insert_envelope(entities=[headers, attachment…])` → kursor. **Nie chunkuje i nie embeduje** | `jobs/mail-imap-sync/` |
| 5 | Ekstrakcja wspólnego parse'u (`parse_headers`, `message_id`, `parse_date`, `parse_attachments`) do `kb-mail` — klucz dedup z jednej implementacji | `packages/kb-mail/src/kb_mail/{headers,message}.py` |
| 6 | `--sources` + `--only-unchunked` w `mail-body-ingest`; pre-fetch kluczy chunków zawężony do zbioru roboczego | `jobs/mail-body-ingest/` |
| 7 | `DEFAULT_SUMMARYLESS_SOURCES += "fastmail"` — Decyzja (g), w tym samym commicie co źródło | `packages/kb-retrieval/` |
| 8 | Etap mailowy w `kb-ingest` (kolejka „koperty bez chunków") + takt timera 03:30 → co 2 h — Decyzja (d) | `jobs/documents-ingest/` |
| 9 | Jednostki systemd (**nieaktywowane**) + deklaracja jednostek host-level | `jobs/mail-imap-sync/systemd/`, `hosts/piha/jobs.yaml` |
| 10 | Metryki `.prom` per konto + reguła `KbMailSyncStale` | `services/fleet-prometheus/rules/kb-mail-sync.yml` |
| 11 | `env.example` z placeholderami; poświadczenia wyłącznie ze środowiska — Decyzja (c) | `jobs/mail-imap-sync/env.example` |
| 12 | Runbook pierwszego uruchomienia + checklista punktów `[do weryfikacji na żywo]` z reconu | `kb/runbooks/mail-sync-run.md` |
Dokumentacja serwisu: `kb/services/job-mail-imap-sync.md`.
**Poza zakresem tej implementacji, świadomie:** pierwszy żywy sync, pomiar
`STATUS (MESSAGES)` na Fastmailu i wynikająca z niego **decyzja o historii
Fastmaila** (Decyzja (e) — recon celowo jej nie podejmuje, bo zależy od liczby,
której nikt jeszcze nie zna), oraz aktywacja timera. Wszystko to robi operator
wg runbooka. Alert „cisza w skrzynce" **odrzucony** (decyzja operatora, zgodna
z reconem §3.4): zero nowych maili to legalny stan skrzynki, a alert zapalający
się na zdrowym systemie zostaje wyciszony — i przestaje działać wtedy, gdy jest
potrzebny. Jedyny alert to `KbMailSyncStale` („czy poller w ogóle działa"),
który fałszywych trafień nie ma.
Zakotwiczone w kb-00 jako etapy 34 (`jobs/fastmail-poller`,
`jobs/gmail-imap-poller`). Zarys decyzji do tamtego reconu:
@ -766,24 +617,20 @@ Zakotwiczone w kb-00 jako etapy 34 (`jobs/fastmail-poller`,
| `mail_ui_url` (klikalny link do maila w UI) | kb-00 etap 6, pole zarezerwowane w kb-query | z modułem mail-UI |
| Graf wątków / entity_link z `entities[type=threading]` | ta faza tylko zapisuje surowiec (Decyzja 10) | przyszła faza „połączenia" |
| Streszczenia selektywne maili (hybryda Haiku) | Decyzja 6 — odłożona | po ocenie trybu hybrid w praktyce |
| ~~IMAP/JMAP przyrostówka — implementacja~~ | Krok 7 | **wykonane 2026-08-06** (§10 „Zakres wdrożony"); pierwszy żywy sync po stronie operatora |
| Historia Fastmaila (pełny zaciąg vs tylko przyrost) | Decyzja (e) reconu | po pomiarze `mail-imap-sync --measure` — runbook §5 |
| Alert per konto na wiek najnowszego maila (`kb_mail_sync_last_message_ts`) | recon §3.4 | po miesiącu obserwacji; próg z pomiaru, nie z góry |
| Etykiety Gmaila w `entities` (`X-GM-LABELS`) | recon §2.3 | odłożone — `X-GM-EXT-1` przywiązuje kod do Google, wprost wbrew „protokół, nie provider" |
| IMAP/JMAP przyrostówka — implementacja | Krok 7 (zarys) | osobny recon + pakiet |
## 12. Plan implementacji (kolejność = zależności)
| # | Krok | Zależy od | Szacunek | Stan | Dowód (2026-08-04) |
|---|---|---|---|---|---|
| 0 | Chunker → `packages/kb-mail` | — | 0,5 sesji | **WYKONANE** | `348ce10`; `packages/kb-mail/src/kb_mail/chunking.py` + `tests/test_chunking.py` |
| 1 | `embed_batch` w kb-retrieval | — | 0,5 sesji | **WYKONANE** | `51998fd`; `kb_retrieval/embed.py:61` (`embed_batch`) + `tests/test_embed.py` |
| 2 | Job `mail-body-ingest` | 0, 1 | 2 sesje | **WYKONANE** | `ad0ef40` (job), `a95524c` (README), `fc5c698` (fix html_to_text); `jobs/mail-body-ingest/` + `tests/test_ingest.py` |
| 3 | Tryb hybrid (kb-retrieval + kb-query) | — (równolegle z 2) | 1 sesja | **WYKONANE** | `a640cf1`; `kb_retrieval/retrieval.py:112` (`hybrid_retrieve`), `:195` (`hybrid_query`), `kb-query/app/main.py:119` (`mode` pattern). Domyślny `mode` przełączony na `hybrid` 2026-08-06 (follow-up z §8, DoD (d) niżej) |
| 4 | rsync + Etap A (12 mies.) + kalibracja | 2 | 1 sesja | **WYKONANE** | §7 „Wynik Etapu A" (run na żywo 2026-07-23); potwierdzone na żywej bazie 2026-08-04: `document_chunk` gmail = 33 871 (6 398 z embeddingiem + 27 473 `newsletter`) — zgodne co do sztuki z tabelą §7 |
| 5 | Bramka jakościowa (eval mailowy + regresja) | 3, 4 + zapytania od operatora | 1 sesja | **WYKONANE** (PASS) | §8 „Wynik bramki"; `56f64e9` (eval + queries.yaml dla hybrid), `bce635c` (`mail_hit@3`, próg N2, werdykt PASS), `71eb264` (`--transport http`) |
| 6 | Etap B (pełne archiwum) + regresja + obserwacja PIHA | 5 = PASS | 1 sesja | **WYKONANE** (2026-08-06) | §9 „Wynik Etapu B"; żywa baza: 389 012 chunków, 0 nie-excluded bez embeddingu. Weryfikacja plastrami 0-4 (wszystkie EXIT 0) + fix NUL `4ec0b78`; `docs/sessions/2026-08-06-kb-etapb-backfill.md` |
| 7 | Recon przyrostówki IMAP/JMAP | — (po 6) | 1 sesja (poza DoD fazy) | **WYKONANE** (2026-08-06) | `kb/audits/mail-sync-2026-08-06.md`. Ustalenia: korpus urywa się 2026-06-19 (dziura 48 dni ≈ 1 800 maili), zero kodu IMAP w repo, brak modelu stanu synca; cały nowy kod to jeden `jobs/mail-imap-sync` + 4 drobne zmiany w istniejącym torze. Decyzje (a)-(g) zatwierdzone przez operatora 2026-08-06 |
| 8 | Implementacja przyrostówki IMAP | 7 + decyzje (a)-(g) | 2 sesje (poza DoD fazy) | **IN PROGRESS** (2026-08-06) | Zakres w §10 „Zakres wdrożony". Testy zielone: 80 (`mail-imap-sync`) + 111 (`kb-mail`) + 191 (`documents-ingest`) + 127 (`mail-body-ingest` / `kb-retrieval`). **Nie uruchomione na żywym koncie** — pierwszy sync, pomiar Fastmaila i aktywacja timera po stronie operatora (`kb/runbooks/mail-sync-run.md`) |
| # | Krok | Zależy od | Szacunek |
|---|---|---|---|
| 0 | Chunker → `packages/kb-mail` | — | 0,5 sesji |
| 1 | `embed_batch` w kb-retrieval | — | 0,5 sesji |
| 2 | Job `mail-body-ingest` | 0, 1 | 2 sesje |
| 3 | Tryb hybrid (kb-retrieval + kb-query) | — (równolegle z 2) | 1 sesja |
| 4 | rsync + Etap A (12 mies.) + kalibracja | 2 | 1 sesja |
| 5 | Bramka jakościowa (eval mailowy + regresja) | 3, 4 + zapytania od operatora | 1 sesja |
| 6 | Etap B (pełne archiwum) + regresja + obserwacja PIHA | 5 = PASS | 1 sesja |
| 7 | Recon przyrostówki IMAP/JMAP | — (po 6) | 1 sesja (poza DoD fazy) |
**Kryterium ukończenia fazy mailowej:** (a) pełny korpus gmail zchunkowany
(bilans domknięty, `parse_errors` na poziomie pojedynczych sztuk jak
@ -792,28 +639,11 @@ hybrid, (c) bramka §8 PASS wraz z regresją po Etapie B, (d) `kb-query`
domyślnie odpowiada trybem hybrid na `kb.kapala.org`, (e) koperty gmail mają
`entities[type=threading]`.
**(d) SPEŁNIONE w repo 2026-08-06** — domyślny `mode` w `/search` przełączony
`cascade``hybrid` (`services/kb-query/app/main.py`, walidator `Query`;
frontend przestał wysyłać `mode` przy odznaczonym „tryb flat", więc UI
dziedziczy domyślny tryb API). Podstawa: eval na **pełnym** korpusie
(187 025 zembedowanych chunków mailowych w HNSW, §9) —
kryterium 1 (regresja paperless) **PASS**: żaden istniejący hit nie
zdegradował ani we flat, ani w hybrid; mailowe **hit@3 = 5/5**; koszt
hybrydy to jedno dodatkowe zapytanie SQL na wyszukiwanie. Surowe wyniki:
`eval-http-2026-08-06.json` i `eval-direct-2026-08-06.json`
w `~/kb/mail/ingest-logs` na PIHA (celowo niecommitowane — artefakt runu).
Jawne `?mode=cascade` / `?mode=flat` działają bez zmian.
**Deploy na PIHA robi operator z mastera po mergu** — do tego czasu
`kb.kapala.org` nadal odpowiada kaskadą.
## 13. Szacunki zbiorcze
- **Dane**: +~496k wierszy `document_chunk` (~271k z embeddingiem, ~225k
flagowanych `newsletter`); baza 250 MB → ~57 GB (dysk PIHA: 144 GB wolne,
zapas >20×). HNSW rośnie inkrementalnie przy insertach — bez rebuildu.
**Wykonanie (2026-08-06, §9): 389 012 wierszy — 187 025 z embeddingiem,
201 849 flagowanych `newsletter`.** Mniej niż ekstrapolacja z §1.3, bo
quote-strip (Decyzja 2) realnie ucina objętość, co §1.3 zapowiadał.
- **GPU/czas runów**: Etap A <1 h e2e; Etap B: parse ~0,51 h (24 rdzenie)
+ embed ~11,5 h (batch 64, zmierzone 818 ms/chunk) + inserty do PIHA.
- **Koszty zewnętrzne: 0 USD** (bez streszczeń — Decyzja 6).
@ -827,11 +657,6 @@ Jawne `?mode=cascade` / `?mode=flat` działają bez zmian.
## 14. Podsumowanie dla Oskara
> **Uwaga (2026-08-06):** poniższe to podsumowanie z chwili reconu (2026-07-22),
> zachowane jako zapis intencji. Plan został wykonany — treści 225k maili są
> w bazie i w retrievalu (§9 „Wynik Etapu B"); otwarty jest już tylko Krok 7
> (przyrostówka IMAP/JMAP).
Treści Twoich 225 tysięcy maili leżą dziś martwe w 27 GB archiwum na PIHA —
w bazie są tylko nagłówki, a wyszukiwarka z fazy 4 słusznie skarży się „mało
danych". Ten plan wprowadza je do retrievalu w ~7 sesji i za 0 USD: ponowny

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-13
links: []
---
# Modul 5, faza 2 — koperta dokumentow + embeddingi + cross-source (RECON + PLAN)
> Status: RECON ZAKONCZONY (2026-07-13), architektura DO ZATWIERDZENIA. Zaden kod nie
@ -545,7 +536,7 @@ zeby dalo sie uruchomic partiami i zweryfikowac progres bez czekania na cale 225
rzedu dziesiatek-set chunkow/s. Caly pilot (23k chunkow) → **rzedu minut**, nie wymaga
specjalnego batchowania/partii.
- **Skala docelowa (70k zalacznikow z maili)**: modul 5 faza-1 to swiadomie **probka, nie
bulk** (`kb/phases/kb-m5-documents-ingest-fazy.md` — decyzja architektoniczna). Realny wolumen
bulk** (`jobs/documents-ingest/README.md` — decyzja architektoniczna). Realny wolumen
ktory trafi do embeddingu zalezy od (a) throughput OCR-workera na SOLARII (modul 3) —
**to jest waskie gardlo skalowania, nie embedding** — oraz (b) filtra selektywnosci
(decyzja #6). Sam embedding bge-m3 nie bedzie bottleneckiem nawet przy tysiacach

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-08-27
links: []
---
# Moduł 5, faza 3 — warstwa kompilacji (RECON + PLAN)
> Status: RECON ZAKOŃCZONY (2026-07-16), plan DO ZATWIERDZENIA. Zero kodu, zero migracji,
@ -448,7 +439,7 @@ Próbka 25 dokumentów (stratyfikowana: polisy, faktury, umowy, urzędowe, FLL/s
- **kompletność faktów kluczowych** (kwoty, daty, strony, numery),
- **jakość tagów** (trafność + zgodność ze słownikiem).
Wynik do `kb/phases/kb-m5-faza3-pilot-streszczen.md`: tabela per dokument + wnioski.
Wynik do `docs/kb/modules/05-faza3-pilot-streszczen.md`: tabela per dokument + wnioski.
**Kryterium „lokalny wystarcza na skalę mailową"**: mediana wierności = 2 (zero tolerancji
dla przekręconych kwot — to trafia do wiki) i kompletność ≥ 80% punktów API. Jeśli lokalny
nie daje rady → decyzja o skali mailowej rozważa API z polityką eskalacji fazy 5 (koszt
@ -505,7 +496,7 @@ aktywnych chunków, N większe niż liczba kopert, no-summaries short-circuit)
pakietu przechodzi.
**Eval-set utrwalony**: `jobs/documents-ingest/eval/queries.yaml` (7 zapytań z pilota
07-16, 1:1 z `kb/phases/kb-m5-eval-retrieval-pilot.md`, ten plik pozostał nietknięty —
07-16, 1:1 z `docs/kb/eval/retrieval-pilot-2026-07-16.md`, ten plik pozostał nietknięty —
`queries.yaml` to jego wersjonowana kopia robocza). Skrypt bramki (read-only, integracyjny,
**nie wchodzi do pytest**): `jobs/documents-ingest/eval/retrieval_eval.py`.
@ -560,19 +551,10 @@ Adapter i embed są już idempotentne — nowość to wyłącznie orkiestracja i
`/etc/systemd/system/`, `systemctl enable --now kb-ingest.timer`). Pierwszy
systemd-timer w repo — świadomie host-level, nie kontener (joby potrzebują jednocześnie
LAN, DB i plików hosta; konteneryzacja nic tu nie daje).
- **Harmonogram**: ~~`OnCalendar=*-*-* 03:30`~~**`OnCalendar=0/2:00:00` (co 2 h) od
2026-08-06**, `Persistent=true` (nadgania po reboocie). Zmiana wynika z Decyzji (d) reconu
przyrostówki (`kb/audits/mail-sync-2026-08-06.md` §3.3): o 03:30 SOLARIA prawie na pewno
śpi (potwierdzone odczytem `kb_ingest_embed_skipped 1`), a od tej daty tick dostaje też
etap mailowy — ~60 nowych chunków na dobę pomijanych każdej nocy zapaliłyby
`KbEmbedBacklogGrowing` na stałe. Co 2 h zamiast stałej godziny dopasowanej do nawyków
operatora: probe Ollamy sam wybiera okno, więc któryś tick w nie trafi niezależnie od tego,
o której SOLARIA wstaje w danym tygodniu.
- **Harmonogram**: `OnCalendar=*-*-* 03:30`, `Persistent=true` (nadgania po reboocie).
- **Sekwencja skryptu**: adapter `--apply` → chunk_embed `--apply`
(`OLLAMA_URL=http://solaria:11434`) → **mail_body_ingest `--only-unchunked`** (dodane
2026-08-06 — konsument kolejki, którą wypełnia `jobs/mail-imap-sync`; import miękki, więc
venv bez tego pakietu pomija etap zamiast wywracać wrapper) → summarize → embed-summaries.
Log do `/opt/homelab/logs/kb-ingest/run-YYYYMMDD.log`.
(`OLLAMA_URL=http://solaria:11434`) → (po decyzji z pilota, rozszerzenie później:
summarize nowych dokumentów). Log do `/opt/homelab/logs/kb-ingest/run-YYYYMMDD.log`.
- **Tolerancja na SOLARIĘ offline** (`availability_target: medium`): wrapper odróżnia
„Ollama nieosiągalna" (probe `GET /api/tags` przed embedem; brak → pomiń embed,
odnotuj, **to nie jest fail** — nadrobi następny run, bo embed jest idempotentny) od
@ -628,17 +610,6 @@ Adapter i embed są już idempotentne — nowość to wyłącznie orkiestracja i
> półprodukt kompilacji) i PO filtrze śmieciowych chunków. Pełna wiki po fazie mailowej
> (przyrostówka) — wcześniej kompilat byłby fotografią przeszłości.
**Inwariant 7 (dodany 2026-08-27 — rozszerzenie, nie zmiana inwariantów 16
powyżej, które pozostają NIE podlega zmianie):** decyzja (f),
`kb/audits/wiki-kompilat-recon-2026-08-26.md` §10, zatwierdzona przez operatora
w całości 2026-08-27. Kompilacja strony wiki **nigdy** nie czyta `source='wiki'`
jako dowodu (`exclude_sources=('wiki',)` w `cascade_retrieve`/`hybrid_retrieve`) —
retrieval na potrzeby kompilacji zawsze wyklucza wiki, czyta wyłącznie warstwę
dowodową (mail, paperless). Tylko `/search` (warstwa użytkownika, po zbudowaniu
syntezy odpowiedzi — inwariant 5, faza 5) widzi wiki w kaskadzie. Mitygacja
self-citation/citogenesis przy źródle retrievalu, nie tylko przez lint (inwariant
3) po fakcie — patrz audyt §7 dla pełnego rozumowania.
### 8.2 Rozwinięcie wykonawcze (szczegóły, decyzje architektoniczne bez zmian)
**Repozytorium** (rozstrzygnięcie punktu 6 szkicu): osobne repo `kb-wiki` — decyzja 7,
@ -720,7 +691,7 @@ streszczeń). Kandydaci na pierwsze strony (encje z pilota): PZU/WARTA (polisy),
## 9. Poza zakresem fazy 3
Granice planu — wszystko poniżej jest świadomie odłożone, z istniejącym miejscem w
roadmapie (`kb/subsystems/kb-overview.md` „Stan etapów/Backlog", sesja
roadmapie (`docs/kb/kb-00-overview.md` „Stan etapów/Backlog", sesja
`docs/sessions/2026-07-16.md`, backlog operatora):
| Temat | Gdzie zakotwiczone | Kiedy |

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-22
links: []
---
# Moduł 5, faza 4 — kb-query + UI (RECON + PLAN)
> Status: RECON ZAKOŃCZONY (2026-07-22), plan DO ZATWIERDZENIA. Zero kodu, zero
@ -68,7 +59,7 @@ links: []
### 1.3 PIHA — budżet RAM (istotne dla decyzji D2 / lokalnego fallbacku)
- Audyt `kb/audits/piha-slim-2026-07-02.md`: po "bezpiecznym usuń"
- Audyt `docs/infra/piha-slim-audit-2026-07-02.md`: po "bezpiecznym usuń"
(elasticsearch+diskover+stary llm-gateway) available rosło z **2.9Gi do ~3.8Gi**
(na 8 GB total, +4 GB swap). To jest **jedyna zweryfikowana liczba w repo i ma
3 tygodnie** — od tego dnia na PIHA doszły (GitOps, z `mem_limit`): paperless+db+broker
@ -97,7 +88,7 @@ domeny `*.kapala.org`:
(cert #51 *.okit.pl, cert osobny dla *.kapala.org — sesje ją tylko konfigurują
ręcznie/SQL-em, nigdy nie deployują z repo). kb-query dogania się do tego samego
wzorca: nowy vhost w `npm@PIHA`, TLS z **już istniejącego** wildcard `*.kapala.org`
(pokrywa `paper.`/`cloud.`/`vikunja.kapala.org` — `kb/decisions/kb-dokumenty-otwarte.md`
(pokrywa `paper.`/`cloud.`/`vikunja.kapala.org` — `docs/kb/modules/DECYZJE-do-podjecia.md`
#6) — **żaden nowy certyfikat nie jest potrzebny**.
- **DNS — dwie warstwy, obie trzeba dotknąć** (lekcja `okit-cloudflare-migracja.md`
§"WAZNE: split-horizon DNS"): (a) Cloudflare rekord A → Tailscale IP PIHA
@ -261,7 +252,7 @@ przed backfillem" z fazy 3 §3.1 (zmierz, obejrzyj, dopiero wtedy zaufaj progowi
### Decyzja 3 — Linki do źródeł: paperless vs gmail
**Paperless**: URL do dokumentu — **do zweryfikowania na żywym Paperless przed
implementacją** (repo nie ma zapisanego przykładu, `kb/phases/kb-m2-paperless.md`
implementacją** (repo nie ma zapisanego przykładu, `docs/kb/modules/02-paperless-service.md`
dokumentuje tylko subdomenę, nie ścieżkę). Kandydat wg konwencji paperless-ngx UI:
`https://paper.kapala.org/documents/<id>/details` (Angular routing) — `envelope_id`
`paperless:<id>` już niesie surowy `<id>` do wstawienia. Krok implementacji: jeden
@ -471,7 +462,7 @@ Jedna strona (Jinja2 template + vanilla JS + CSS, serwowane z tego samego FastAP
- Pole zapytania + submit (Enter albo przycisk).
- Wyniki grupowane po `envelope_id` (dokument), w obrębie dokumentu chunki
posortowane po `dist`.
- Kolorowanie progów (progi z fazy 3, `kb/phases/kb-m5-faza3.md` §1.2,
- Kolorowanie progów (progi z fazy 3, `docs/kb/modules/05-faza3-plan.md` §1.2,
zweryfikowane bramką): `dist < 0.45` zielony, `0.450.55` żółty, `> 0.55`
**nie renderować wyniku**, tylko komunikat "brak odpowiedzi w KB" (żółta/czerwona
strefa nadal renderuje wynik z ostrzeżeniem wizualnym; czerwona = brak sensownego

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: decision
visibility: private
status: planned
updated: 2026-07-09
links: []
---
# Decyzje do podjęcia — filar dokumentów (moduły 2/3/4)
> Zbiorcza lista decyzji z przygotowania configów (2026-07-06, branch
@ -63,7 +54,7 @@ zdecydować, czy podbić concurrency, czy zostawić zapas na ollama/AI.
rsync/borg → SOLARIA (2 TB, ta sama LAN). Retencja: 7 dziennych +
4 tygodniowe + 6 miesięcznych. Offsite (np. restic → chmura) zostaje jako
future-note, poza zakresem tego etapu. Cron/skrypt deployowy powstaje przy
deployu modułu 2, nie teraz. Szczegóły: `kb/services/paperless.md`.
deployu modułu 2, nie teraz. Szczegóły: `services/paperless/README.md`.
- **4. Redis brokera: `requirepass`.** Broker (6380) dostaje hasło —
`PAPERLESS_REDIS_PASSWORD` w `.env` po obu stronach (paperless@PIHA,
@ -76,7 +67,7 @@ zdecydować, czy podbić concurrency, czy zostawić zapas na ollama/AI.
+ SOLARIA po NFS) świadomie zaakceptowane — indeks jest odtwarzalny
(`document_index reindex`), oryginałom nic nie grozi. Bez zmian w
configu; fallback-worker na PIHA zostaje. Szczegóły:
`kb/services/paperless-worker.md`.
`services/paperless-worker/README.md`.
- **6. Domeny: `kapala.org` (mesh, prywatne).** `paper.kapala.org`
(Paperless), `cloud.kapala.org` (Nextcloud) — potwierdzone, `*.okit.pl`
@ -99,7 +90,7 @@ zdecydować, czy podbić concurrency, czy zostawić zapas na ollama/AI.
maintainerów paperless-ngx (nieoficjalnie wspierany): ten sam obraz,
`command: celery --app paperless worker`, wspólny Redis+Postgres+storage,
identyczne ścieżki kontenerowe i numeryczny UID po obu stronach. Pełny
wynik badania + ryzyka: `kb/services/paperless-worker.md`.
wynik badania + ryzyka: `services/paperless-worker/README.md`.
- Storage dokumentów na PIHA; NFS export → SOLARIA po LAN
(192.168.31.5 → 192.168.31.70), nie Tailscale.
- AOF w Redis brokera (kolejka przeżywa restart — zero utraty zadań).

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: active
updated: 2026-05-11
links:
- ../runbooks/service-operational-recovery.md
---
# Service Lifecycle and Recovery
This document defines the lifecycle of a service in the homelab and the procedures for operational recovery.
@ -35,6 +25,25 @@ This document defines the lifecycle of a service in the homelab and the procedur
- `docker compose down`.
- Archive `/opt/homelab/data/<service>` if necessary.
## Operational Recovery
### 1. Container Failure
If a service is unhealthy:
- Check `docker compose logs`.
- Restart: `docker compose restart`.
- Recreate: `docker compose up -d --force-recreate`.
### 2. Node Failure
If a host node fails:
- Services with `owner_node` matching the failed node must be recovered on a backup node or the node must be restored.
- Persistence data must be restored from backups to `/opt/homelab/data/<service>`.
### 3. Dependency Recovery
If a dependency fails:
- Services depending on it might report unhealthy status.
- Recover the dependency first.
- Re-verify dependent services.
## Persistent Data Conventions
- **Data**: `/opt/homelab/data/<service>` - Primary persistent state.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "kb/subsystems/fleet-inventory.md + hosts/<node>/capabilities.yaml (stub z 2026-04-15, sprzed floty)"
---
# Networking
## Description

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: runbook
visibility: public
status: active
updated: 2026-05-12
links: []
---
# Node Onboarding Workflow
This document describes the process of onboarding a new Linux machine into the homelab platform.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-06-17
links: []
---
# Observer Runtime
The Observer Runtime is a lightweight agent responsible for synthesizing the operational world state of the homelab from raw events, logs, and state files.

65
docs/questions.md Normal file
View file

@ -0,0 +1,65 @@
# Unknowns and Clarification Questions
## Description
This page lists information that is missing or unclear from the current homelab documentation.
## Current configuration
The currently documented configuration is limited to:
- Raspberry Pi 5 as the main server.
- Docker, Portainer, and Nginx Proxy Manager as the core stack.
- NAT with forwarded ports:
- `80-81` to `4480-4481`
- `443` to `4443`
- Public access through Nginx Proxy Manager with Let's Encrypt HTTPS.
- Private access through Tailscale.
- Hetzner VPS handoff:
- Hostname: `ubuntu-4gb-hel1-1`
- Tailscale IP: `100.95.58.48`
- Public IPv4: `135.181.153.108`
- Public IPv6: `2a01:4f9:c014:98f0::1`
- Running container: `npm`
- Joplin files created but not running.
## Known facts
- The homelab is documented only from the known facts above.
- Anything not listed as known remains unconfirmed.
## Unknown / needs clarification
1. What operating system and version is running on the Raspberry Pi 5?
2. What is the Raspberry Pi 5 RAM size?
3. What storage devices are used, and where is persistent service data stored?
4. What is the Raspberry Pi 5 LAN IP address?
5. Is the Raspberry Pi 5 using DHCP or a static IP address?
6. What router or firewall performs NAT and port forwarding?
7. Is the WAN IP static, dynamic, or behind CGNAT?
8. Does external port `80` map to internal port `4480`, and does external port `81` map to internal port `4481`?
9. Are the forwarded ports TCP only, UDP only, or both?
10. Are any other ports forwarded?
11. What domain names or subdomains point to the homelab?
12. What are the Nginx Proxy Manager proxy hosts?
13. Which services are public, and which are private-only?
14. Is HTTP-to-HTTPS redirection enabled in Nginx Proxy Manager?
15. Are Nginx Proxy Manager access lists used?
16. How are Docker, Portainer, and Nginx Proxy Manager deployed?
17. Are Docker Compose files, Portainer stacks, or other manifests available?
18. What containers are currently running?
19. What Docker networks and volumes exist?
20. What is the Tailscale device name for the Raspberry Pi 5?
21. Does the Raspberry Pi 5 advertise Tailscale subnet routes?
22. Is the Raspberry Pi 5 configured as a Tailscale exit node?
23. Is Tailscale SSH enabled?
24. What backup system exists, if any?
25. What monitoring or alerting exists, if any?
26. Is the Hetzner VPS part of the homelab documentation scope, a separate system, or both?
27. What is the operating system version on `ubuntu-4gb-hel1-1`?
28. Is public Nginx Proxy Manager admin access on port `81` intentionally reachable on `135.181.153.108`?
29. Has DNS record `joplin.okit.pl -> 135.181.153.108` been created?
30. Has optional AAAA record `joplin.okit.pl -> 2a01:4f9:c014:98f0::1` been created?
31. Has `POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD` been changed before first Joplin production start?
32. Has the Nginx Proxy Manager proxy host for `joplin.okit.pl` been created?
33. Are ports `80` and `443` publicly reachable on the Hetzner VPS for Let's Encrypt HTTP validation?

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-05-11
links: []
---
# Service Model and Healthchecks
This document defines the normalized service model for the homelab.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "kb/subsystems/fleet-inventory.md + hosts/<node>/capabilities.yaml (stub z 2026-04-15, sprzed floty)"
---
# Services
## Description

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-05-27
links: []
---
# SESSION: Budowa planner-agent — LLM-based diagnostics
**DATA:** 2026-05-27

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-05-27
links: []
---
# SESSION: Stabilizacja systemu wieloagentowego homelabu
**DATE:** 2026-05-27

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-09
links: []
---
# Sesja 2026-06-08 — onboarding LUSTRO (RPi4 / Magic Mirror / KEN)
## Cel
@ -90,7 +81,7 @@ przez Tailscale działa bezhasłowo. Verify czysty (arch=aarch64).
## Learnings
(odzwierciedlone też w `kb/runbooks/node-onboarding-tool.md`)
(odzwierciedlone też w `scripts/onboard/README.md`)
- mDNS `.local` zawodny do automatyzacji → `first_contact` przez IP lub tailscale, nie `.local`
- istniejący node z userem uid=1000: użyj go zamiast tworzyć `oskar` (kolizja uid)

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-09
links: []
---
# Sesja 2026-06-09 — flota recovery + LUSTRO register
## Cel
@ -130,4 +121,4 @@ Docelowo: osobny worktree per task.
## Tech-debt złapany w sesji
→ wpisany do `kb/phases/backlog.md`
→ wpisany do `docs/backlog.md`

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-11
links: []
---
# Sesja 2026-06-10/11 — lustro SSH shipping fix + ha-diag-agent piha
## Cel
@ -79,13 +70,13 @@ solaria / piha / chelsty to wciąż **stare root kontenery** node-agenta
(piha Created 2026-05-27, uid 0). Ich mount `/root/.ssh` działa tylko dlatego,
że kontenery są sprzed `user: "1000:1000"`. Pierwszy `--force-recreate` / reboot
hosta / update obrazu przełączy je na uid 1000 i shipping padnie jak na lustrze.
**NIE RECREATE bez fixu.** Szczegóły i fix: `kb/phases/backlog.md`.
**NIE RECREATE bez fixu.** Szczegóły i fix: `docs/backlog.md`.
---
## Tech-debt złapany w sesji
→ wpisany do `kb/phases/backlog.md` (flota-bomba, ha-diag-agent blocked,
→ wpisany do `docs/backlog.md` (flota-bomba, ha-diag-agent blocked,
poison-quarantine review, `--omit-dir-times`, stale komentarz node_agent.py,
shipping success na `logger.debug`, event-bloat lustro na VPS).
@ -96,8 +87,8 @@ fa59625 docs(ha-diag-agent): replace curl verify commands with docker exec
d7e0d31 fix(ha-diag-agent): remove host port mapping for 8087
### Files changed
kb/runbooks/ha-diag-agent-deploy.md | 4 ++--
kb/services/ha-diag-agent.md | 4 ++--
services/ha-diag-agent/DEPLOY.md | 4 ++--
services/ha-diag-agent/README.md | 4 ++--
services/ha-diag-agent/docker-compose.yml | 3 ---
services/ha-diag-agent/service.yaml | 3 ---
4 files changed, 4 insertions(+), 10 deletions(-))

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-17
links: []
---
# Sesja 2026-06-17 — KB foundations (etap 1 maili)
## Cel
@ -120,14 +111,14 @@ services/kb-postgres/service.yaml
services/kb-postgres/env.example
services/kb-postgres/healthcheck.sh
services/kb-postgres/init/001_envelope.sql
kb/services/kb-postgres.md
services/kb-postgres/README.md
hosts/solaria/runtime/kb-postgres/docker-compose.override.yml
hosts/solaria/services.yaml
inventory/topology.yaml
packages/kb-mail/pyproject.toml
packages/kb-mail/src/kb_mail/{__init__,envelope,db,archive}.py
packages/kb-mail/tests/{conftest,test_envelope,test_archive,test_db,test_migration}.py
kb/subsystems/kb-overview.md (etap 1 done, konwencja packages/)
docs/kb/kb-00-overview.md (etap 1 done, konwencja packages/)
CLAUDE.md (sekcja Shared Python Libraries)
docs/sessions/2026-06-17-kb-foundations.md
```

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-17
links: []
---
# Sesja 2026-06-17 — Vikunja OIDC+GitOps · Observer heartbeat-TTL · panel-source
## Zrobione i wdrożone

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-22
links: []
---
# Sesja 2026-06-22 — KB spine relokowany na PIHA + przygotowanie hosta
## Cel

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-22
links: []
---
# Sesja 2026-06-22 — decyzja: Prometheus jako źródło prawdy dla liveness floty
## Cel

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-24
links: []
---
# Sesja 2026-06-24 — KB etap 2: importer Gmail (kod gotowy)
## Cel
@ -171,7 +162,7 @@ jobs/gmail-bulk-import/pyproject.toml
jobs/gmail-bulk-import/tests/test_importer.py
hosts/piha/capabilities.yaml
.gitignore
kb/subsystems/kb-overview.md (etap 2 gotowy, konwencja jobs/)
kb/subsystems/kb-mail-pillar.md (§8 krok 2 = kod gotowy)
docs/kb/kb-00-overview.md (etap 2 gotowy, konwencja jobs/)
docs/kb/kb-01-email-design.md (§8 krok 2 = kod gotowy)
docs/sessions/2026-06-24-kb-gmail-importer.md
```

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-24
links: []
---
# Sesja 2026-06-24 — fleet-prometheus etap 1: scaffold + deploy-node hostname fix
## Cel
@ -108,7 +99,7 @@ docker compose \
---
## Nowe tech-debty (dodane do `kb/phases/backlog.md`)
## Nowe tech-debty (dodane do `docs/backlog.md`)
1. **Rozjazd stanu Docker Compose na VPS** — serwisy `node-agent`, `control-plane` i inne
stworzone innym `project-name` niż `deploy-node.sh` oczekuje; Recreate pada na stale

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-26
links: []
---
# Sesja 2026-06-25 — KB etap 2: bulk import Gmail uruchomiony
## Cel

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-25
links: []
---
# Sesja 2026-06-25 — fleet-prometheus etap 1: uruchomienie na VPS + incydent mózgu
## Cel

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-26
links: []
---
# Sesja 2026-06-26 — fleet-prometheus etap 2: targety floty + zamknięcie buga deploy.sh vps
## Cel

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-30
links: []
---
# Sesja 2026-06-30 — Inwentaryzacja floty + gaszenie dysku SATURN
## Cel
@ -19,7 +10,7 @@ architekturą dokumentów KB. Start od weryfikacji stanu faktycznego wszystkich
- CC (Sonnet 4.6) w worktree `fleet-inventory` zebrał stan faktyczny (docker ps +
free/df/nproc) z 4 dostępnych nodów: PIHA, VPS, SOLARIA, SATURN. LUSTRO+CHELSTY
offline (timeout :22) -> oznaczone UNREACHABLE.
- Wynik: `kb/subsystems/fleet-inventory.md` — 23 zpriorytetyzowane rozjazdy.
- Wynik: `docs/infra/inventory-2026-06-30.md` — 23 zpriorytetyzowane rozjazdy.
- Kluczowe ustalenia:
- **forgejo** biega na PIHA (always-on), ale `service.yaml owner_node=saturn` — rozjazd
- **mosquitto** biega na VPS, `service.yaml owner=piha`, na PIHA go nie ma

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-30
links: []
---
# 2026-06-30 — Migracja kapala.org → Cloudflare + wildcard DNS-01, HA i Immich na mesh
## Cel

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-30
links: []
---
# Sesja 2026-06-30 — fleet-prometheus liveness: reguły NodeDown + wpięcie watchdog→Prometheus
## Cel

View file

@ -1,16 +1,7 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-02
links: []
---
# Sesja 2026-07-02 — Modul 0 (odchudzenie PIHA) + migracja Forgejo/Vikunja na kapala.org
## Modul 0 kb-02 — WYKONANY (faza 1 audyt + faza 2 egzekucja)
- Audyt CC (read-only): kb/audits/piha-slim-2026-07-02.md
- Audyt CC (read-only): docs/infra/piha-slim-audit-2026-07-02.md
- Review Oskara skorygowal audyt: llm-gateway = WLASNY kod (FastAPI-router LLM,
/opt/llm-gateway, proxy do Ollama@SOLARIA) — NIE martwy; immich MUSI byc 24/7
na PIHA (SOLARIA sesyjna) — rekomendacja przeniesienia wykreslona.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-02
links: []
---
# Sesja 2026-07-02 — recon-weryfikacja inwentaryzacji + rozbrojenie trzech min
## Cel
@ -21,7 +12,7 @@ Sesja tylko-recon + minimalne fixy; bez deployów nowych feature'ów.
### Recon-weryfikacja inwentaryzacji floty (commit `57a6dff`, read-only)
Wynik: `kb/subsystems/fleet-inventory-verify.md`.
Wynik: `docs/infra/inventory-verify-2026-07-02.md`.
**Bilans 23 rozjazdów z audytu 2026-06-30**:
- **20 wciąż aktualnych** — nic się samo nie naprawiło.
@ -100,7 +91,7 @@ Po jednej linii per plik; `owner_node` nie występował nigdzie indziej w repo.
nie rezolwuje z SOLARII; brak formalnego override mem_limit fleet-prometheus
w `hosts/vps/runtime/` (siedzi w bazowym compose — kosmetyka).
Wpisy dodane do `kb/phases/backlog.md` w tej sesji.
Wpisy dodane do `docs/backlog.md` w tej sesji.
---

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-06
links: []
---
# Sesja 2026-07-06 — cutover liveności na Prometheus: recon starego toru + Etap 0 udowodniony w boju
## Cel
@ -22,7 +13,7 @@ Prometheus → brain-watchdog → Telegram, którego brakowało od 2026-06-30.
### Recon cutoveru — wmergowany (commit `d94bb38`)
Wynik: `kb/audits/prometheus-cutover-2026-07-06.md` (517 linii, read-only,
Wynik: `docs/infra/prometheus-cutover-recon-2026-07-06.md` (517 linii, read-only,
zero zmian w kodzie). Kluczowe ustalenia:
- **Cutover to podmiana klasyfikacji liveności w JEDNYM miejscu**

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-07
links: []
---
# Session log 2026-07-07 — Immich upload fix / pimain cleanup (kontynuacja 2026-07-03)
## Kontekst

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-07
links: []
---
# Sesja 2026-07-07 — okit.pl Faza 1 (wildcard cert) + przepiecie 9 hostow
## Kontekst

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-09
links: []
---
# Sesja 2026-07-09 — KB configi (9 decyzji) + wzorzec dzielenia plikow (Nextcloud twierdza + Gokapi)
## Kontekst

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-10
links: []
---
# Sesja 2026-07-10 — Deploy 1 Paperless (DZIALA) + swap PIHA + npm-API tool w akcji
## Glowne osiagniecie: Paperless serwis LIVE

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-12
links: []
---
# Sesja 2026-07-12 — Deploy 2: split-host OCR-worker (DZIALA) + decyzja kierunku KB
## Deploy 2 — OCR-worker na SOLARIA przez NFS: DZIALA end-to-end
@ -46,7 +37,7 @@ maila = ta sama encja, DOWOD zasady kb-00 #7), (3) interfejs pytan (RAG) — pie
realnej uzytecznosci. Dopiero POTEM dopelniac importy (reszta Takeout, zdjecia, transakcje).
## TODO nastepne
- MODUL 5 (koperta + ingest + embeddingi + cross-source) — kb/phases/kb-m5-documents-ingest.md
- MODUL 5 (koperta + ingest + embeddingi + cross-source) — docs/kb/modules/05-documents-ingest.md
- Import probki zalacznikow z maili (kilkaset, nie 70k) — do zbudowania RAG
- Interfejs pytan / RAG — warstwa uzytkowa
- Deploy 3 (Nextcloud), Deploy 4 (Gokapi) — configi gotowe, czekaja

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-15
links: []
---
# Sesja 2026-07-15 — Prometheus cutover Etap 2 (analiza GO) + domknięcie rodziny bugów event-pipeline/checkpoint
## Kontekst
@ -27,12 +18,12 @@ parsowany z nazwy `evt-<node>-<ts>-...` (fallback mtime, **nigdy 0** dla
istniejącego pliku — 0 = leksykalne "starszy niż checkpoint" = dokładnie ten
poison), migracja starych path-checkpointów przy starcie. Zdeployowany na VPS
(observer `StartedAt` 07-14). Zweryfikowany dziś jako kompletny i zdeployowany.
Szczegóły: `kb/phases/backlog.md` (sekcja "Bug: checkpoint observera po ścieżce
Szczegóły: `docs/backlog.md` (sekcja "Bug: checkpoint observera po ścieżce
leksykalnej").
### 2. docs(infra) analiza Etapu 2 shadow-run (Fable, `8fec62d`)
`kb/phases/prometheus-cutover-etap2.md` — 165 mismatchy
`docs/infra/prometheus-shadow-etap2-analiza-2026-07-15.md` — 165 mismatchy
`SHADOW_LIVENESS_MISMATCH` solaria/lustro w dobie 2026-07-14 WYJAŚNIONE: dwa
nocne wyłączenia węzłów (lustro 21:30 UTC — regularny power-off, solaria
21:34 UTC). Wzorzec `event=fresh prom=down` to **nie** "żywy węzeł niewidziany

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-16
links: []
---
---
@ -31,7 +22,7 @@ links: []
### 1. RECON lustro shipping (Fable, commit `542bba4`)
`kb/audits/lustro-shipping-2026-07-16.md` — 1507 mismatchy `lustro event=dead prom=up` w trwałym logu WYJAŚNIONE: (a) wczorajszy kontrolowany test (node-agent stał 3h20m, nie 15 min jak zakładano) + (b) poranny boot-race 56s. **Werdykt: shipping lustro działa, ZERO recurring problemu.** Prometheus 0 pomyłek w 48h — wzmacnia rekomendację GO dla Etapu 3 cutoveru.
`docs/infra/lustro-shipping-recon-2026-07-16.md` — 1507 mismatchy `lustro event=dead prom=up` w trwałym logu WYJAŚNIONE: (a) wczorajszy kontrolowany test (node-agent stał 3h20m, nie 15 min jak zakładano) + (b) poranny boot-race 56s. **Werdykt: shipping lustro działa, ZERO recurring problemu.** Prometheus 0 pomyłek w 48h — wzmacnia rekomendację GO dla Etapu 3 cutoveru.
Znaleziska poboczne: lustro biega na obrazie sprzed 5 tyg (deploy-node bez `--build` — patrz fix #2 niżej); fake-hwclock boot-race (RPi bez RTC — pierwszy event po boocie ma stary stempel, dropnięty przez timestamp checkpoint).

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-20
links: []
---
# Sesja 2026-07-17/18 — KB faza 3: kroki 2-5 DOMKNIĘTE
## Krok 2 — migracja 004 + pilot streszczeń A/B (17.07)

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-22
links: []
---
# Sesja 2026-07-17…21 — wątek KB: faza 3 kroki 3-5 DONE (+ incydenty)
**Krok 3 — kaskada summary→chunk:** bramka **PASS at N=10, k=5** (N-sweep {1..20}: N=5 to zmierzona podłoga, N=10 niesie 2× margines); kaskada nie degraduje niczego, na 186 dok nie poprawia (test architektury pod skalę mailową, zgodnie z przewidywaniem planu); koszt +1 SQL, zero dodatkowych embedów. cascade_query = domyślna ścieżka kb-query; flat_query zostaje jako baseline. Eval przepisany do wersjonowanego eval/queries.yaml + retrieval_eval.py.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-22
links: []
---
# 2026-07-22 — HA: incydent dwóch mózgów, cutover ken, archiwum legacy
## Odkrycie

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-22
links: []
---
# Sesja 2026-07-22 — KB faza 4: recon + kroki 1-2 (kb-query LIVE na PIHA)
**Recon+plan fazy 4** (05-faza4-plan.md, 603 linie): D1 wydzielenie packages/kb-retrieval (documents-ingest ciągnie anthropic+CLI — nie do obrazu serwisu); D2 fallback embed z pełną maszyną stanów, ale gate'owany kalibracją RAM na żywym PIHA (audyt nieaktualny, ~3.8Gi zajęte — plan daje kryteria i alternatywę: jawna degradacja 503 zamiast łamania inwariantu modelu); OIDC wbudowane w apkę (authlib, wzorzec repo — nigdy forward-auth); gmail: envelope_id już JEST Message-ID.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-23
links: []
---
# 2026-07-22/23 — Control-plane: dziura w operator_ui + pierwszy pełny cykl remediacji bez SSH
Zamknięcie wielosesyjnego wątku "czemu mózg nie leczy floty". Dwa dni pracy,
@ -70,7 +61,7 @@ approval → executor → node-agent → docker restart → completed.
test E2E padł: agent rzucał `[Errno 13] Permission denied:
/opt/homelab/actions/dispatch` co cykl. Root cause to ZNANY, POWRACAJĄCY
(już 4. raz — patrz sekcja "Tech-debt: globalny porządek uid/gid/uprawnień
we flocie" w `kb/phases/backlog.md`) motyw
we flocie" w `docs/backlog.md`) motyw
uid/gid na PIHA: oskar ma uid 1004, kontener agenta biega jako uid 1000
(= user `pi` na hoście). `/opt/homelab/actions` było `oskar:oskar
drwxr-xr-x` (utworzone w maju), podczas gdy DZIAŁAJĄCY wzorzec to
@ -111,7 +102,7 @@ approval → executor → node-agent → docker restart → completed.
Pierwszy w historii systemu pełny cykl remediacji end-to-end potwierdzony w
produkcji (PIHA), bez SSH z control-plane do węzłów. Publiczna dziura
autoryzacji na `operator_ui.py:18180` zamknięta (bind ograniczony do
Tailscale). Otwarte follow-upy — patrz `kb/phases/backlog.md` (retry-w-nieskończoność
Tailscale). Otwarte follow-upy — patrz `docs/backlog.md` (retry-w-nieskończoność
zepsutego JSON, uprawnienia `actions/` na innych węzłach, brak twardego checka
`.env`/`TAILSCALE_BIND_IP` w `deploy-local.sh`, brak autoryzacji w
`operator_ui.py`, zapchana approval queue przez `alert_only`, brak

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-23
links: []
---
# 2026-07-22/23 — HA: adapter api, import ken, deploy.sh, otwarcie fazy 1
## Wykonane

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-23
links: []
---
# 2026-07-23 (cd.) — HA: klima E2E, audyt Fable, fix-pack 1
## Klima salonowa — pierwsza automatyzacja LLM przez repo (E2E)

View file

@ -1,15 +1,6 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-23
links: []
---
# Sesja 2026-07-23 — KB faza 4: ingress kb.kapala.org (krok 5/§8)
**Zakres**: wyłącznie ingress (`kb/phases/kb-m5-faza4.md` §8, krok 5) —
**Zakres**: wyłącznie ingress (`docs/kb/modules/05-faza4-plan.md` §8, krok 5) —
frontend i `/search` już LIVE na PIHA (port 8230) od sesji 2026-07-22. Zero
zmian w kodzie kb-query w tej sesji.
@ -70,8 +61,8 @@ w tej samej sesji, osobnym przebiegiem po zgłoszeniu przez operatora:
Potwierdzone w repo (zgodnie z `05-faza4-plan.md` §1.4): **brak wzorca
forward-auth/reverse-proxy-level auth** — NPM community edition go nie ma
(sprawdzone: brak `oauth2-proxy`/`authelia`/`forward_auth` w kodzie repo poza
wzmiankami "przyszła opcja" w `kb/subsystems/kb-documents-pillar.md` i
`kb/nodes/vps.md`). Wszystkie 3 precedensy (paperless/nextcloud/vikunja)
wzmiankami "przyszła opcja" w `docs/kb/kb-02-documents-design.md` i
`hosts/vps/README.md`). Wszystkie 3 precedensy (paperless/nextcloud/vikunja)
robią OIDC **wewnątrz aplikacji**. kb-query nie ma dziś żadnego logowania.
Zgodnie z instrukcją zadania: **nie budowano** nowego komponentu auth.
@ -122,7 +113,7 @@ username collision, `docs/sessions/2026-07-10-paperless-deploy.md`).
## Pliki repo zmienione
- `kb/services/kb-query.md` — sekcja "Ingress" (co żyje, co nie, dlaczego
- `services/kb-query/README.md` — sekcja "Ingress" (co żyje, co nie, dlaczego
auth odłożone) zastępuje starą notatkę "not wired up yet".
- `docs/sessions/2026-07-23-kb-f4-ingress.md` — ten dokument.

View file

@ -1,63 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-27
links: []
---
# 2026-07-27 — HA: legacy zamkniete, kasacje, pimirror, sonda Zigbee
## Wykonane
- Klima: 4 dni autonomii bez interwencji (sunset shutdown 26.07 21:05
zadzialal naturalnie; dzis poprawny brak startu — prog 27 > salon 24.4,
fix faa2e2a dziala w praktyce).
- Legacy: kontener homeassistant5 juz nie istnial (podejrzenie: cleanup
policy node-agenta — do wyjasnienia kiedys; kolizja "stop bez rm dla
rollbacku" vs auto-cleanup). Katalog /home/pi/homeassistant nietkniety
(config z 22.07). Strata zerowa — archiwum w gicie.
- Kasacje pkt 10 audytu przez API (6 automatyzacji: para Tymka, powitanie
test, notify router, para prototyp) + drift commit 36b43e5. Incydent:
petla DELETE odpalona z placeholderami ID1..ID6 przed identyfikacja —
bez szkod (404), lekcja: operator nie podstawia, tor przez repo
(deploy.sh --delete w backlogu).
- task/ha-porzadki (09624e0): pimirror graceful shutdown odtworzony
z ken-legacy (23:28 graceful przed twardym 23:35; legacy button
unavailable — przepisane na pimirror2 po registry, entity_id wg nowej
konwencji), sekcja "Konwencje automatyzacji" w DESIGN.md, backlog:
deploy.sh --delete, trigger na zmiane tolerancji klimy. Deploy 1/0/0.
## Sonda Zigbee (read-only) — diagnoza awarii "2026-07-17"
last_changed w HA bezuzyteczne po restarcie (277 encji ze stemplem 26.07 =
odcisk restartu, nie fala). Log z2m: tydzien bez przejsc offline/online
(padly wczesniej). Availability z MQTT (retained): ~20 urzadzen offline,
w tym WSZYSTKIE TRZY ROUTERY (routerIKEA/Salon/Sypialnia) + urzadzenia
sieciowe (ledTV, zbLampkiRegal, zbSwitchBlatZasilanie, listwy LED).
Wniosek: to nie baterie i nie koordynator — urzadzenia zasilane sieciowo
sa fizycznie bez pradu, mesh sie zapadl kaskadowo (bateryjne koncowki
poza zasiegiem: mdHeli, thHeli, mdSypialnia, mdUbikacja, mdWejscie,
waterLeakWc, 4button, heaterLazienkaTRV07). Geografia strat = dziury po
routerach.
## Nastepne kroki
1. FIZYCZNIE: sprawdzic zasilanie 3 routerow + wtyczek LED; po powrocie
odczekac dobe; re-pairing tylko dla tego, co nie wroci samo. Przy
okazji: zasilanie puryfikatorow zhimi (WiFi, osobny tor xiaomi_miot).
2. Po odbudowie mesh: ponowna sonda availability + drift/re-import;
duplikat mdwejscie/mdWejscie do sprzatniecia w z2m.
3. Wieczorem 23:28: pierwszy zywy test pimirror graceful shutdown (trace).
4. Nastepna sesja: projekt Fable "tryby domu" (night/sleep/empty/on_leave)
— prompt gotowy w historii; TRV guard ~09; przepiecie ha-diag-agent.
## Dogrywka: zigbee.okit.pl -> zigbee.kapala.org (mesh-only)
Runtime (poza gitem, log tutaj): npm@PIHA proxy host #36
(zigbee.kapala.org -> 192.168.31.5:8087, cert #49 wildcard, websocket ON,
advanced puste — pulapka kapala.org), przez scripts/npm/npm_api.py
(dry-run -> --apply). Pi-hole custom.list: 192.168.31.5 zigbee.kapala.org
(split-horizon jak kb). Cloudflare kapala.org: A zigbee -> 100.108.208.3
(Tailscale piha, DNS only — wzorzec forgejo). Test: dig OK, curl 200.
Sprzatniecie: rekord zigbee.okit.pl usuniety z Cloudflare, vhost z NPM@VPS.
Frontend z2m bez auth_token — publiczna ekspozycja na okit.pl [TODO operator:
status auth przed migracja + ew. przeglad access logow NPM@VPS].

View file

@ -1,212 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-30
links: []
---
# Sesja 2026-07-27 — KB faza 4: fallback embed SOLARIA→PIHA (krok 3, ostatni element rdzenia)
> **Dopisek redakcyjny (2026-07-30, dedup — `kb/phases/kb-m5-faza4-fallback-dedup.md`):**
> implementacja kodu z tej sesji (`app/fallback.py`, branch `task/kb-f4-fallback`, 3d4ee38)
> została **porzucona** — do mastera weszła równoległa, szersza implementacja tego samego
> kroku planu (e7625cd, `app/embed_router.py`, 2026-07-29) i to ona biega na PIHA. Ten log
> wciągnięto do repo, bo dokumentuje fakty operacyjne niezależne od porzuconego kodu:
> znalezisko i wyłączenie osieroconego natywnego `ollama.service` na PIHA (§3, z backlogiem
> odinstalowania ≈2026-08-10), kalibrację live ollama-piha z werdyktem GO (§4 — konfiguracja
> kontenera identyczna na masterze, pomiar przenosi się) oraz metodologię i baseline bramki
> §9 (§5, Δ~3e-4). Wyniki bramki i testu sol-down dotyczyły kodu z brancha — na wdrożonym
> masterze wymagają powtórki (raport dedup, follow-up (b)). Sekcje o deployu (§6) i
> "Do zrobienia przez operatora" pkt 12 opisują stan sprzed merge'a e7625cd — historyczne.
> Z delty brancha uratowano ponadto: `retrieval_eval.py --transport http` (plan §2 D6/§9)
> i luki testowe T1/T2 przeniesione do `test_embed_router.py`.
**Zakres**: `kb/phases/kb-m5-faza4.md` §2 decyzja 2 / §5 — aktywny fallback
embedu, ostatni brakujący element rdzenia fazy 4 (frontend i ingress LIVE od
2026-07-22/23, `docs/sessions/2026-07-23-kb-f4-ingress.md`). Zero zmian w schemacie
DB, zero zmian w `kb_retrieval`'s retrieval logice — wyłącznie warstwa embed + health.
Praca wykonana w task worktree (`task/kb-f4-fallback`, `.claude/skills/worktree-aware`).
Zgodnie z ustaleniem na starcie sesji (patrz "Ustalenia proceduralne" niżej): kod
napisany i przetestowany lokalnie w worktree, produkcyjne kroki (kalibracja, deploy,
live-test) wykonane po jawnej zgodzie operatora, z osobnym potwierdzeniem przed
każdym kolejnym krokiem dotykającym PIHA/SOLARIĘ.
## Ustalenia proceduralne
Zadanie wprost wymagało kroków produkcyjnych (kalibracja RAM/latencji na żywym PIHA,
symulacja sol-down dotykająca SOLARII, deploy, push) — sprzeczne z ogólną dyscypliną
`worktree-aware` ("nigdy nie uruchamiaj deployów/healthchecków przeciw produkcji z
worktree"). Zamiast rozstrzygać to samodzielnie, zapytano operatora:
1. Recon read-only (bez zmian stanu) — zgoda bez pytania.
2. Właściwe kroki produkcyjne (kalibracja, deploy, live-test, push) — operator
potwierdził jawnie ("Yes, proceed with all of it") po zobaczeniu pełnego zakresu.
## 1. Kod (warstwa embed + health, zero zmian retrievalu/DB)
- **`packages/kb_retrieval/embed.py`**: `embed_chunk` dostał opcjonalny `timeout_s`
(domyślnie `None`, zero zmiany zachowania istniejących wołań) — potrzebny do
twardego 3 s timeoutu na nodze SOLARIA bez zmiany zachowania nogi PIHA.
- **`services/kb-query/app/fallback.py`** (nowy): `SolCircuitBreaker` (cache 30 s,
zegar wstrzykiwalny do testów) + `resolve_sol_status` (probe `/api/tags`, 500 ms) +
`embed_with_fallback` (SOLARIA z twardym 3 s timeoutem → jednorazowe przełączenie na
PIHA **w tym samym requeście** przy timeout/błędzie → PIHA bez dodatkowego
timeoutu). Dokładnie maszyna stanów z planu §2 decyzja 2.
- **`app/search.py`**: `run_search` liczy embedding raz przez `embed_with_fallback`,
potem woła `flat_retrieve`/`cascade_retrieve`/`hybrid_retrieve` (niskopoziomowe
funkcje `kb_retrieval`, biorą gotowy wektor) zamiast `flat_query`/`cascade_query`/
`hybrid_query` (które embedują same) — dzięki temu decyzja fallbacku żyje wyłącznie
w warstwie HTTP kb-query, zero zmiany w `kb_retrieval`. `sol_status` w odpowiedzi to
teraz realny wynik, nie zahardkodowane `"up"`.
- **`app/main.py`**: `/healthz` i `/search` dzielą jeden `SolCircuitBreaker`
(`app.state.sol_breaker`) — oba endpointy zawsze zgadzają się co do aktualnego
stanu. Nowy env `OLLAMA_PIHA_URL` (domyślnie `http://localhost:11434` — celowo
"inertny" placeholder, fail-closed, dopóki operator nie ustawi realnego adresu).
- **Inwariant modelu**: **nie dodano** drugiego, per-request sprawdzenia w DB —
`EMBED_MODEL` to jedna stała wątkowana przez obie nogi `embed_with_fallback`,
więc startowy check (`app/startup.py`, niezmieniony) pokrywa obie ścieżki z
konstrukcji. Dodanie drugiego DB-checka chroniłoby przed scenariuszem, który nie
może wystąpić (CLAUDE.md: nie dodawaj walidacji dla scenariuszy, które nie mogą się
zdarzyć) — zamiast tego nowy test (`test_both_legs_use_identical_embed_model`)
strukturalnie dowodzi, że obie nogi w tym samym requeście dostają identyczny
`embed_model`.
- **`jobs/documents-ingest/eval/retrieval_eval.py`**: dodano `--transport
{direct,http}` + `--base-url` (plan §2 decyzja 6 / §9) — dotąd nieistniejące (tylko
ręczny smoke-test, `docs/sessions/2026-07-23-kb-f4-ingress.md` follow-up). Tryb
`http` woła trzy `GET /search` (flat/cascade/hybrid) na żywym kb-query zamiast
embedować+odpytywać lokalnie; `envelope.source` do kryterium 4 bierze się z pola
`source` w odpowiedzi JSON, nie z osobnego zapytania do DB. Nie da się swipe'ować
N przez HTTP (kb-query serwuje jeden N per request) — tryb http raportuje tylko
przy `--gate-n`.
## 2. Nowy serwis `services/ollama-piha`
Klon wzorca `services/ollama` (`owner_node: piha` zamiast `solaria`, bez rezerwacji
GPU — PIHA to arm64 bez akceleracji), `OLLAMA_KEEP_ALIVE=0` (model ładowany tylko na
czas requestu). `mem_limit: 2560m` (tentatywny wg planu, potwierdzony pomiarem —
patrz §3). Wpisany do `hosts/piha/services.yaml` (`depends_on.local` kb-query →
`[kb-postgres, ollama-piha]`, fallback nie jest twardą zależnością na starcie).
## 3. Znalezisko: osierocony natywny `ollama.service` na PIHA
Podczas pierwszej próby deployu `ollama-piha` (bind `127.0.0.1:11434`) — konflikt
portu. Okazało się, że PIHA ma **natywny (nie-Docker) systemd `ollama.service`**
(v0.6.1, `enabled`, działający od 2026-06-22, PATH env wskazujący na użytkownika
`/home/pi/...`), o którym nic nie wiadomo w repo — plan §1.2 wprost zakładał "PIHA:
brak Ollamy", co okazało się nieaktualne/błędne. To realna sprzeczność planu z
rzeczywistością → STOP, pytanie do operatora zamiast cichej decyzji.
Weryfikacja przed jakąkolwiek akcją: `journalctl -u ollama --since "7 days ago"`
**tylko własne, właśnie wykonane** zapytania probe (`/api/version`, `/api/tags`),
`total blobs: 0` od startu (nigdy nic nie pobrano). Operator potwierdził: martwy
balast, `sudo systemctl disable --now ollama.service` (**disable, nie uninstall** —
odwracalne). Port 11434 zwolniony, `ollama-piha` wystartował normalnie.
**Backlog**: PIHA host-level shadow — natywny `ollama.service` wyłączony
2026-07-27; odinstalować binarkę/unit po ~2 tygodniach jeśli nic się nie posypie.
## 4. Kalibracja (plan §5, gate) — **werdykt: GO**
Zmierzone na żywym PIHA pod normalnym obciążeniem (kb-postgres, paperless, Immich,
HA, Forgejo działające, nie okno nocnej ciszy), 3 kolejne wywołania `/api/embeddings`
po `ollama pull bge-m3`:
| Wywołanie | Latencja |
|---|---|
| 1 (pierwsze, zimny start) | 5.25 s |
| 2 | 4.41 s |
| 3 | 4.16 s |
Brak przyspieszenia między wywołaniami — zgodnie z projektem (`OLLAMA_KEEP_ALIVE=0`
zwalnia model po każdym requeście, `ollama ps` pokazuje zero rezydentnych modeli
między wywołaniami).
RAM: baseline idle ~66 MiB, szczyt podczas burst ~983 MiB (`docker stats`, próbkowane
co 0.3 s w trakcie 3 wywołań) — komfortowo w granicach ceilingu `2560m`. `free -h`
systemowe: `available` nie spadło poniżej ~1.3 GiB w trakcie, osiadło na ~4.2 GiB po
(dla porównania: przed startem eksperymentu `available` = 3.7 GiB).
**Werdykt**: oba kryteria planu spełnione (latencja pojedyncze sekundy, nie
dziesiątki; RAM ze sporym zapasem) → **włączony jako domyślny fallback**, bez flagi
`KB_QUERY_LOCAL_FALLBACK_ENABLED`.
## 5. Bramka jakościowa (plan §9)
Wszystko uruchomione z `~/kb/venv` na PIHA (istniejący venv z poprzednich sesji,
`aiohttp`/`asyncpg`/`yaml` już obecne) przeciw żywej bazie + żywemu kb-query.
**HTTP-equivalence** (`--transport http` vs `--transport direct`, SOLARIA up, ten sam
`--gate-n 10`): oba PASS, **0 rozbieżności** w `dist` na wszystkich zapytaniach
(`flat_top1_dist`, `hybrid_top1_dist`, `cascade[10].top1_dist`) — identyczne bit w
bit, jak wymagał plan (nie ±epsilon, bo to ten sam kod, HTTP to tylko opakowanie).
**Live sol-down fallback test**: symulacja przez `OLLAMA_URL=http://solaria:1`
(zły port, zgodnie z rekomendacją planu — zero dotknięcia SOLARII/innych
konsumentów Ollamy) w `.env` kb-query, restart kontenera. `/healthz`
`sol_status: "down"`. `/search` → 200, wyniki z PIHA, ~4.3 s (zgodnie z kalibracją).
Pełna bramka `retrieval_eval.py --transport http` z SOLARIA-down: **PASS**
identyczny wzorzec hit@3 co na SOLARII, `dist` w granicach epsilon:
| Zapytanie | dist (SOLARIA) | dist (PIHA fallback) | Δ |
|---|---|---|---|
| 1 | 0.341780 | 0.341509 | 0.000271 |
| 2 | 0.324808 | 0.324858 | 0.00005 |
| 3 | 0.428898 | 0.429184 | 0.000286 |
| 4 | 0.448199 | 0.447903 | 0.000296 |
| 5 | 0.386901 | 0.386816 | 0.000085 |
| N (negative control) | 0.598301 | 0.598017 | 0.000283 |
| N2 (negative control borderline) | 0.529772 | 0.529530 | 0.000242 |
Maksymalna rozbieżność: **~3e-4** — rząd wielkości mniejszy niż oczekiwany przez plan
(1e-31e-2), kolejność top-k identyczna, wynik bramki (`gate.passed`) identyczny.
Kb-query przywrócony do normalnej konfiguracji po teście (`.env` z prawdziwym
`OLLAMA_URL`, restart), `/healthz` z powrotem `sol_status: "up"`.
## 6. Deploy
Kod nie był jeszcze zmergowany do `master` (dyscyplina worktree: agent nigdy nie
mergeuje/pushuje `master`) — deploy przez standardowy `deploy-node.sh`
niedostępny bez mastera. Zamiast tego: `rsync` zmienionych plików
(`packages/kb-retrieval`, `services/kb-query`, `services/ollama-piha`,
`hosts/piha/runtime/ollama-piha`, `hosts/piha/services.yaml`,
`jobs/documents-ingest/eval/retrieval_eval.py` + README) do żywego checkoutu
`~/homelab-codex-ws` na PIHA (bez zmiany brancha — working tree pozostaje na
`master` z niescommitowanym diffem 1:1 identycznym z tą gałęzią), potem
standardowy `docker compose ... up -d --build` z tego miejsca. Efekt: realny,
działający deploy, ale **repo na PIHA ma dziś dirty working tree** — wymaga domknięcia
(patrz "Do zrobienia przez operatora" niżej).
Zweryfikowane: `kb-query` (healthy), `ollama-piha` (healthy, `bge-m3` w wolumenie),
`curl https://kb.kapala.org/healthz``200 {"sol_status":"up"}`,
`curl https://kb.kapala.org/search?q=test``200`.
## Stan na koniec sesji
| Element | Status |
|---|---|
| `packages/kb-retrieval``embed_chunk(timeout_s=...)` | ✅ kod + testy |
| `services/kb-query/app/fallback.py` — maszyna stanów | ✅ kod + testy (38/38 kb-query, 25/25 kb-retrieval) |
| `services/ollama-piha` — nowy serwis GitOps | ✅ zdefiniowany, ✅ LIVE na PIHA |
| Natywny `ollama.service` na PIHA (osierocony) | ✅ wyłączony (nie odinstalowany) |
| Kalibracja RAM/latencja | ✅ zmierzone — werdykt GO |
| `retrieval_eval.py --transport http` | ✅ zaimplementowane, ✅ PASS na żywo |
| Live sol-down fallback test | ✅ PASS, Δ~3e-4 |
| Deploy kb-query + ollama-piha na PIHA | ✅ LIVE, working tree PIHA dirty (patrz niżej) |
| Merge do `master` | ⛔ nie wykonany (dyscyplina worktree — operator) |
## Do zrobienia przez operatora
1. **Merge** `task/kb-f4-fallback``master` (`scripts/dev/agent.sh merge` albo
ręcznie) — branch popchnięty do `origin/task/kb-f4-fallback` (patrz commit poniżej).
2. Na PIHA: `cd ~/homelab-codex-ws && git status` będzie dirty (diff identyczny z tym
commitem, bo już wdrożony ad-hoc przez `rsync` w tej sesji) — po mergu do mastera,
`git checkout -- .` (working tree już ma dokładnie tę treść) albo zwyczajnie
`git pull` po mergu powinien wylądować "already up to date"/no-op, bo pliki na
dysku już są zgodne z tym co przyjdzie z mastera. **Zweryfikować** `git diff` jest
puste po pull, nie zakładać.
3. Backlog: natywny `ollama.service` na PIHA wyłączony `systemctl disable --now`
2026-07-27 (§3 wyżej) — jeśli nic się nie posypie przez ~2 tygodnie, odinstalować
binarkę/unit całkiem.
4. OIDC dla kb-query nadal odłożone (decyzja z 2026-07-23) — nie w zakresie tej sesji.

View file

@ -1,31 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-28
links: []
---
# Session log 2026-07-28
## Session 21:59
### Commits
```
905ad96 docs(architecture): plan naprawy subsystemu A (control-plane) 2026-07-28
e8aa3e3 docs(architecture): recon multiagent 2026-07-27
```
### Files changed
```
kb/phases/subsystem-a-naprawa.md | 60 +++
kb/subsystems/recon-multiagent.md | 551 +++++++++++++++++++++++
2 files changed, 611 insertions(+)
```
### Deploys
None recorded
### Narrative
> _user-provided summary_

View file

@ -1,47 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-30
links: []
---
# 2026-07-30 — HA: legacy zamknięte, MCP read-only (faza 2a)
## Legacy — finał
- Tydzień obserwacji czysty; kontener homeassistant5 już nie istniał przy
próbie rm — usunięty przez nieustalony mechanizm (node-agent cleanup?
remediation?). Katalog /home/pi/homeassistant NIETKNIĘTY (fałszywy alarm:
2>/dev/null maskował Permission denied — lekcja). Backlog: ustalić, co
usunęło kontener (granice autonomii agentów); katalog do kasacji przy
porządkach piha.
## Faza 2a — własny MCP server (decyzja operatora: własny > hass-mcp)
- services/ha-mcp: 7 tools read-only strukturalnie (klient bez metod
mutujących, WS allowlista, test grepujący za call_service), stdio,
reuse ha_api/ha_ws, rejestracja w .mcp.json. 42 testy offline, smoke
na żywym ken (1647 encji, 115 automatyzacji = zgodne z repo).
- Wtopa wdrożeniowa: venv żył w worktree, zginął przy merge-cleanup;
fix: odtworzenie w głównym checkoucie. Backlog: run.sh bootstrap venva.
- Test bojowy (świeży CC, zero kontekstu): MCP wołany natywnie (5 calls),
synteza hybrydowa MCP+repo — pełna mapa salonu z odwróconym indeksem
encja→automatyzacje. Ujawniona luka: brak find_automations_using_entity
(cross-ref robiony grepem). Backlog: dodać tool przed fazą 2b.
## Znaleziska testu bojowego (klasy audytowej)
- Choinka/lampki (tasmota_12, zblampkiregal, tasmota_8) sterowane wyłącznie
przez device_id — niewidoczne dla grep po entity_id; martwe od 26-29.07,
automatyzacje cicho nie działają.
- DRUGA FALA martwych urządzeń 26-29.07 (switche choinki/regału + pilot
4button ponownie) — osobna od awarii 17.07; tłumaczy trend unavailable
305→377. Diagnoza sprzętowa: priorytet podniesiony, dwie daty padów.
- Task porządkowy device_id→entity_id (checklista pkt 17) dostał twardy
dowód zasadności — do wykonania PO diagnozie sprzętowej.
## Następne
- Diagnoza sprzętowa (fizyczna): dwie fale, z2m pokazuje część urządzeń
żywych (baterie OK) — podejrzenie na most z2m↔HA / integrację.
- Faza 2b: propose_change/dry_run/request_approval przez kolejkę
control-plane (pending→approved→executed) + Telegram. Osobna sesja.
- Tool find_automations_using_entity + bootstrap venva w run.sh.

View file

@ -1,90 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-04
links: []
---
# Session log 2026-07-31 — KB Faza 4: zamknięcie + pilot narty27
## Zakres
Finalne domknięcie fazy 4 subsystemu B (KB) — **na twardo** — oraz podsumowanie
pilota fazy 5 (narty27), wykonanego równolegle.
## Faza 4 — zamknięcie (na twardo)
### Dedup podwójnej implementacji fallbacku embed
- Rozstrzygnięcie: master (`e7625cd`, `app/embed_router.py`) = źródło prawdy.
- Z porzuconego brancha uratowano S1S4 (`cb8a19d`):
- `retrieval_eval.py --transport http`
- session log 27.07
- testy luk T1T3
- komentarz kalibracji progów dist.
- Pełny rozbiór obu implementacji: `kb/phases/kb-m5-faza4-fallback-dedup.md`.
### Deploy na PIHA (z mastera)
- `ollama-piha`: named volume `ollama_piha_models`, model bge-m3, `KEEP_ALIVE=0`.
- `kb-query`: port 8230, bind 192.168.31.5, `EMBED_FALLBACK_URL` ustawione.
### Test sol-down — PASS na żywej produkcji
Przebieg: pause ollama@SOLARIA → cache 30 s trzyma `up` → zapytanie przełącza się
one-shot na PIHA (`embed_backend: piha`, wyniki poprawne) → `sol_status: down`
unpause → powrót `up` w ≤35 s.
Dodatkowo zaobserwowano **samoistne, jednorazowe zadziałanie breakera na produkcji**
przyczyna nieustalona, zachowanie zgodne z projektem (przełączenie i powrót bez
utraty odpowiedzi).
### Progi dist (skalibrowane, obowiązują)
`<0.45` hit / `0.450.55` szara strefa / `>0.55` brak odpowiedzi.
## Pilot fazy 5 — narty27 (POC, zostaje na stałe)
Publiczna wystawka `narty27.kapala.org` zbudowana **pełnym wzorcem docelowym fazy 5
w miniaturze**:
markdown+frontmatter (OKF v0.1) → walidator konformancji (`check_okf`) → generatory
(graf cytoscape, karty HTML, tabela porównawcza, zdjęcia z filtrem percepcyjnym,
landing) → statyczny hosting (PIHA nginx + named volume) → publiczny ingress
(NPM VPS + Let's Encrypt).
Źródło treści: `~/narty-2027/saalbach-kb` — lokalny git na SOLARII, **celowo poza repo
infry**. Infra: `services/narty27`.
### Wnioski do przeniesienia na fazę 5 (wiki-kompilat)
1. **OKF v0.1 działa w praktyce**, a pinowanie wersji okazało się słuszne — spec
ewoluuje (v0.2: `timestamp``generated: {by, at}`, provenance first-class;
migracja = jedna zamiana pola). Przy fazie 5 rozważyć start od razu na v0.2 albo
pin v0.1 z zaplanowaną migracją.
2. **Walidator-lint jako stały element pipeline'u** wiki, nie jednorazowy skrypt.
3. **Bug upstreamu `knowledge-catalog`**: generator grafu pomija linki od `/` wbrew
§5.1 własnej spec → napisany własny generator. Kandydat na issue/PR do
`GoogleCloudPlatform/knowledge-catalog`.
4. **Reserved files**: `index.md` bez frontmattera (poza `okf_version` w root) —
walidator to łapie.
5. **Warstwa prezentacji z generatorów** — tani, użyteczny wzorzec do reużycia nad
wiki-kompilatem KB.
6. **Każdy artefakt ma mieć dom w gicie; sesje mają logi.**
## Otwarte po sesji
1. **`expected_envelope` w `mail_queries`** (`jobs/documents-ingest/eval/queries.yaml`)
— nadal `null` (placeholdery, artefakt danych, nie kodu); kryterium 4 bramki na
stubie nie przechodzi wyłącznie z tego powodu.
2. **`hosts/solaria/runtime/ollama/docker-compose.override.yml` — brak w repo**
(rozjazd repo↔runtime na SOLARII).
3. **R1R3 node-agent** (incydent `kb/incidents/2026-07-30-ollama-solaria-vanish.md`)
**niezrobione**: R1 `_prune_stopped_containers` nie może kasować kontenerów
zarządzanych, R2 rate-limit dla `ai_node`/`standard`, R3 logowanie usuniętych
zasobów. Przyczyna nadal aktywna → blokuje/warunkuje fazę mailową
(M1 = mitygacja doraźna na czas backfillu).
## Następne kroki
1. Faza mailowa: batching Ollamy → backfill ~225k kopert → IMAP przyrostówka →
PDF-y (~336) → GDrive Takeout.
2. Blokada/ryzyko: node-agent R1R3 (unfiltered prune) — eskalacja do subsystemu A;
mitygacja M1 na SOLARII na czas backfillu.

View file

@ -1,87 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-05
links: []
---
# Sesja 2026-08-05 — batching embed (start fazy mailowej)
## Weryfikacja zaległości
- **Uninstall natywnego `ollama.service` na PIHA — POTWIERDZONY.** Unit i binarka nie
istnieją.
- **Session log fazy 4 istniał już na masterze** (`e619c00`). Zgłoszony brak był fałszywym
alarmem z niedociągniętego working tree na SOLARII.
- **`task/prune-fix` (R1R3 + M1 VPS) gotowy do review** w subsystemie A — `0526af1`,
285 insertions, z testami.
## Task `kb-mail-batching`
Zmergowany do mastera: `02a0079` + `75116ad`.
### Bug blokujący Etap B (znaleziony i naprawiony)
Goły `builtins.TimeoutError` z wyczerpanego `aiohttp` `ClientTimeout` **nie był łapany**
przez `flush_embed_buffer` — obsługa łapała wyłącznie `ClientError`. Skutek: zawieszona
Ollama (failure mode „przyjmuje połączenie i milczy") wywalała cały run, bez breakera
i bez flushu. Klasy przejściowe wyliczone są teraz jawnie w `TRANSIENT_EMBED_ERRORS`.
### `embed_batch_resilient()`
- Retry z backoffem wykładniczym.
- Po wyczerpaniu retry — probe `/api/tags`:
- backend **żywy** → bisekcja izolująca trujący chunk,
- backend **martwy**`gave_up`, bez bisekcji.
### Semantyka breakera (zmiana)
Breaker liczy **give-upy** (backend down wg probe), nie nieudane batche. Porażka częściowa
nie przesuwa licznika. Zmiana znaczenia `--max-embed-failures` opisana w
`kb/phases/kb-m5-faza-mailowa.md` §9.
### Parametryzacja i metryki
- Flagi: `--batch-size`, `--embed-retries`, `--embed-backoff`, `--embed-timeout`
(env `MAIL_INGEST_*`).
- Metryka `embed_ms_per_chunk`.
- Wiersze zembedowane w umierającym batchu są commitowane przed abortem.
### Decyzja: brak fallbacku SOLARIA→PIHA dla backfillu
Świadomie **nie powstaje** — 790 ms/embed na CPU × 271k ≈ 60 h na współdzielonym nodzie.
Tor online (`embed_router`) zachowuje fallback. Rozdział torów udokumentowany w docstringu
`embed.py` / `embed_batch` oraz w `kb/services/job-mail-body-ingest.md`.
### Benchmark `mail-body-ingest-bench`
Read-only (SELECT + inferencja). Wyniki na SOLARII (bge-m3, GPU), próbka 640 chunków
(avg 1559 znaków):
| batch | ms/chunk |
|------:|---------:|
| 32 | 22.70 |
| 64 | 16.57 |
| 128 | 15.82 |
**Default batch 64 POTWIERDZONY** — zysk z 128 to ~4.5%, a przy 64 koszt bisekcji jest
mniejszy. Ekstrapolacja na 271k chunków: **~1.25 h GPU** vs ~11 h przy per-request.
### Testy
117 testów zielonych.
## Wyjątki procesowe
Żadnych. Commit z PIHA nie zaistniał — CC nie dopisał wskaźnika, uznano za zbędne.
## TODO wynikające
- **Rotacja hasła `kb-postgres`** — poszło do historii shella i na screeny sesji.
- **`POSTGRES_PASSWORD` plaintext w `services/*/service.yaml`** — kandydat na backlog
sekretów.
- **`kb-site` `robots.txt` blokuje fetch Claude** (`ROBOTS_DISALLOWED`) — fix: `X-Robots-Tag
noindex` zamiast `Disallow` (wariant B), przy okazji taska `kb-site`.
- **Backfill 271k** — dopiero po merge + deploy `task/prune-fix` (subsystem A).

View file

@ -1,69 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-05
links: []
---
# Session log 2026-08-05
## Session 22:47
Redeploy node-agenta R1/R2/R3 na flotę (supervised, checkpointy zatwierdzane przez
operatora). Incydent źródłowy: `kb/incidents/2026-07-30-ollama-solaria-vanish.md`.
### Commits
Ta sesja **nie wprowadziła żadnych commitów** poza niniejszym logiem — deploy runtime,
zero zmian w kodzie. Granica wyznaczona fallbackiem 24 h (poprzedni log sesji nie używa
nagłówków `## Session HH:MM`), więc poniższa lista obejmuje też wcześniejszą pracę
operatora z tego samego okna, niezwiązaną z tym deployem:
```
bb3792d docs(kb-site): przekaz ACCESS_TOKEN generatorowi w procedurze publikacji
f5c6f3b fix(kb-site): token bramki w kazdym linku wewnetrznym generatora
04251b5 docs(sessions): log sesji 2026-08-05 — batching embed (start fazy mailowej)
75116ad docs(kb-retrieval): rozdzial torow embed takze w docstringu embed_batch
02a0079 feat(kb-mail-batching): retry + izolacja trujacego chunka w torze embed + benchmark
71eaab0 feat(supervisor): duty-cycle nodes — liveness transitions logged, not actioned
19548d8 fix(kb-site): wycofaj robots.txt — blokowal legalny fetch z tokenem
67e49a0 docs(kb-site): przepisz nieaktualne kb.okit.pl na kb-e2a24af3.okit.pl
db81cb1 feat(kb-site): noindex + robots.txt + obscure subdomain jako domyslny base-url
7282a5e docs(recon): sciezka redeploy — fix jest w repo od 2026-08-03, nie jest wdrozony
```
### Files changed
Brak — drzewo robocze czyste przez całą sesję, poza tym plikiem.
### Deploys
Recon wykazał, że zakres jest węższy niż zakładano: PIHA i VPS **już** miały kod R1/R2/R3
(weryfikacja sha256 pliku w kontenerze vs repo). Realny zakres: SOLARIA i LUSTRO.
LUSTRO nie było w pierwotnej liście, a było jedynym nodem faktycznie kasującym bez filtra.
| Node | Przed | Po | Wynik |
|---|---|---|---|
| SOLARIA | `c80a711f` (2026-07-22, pre-R1) | `438111e2` | OK, bez rollbacku |
| LUSTRO | `460d5cc5` (2026-06-11, 658 linii) | `3260c74a` | OK, bez rollbacku |
| PIHA | `9141cc61` — sha == repo HEAD | bez zmian | deploy pominięty (już aktualny) |
| VPS | `27be875d` — R1/R2/R3 obecne | bez zmian | deploy pominięty (różnice tylko w komentarzach) |
Weryfikacja po deployu na obu wdrożonych nodach: kontener `Up (healthy)`, zero tracebacków,
sha256 `node_agent.py` w runtime == repo HEAD, brak wykonywalnego `containers.prune()`,
`docker ps -a` identyczne z pre-snapshotem, heartbeat świeży. `NODE_TYPE` zachowane
(SOLARIA `lte_node` = M1, LUSTRO `sd_card`). Marker `last-docker-cleanup` na LUSTRO
nie wyzerował się przy recreate.
Test e2e na SOLARII (kanarek `restart=unless-stopped` bez labela compose): zatrzymany,
przeżył 151 s (>2× CHECK_INTERVAL) jako `exited`, nie usunięty. Dry-run logiki filtra
bez wywołania `remove()`: REMOVE=0 na obu nodach.
Obrazy sprzed deployu otagowane `node-agent:rollback-pre-r1` na SOLARII i LUSTRO
(na SOLARII były dangling — groziło zjedzenie celu rollbacku przez przyszły prune).
### Narrative
> _user-provided summary_

View file

@ -1,65 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-06
links: []
---
# Sesja 2026-08-06 — Etap B: weryfikacja korpusu + fix NUL (ZAMKNIĘTY)
## Przebieg plastrów
Plastry 0-4 (`--offset 0/50000/100000/150000/200000 --limit 50000 --batch-size 64`)
**wszystkie EXIT 0** po fixie. Cały korpus 225k kopert przeskanowany
idempotentnie, zero strat.
## Bug znaleziony i naprawiony: NUL byte (0x00) w treści maila
Plaster offset 50k wywalił się na mailach z 2007 (Sony Ericsson, 3 chunki):
bajt NUL w tekście → `asyncpg.CharacterNotInRepertoireError` przy insercie
(PostgreSQL nie przyjmuje `0x00` w `text`).
Fix `4ec0b78`: strip `\x00` przed chunkowaniem i embedem + liczniki
`nul_bytes_stripped` / `mails_nul_sanitized`. Re-run plastra 1: **EXIT 0**,
3 chunki dobrane.
## Weryfikacja w DB (kb-postgres@PIHA, `document_chunk`)
| Miara | Wartość |
|---|---|
| `document_chunk` total | **389 012** |
| nie-excluded **bez** embeddingu | **0** |
| nie-excluded z wektorem | 187 025 |
| newsletter-flagged bez wektora | 201 849 (odwracalne) |
| excluded **z** wektorem | 138 (artefakt kolejności flagowania, nieszkodliwy) |
## Wniosek
**Korpus był w pełni zembedowany jeszcze przed dzisiejszymi plastrami.**
Zapamiętany stan „6,4k embedded z Etapu A, ~225k kopert do backfillu" był
nieaktualny — wcześniejsze przebiegi pokryły całość. Dzisiejsze runy to
w praktyce pełna, idempotentna weryfikacja korpusu (plus wykrycie i naprawa
buga NUL).
Źródło mylącego odczytu: licznik `chunks_already_embedded` liczy **istnienie
wiersza w DB** (w tym chunków newsletter-flagged bez wektora), a nie obecność
wektora — stąd niespójne wrażenie z liczników plastrów.
## Środowisko
- venv w głównym repo (`pip install -e` dla `kb-mail` / `kb-retrieval` /
`mail-body-ingest`).
- tmux `backfill`, logi w `~/kb/mail/ingest-logs/` (poza repo).
## Follow-upy
- `jobs/gmail-header-backfill` i `jobs/gmail-bulk-import` używają
`sanitize_surrogates` na nagłówkach zapisywanych do `jsonb` — **ta sama
latentna podatność na NUL**. Nieruszone w tej sesji, osobny task.
- **Rotacja hasła `kb-postgres`** — nadal otwarta (z sesji 2026-08-05).
## Następny krok fazy mailowej
**IMAP przyrostówka gmail + fastmail** (`source='fastmail'`) — teraz odblokowana.

View file

@ -1,107 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-06
links: []
---
# Sesja 2026-08-06 (wieczór) — przyrostówka IMAP na żywo (Krok 7 fazy mailowej DONE)
## Ścieżka: recon → decyzje → implementacja
Recon (`75d9695`, `kb/audits/mail-sync-2026-08-06.md`) → decyzje **(a)(g) zatwierdzone
w całości** → implementacja w trzech commitach:
| Commit | Zawartość |
|---|---|
| `c65f0f2` | adapter IMAP w `packages/kb-mail`, migracja 005 `mail_sync_state` |
| `f056b08` | job `mail-imap-sync` |
| `ae16deb` | takt `kb-ingest` co 2h + etap mailowy, korekta `kb-mail-pillar` (JMAP→IMAP) |
**642 testy.**
## Pierwsze uruchomienie wg runbooka `mail-sync-run.md` — z incydentami
### Run #1 — padł na `UID SEARCH ALL`
Gmail: `UID SEARCH ALL` na **227 900** wiadomości przekroczył `imaplib._MAXLINE`
(1 MB) → crash.
Przyczyna: błąd operatora w `.env``MAIL_GMAIL_INITIAL_MODE=full` zamiast `since`,
a `FASTMAIL_INITIAL_MODE` w ogóle niewpisany (fastmail zdążył zsynchronizować
new-only i zapisać stan).
> **FOLLOW-UP do CC:** utwardzić search na duże foldery — zakres UID / `SINCE`
> zamiast `ALL`, podbicie `_MAXLINE`. Obecnie **tryb `full` na dużym koncie = crash**.
### Naprawa
Korekta `.env` + `DELETE` stanu fastmail z `mail_sync_state` → run #2 czysty.
### Run #2 — initial
| Konto | Tryb | Seen | Inserted | Dup | Conflict |
|---|---|---:|---:|---:|---:|
| gmail | initial (SINCE=2026-06-15) | 1 872 | 1 692 | 180 | — |
| fastmail | initial-full | 96 | 87 | 1 | 8 (`conflict_other_source`) |
Razem **1 779 nowych kopert, 0 błędów**. Nakładka `SINCE` zadziałała; cross-account
dedup po `Message-ID` działa (te 8 konfliktów to ta sama poczta widziana z drugiego
konta).
### Run #3 — test przyrostowości
`mode=incremental`: gmail **+5**, fastmail **0**, kursor OK.
## Drenaż embed
`mail-body-ingest --only-unchunked`: **698 embeddingów** (baseline 187 163 → **187 861**).
> **Uwaga (follow-up):** tempo ~570 ms/chunk sugeruje, że `OLLAMA_URL=SOLARIA` nie
> przebił się przez `sudo env` i liczyło CPU PIHA. Do weryfikacji przy następnym
> dużym drenażu.
## Test end-to-end — PASS
Mail wysłany 17:21 (gmail→fastmail), obie kopie wylądowały poprawnie: envelope
gmail + `conflict_other_source` fastmail. Ścieżka **sync → ingest → hybrid search**:
top-1 dist **0.391**. Decyzja **(g) potwierdzona**.
## Automat
- `kb-mail-sync.timer`**enabled**, tick co ~1 h, pierwszy 18:01.
- `kb-ingest.timer` — co 2 h (nowa definicja `0/2:00:00`).
- Reguła `fleet-prometheus/kb-mail-sync.yml` wchodzi przy najbliższym deployu
fleet-prometheus *(follow-up)*.
## Poprawki do runbooka (follow-up, nie zrobione)
- Ręczne runy wymagają `sudo` do odczytu `.env` (`600 root:root`) — podać wariant
`sudo bash -c`.
- Krok 7: `curl` na `localhost:8230` nie zadziała — `kb-query` binduje na
`192.168.31.5`.
## Otwarte
- **Rotacja hasła `kb-postgres` (WISI)** — wyciekło 2026-08-05/06 do transkryptu
i historii shella. **Trzeci udokumentowany przypadek tej klasy.**
- Fix `UID SEARCH` (patrz Run #1).
- Załączniki PDF z maili (~336 + nowe).
- NUL w nagłówkach `jsonb` (`gmail-header-backfill` / `bulk-import`).
## Stan fazy mailowej po sesji
| Element | Status |
|---|---|
| (a) batching | ✓ |
| (b) Etap B | ✓ |
| (c) regresja | ✓ |
| (d) hybrid default | ✓ |
| Krok 7 — przyrostówka | ✓ **ŻYWA** |
Pozostało: backfill PDF, Google Drive Takeout.
**Następna duża rzecz wg roadmapy: FAZA 5 — wiki-kompilat.** Materiał mailowy jest
kompletny i świeży, więc warunek wejścia spełniony.

View file

@ -1,496 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-06
links: []
---
# Session log 2026-08-06
## Session 13:20
Produkcyjna weryfikacja pierwszego cyklu safe-cleanup na LUSTRO (R1/R2/R3, deploy
2026-08-05) oraz pierwszy pełny cykl HITL: approval → dispatch → wykonanie na nodzie →
`action_result``completed`. Sesja supervised, checkpointy zatwierdzane przez operatora,
approvale wykonywane wyłącznie przez operatora. Incydent źródłowy:
`docs/incidents/2026-07-30-ollama-solaria-vanish.md`.
### Commits
Ta sesja **nie wprowadziła żadnych commitów poza niniejszym logiem** — zero zmian w kodzie
i konfiguracji repo. Cała praca to recon read-only + kontrolowane zapisy runtime na
LUSTRO i VPS (kanarki testowe, plik akcji), wszystkie sprzątnięte lub udokumentowane niżej.
Poprzedni log sesji: `67aa092 docs: session 2026-08-05 22:47` — potwierdzony na
`origin/master` (ahead/behind 0/0) na starcie sesji.
### Files changed
Brak — poza tym plikiem.
---
## KROK 1 — safe-cleanup na LUSTRO: obie gałęzie potwierdzone produkcyjnie
### Stan wyjściowy
Marker `/opt/homelab/state/last-docker-cleanup` = `1785925612` (2026-08-05 12:26:52 CEST),
**sprzed deployu** (obraz node-agenta utworzony 22:42:05 CEST). Bramka
`_cleanup_rate_ok()` (`CLEANUP_INTERVAL_SECS = 86_400`) trzymała pierwszy cykl nowego kodu
do 12:26:52 dnia 2026-08-06. Brak linii cleanup w logach do tego momentu był więc
zachowaniem poprawnym, nie awarią.
Kod w runtime zweryfikowany: `md5(/app/src/node_agent.py)` == `md5(repo HEAD)` =
`c9ac64e10b42b3e0ed9e4c168579bfaa`, brak wykonywalnego `containers.prune()`,
`NODE_TYPE=sd_card`.
Pułapka interpretacyjna: logi kontenera node-agent są w **UTC**, host w CEST. Pozorna
6-godzinna dziura w logach to nocny `halt` (root cron `30 23 * * * /usr/sbin/halt`,
boot 06:30) — LUSTRO ma duty cycle jak SOLARIA, zgodnie z `inventory/topology.yaml`
(`duty_cycle: nightly`).
### Test w oknie prune
Do okna przygotowano trzy kontenery `exited` pokrywające obie gałęzie filtra:
| Kontener | Polityka / label | Oczekiwane | Wynik |
|---|---|---|---|
| `prune-disposable` | `restart=no`, brak compose | usunięty | ✅ usunięty |
| `prune-canary` | `restart=unless-stopped`, brak compose | zachowany | ✅ przeżył |
| `node-exporter` (realny serwis) | `restart=always`, brak compose | zachowany | ✅ przeżył |
Log z okna (12:27:49 CEST / 10:27:49 UTC):
```
INFO - Pruned dangling images (0 MB reclaimed)
WARNING - Removed 1 disposable stopped container(s): prune-disposable (kept 2 managed)
```
Marker zaktualizowany na `1786012069`. `kept 2` = `prune-canary` + `node-exporter`.
Wszystkie kontenery produkcyjne nietknięte.
**Werdykt:** obie gałęzie filtra działają na produkcji — gałąź ochronna (kontener zatrzymany
przez operatora z polityką restartu przeżywa prune; dokładny scenariusz incydentu ollamy)
oraz gałąź usuwania (filtr nie jest no-opem, faktycznie kasuje jednorazowe resztki).
### Korekta wniosku z 2026-08-05
Wczorajszy test kanarka na SOLARII (przeżył 151 s) **nie dowodził działania filtra**
SOLARIA ma `NODE_TYPE=lte_node` (mitygacja M1), gdzie `run_safe_cleanup()` kończy się
`return` przed jakimkolwiek prune. Ten test potwierdził M1, nie R1. Dowód dla R1 powstał
dopiero dziś na LUSTRO.
### Stan cleanupu na flocie
| Node | NODE_TYPE | Cleanup |
|---|---|---|
| PIHA | `sd_card` | ✅ działa (2026-08-05 16:27 CEST: `kept 0 managed`) |
| LUSTRO | `sd_card` | ✅ działa (2026-08-06 12:27:49, jw.) |
| SOLARIA | `lte_node` (M1) | wyłączony w całości, brak markera |
| VPS | `lte_node` (M1) | wyłączony w całości, brak markera |
M1 nadal zdjęte do zrobienia na SOLARII i VPS — do tego czasu te nody nie sprzątają
Dockera wcale.
---
## KROK 2 — dlaczego crash-loop watchtowera nie generował akcji
Eventy płynęły poprawnie: `evt-lustro-<ts>-containers_not_running-watchtower.json` co ~60 s,
11 317 plików w `events/lustro/` na VPS (node-agent rsyncuje z `--remove-source-files`,
stąd pusty katalog lokalny). To nie był problem transportu ani emisji.
**Przyczyna:** `supervisor.reconcile()` iteruje wyłącznie po `desired_state["services"]`
ładowanym z `hosts/<node>/services.yaml` (`supervisor.py:380`). `hosts/lustro/services.yaml`
deklaruje `node-agent`, `node-exporter`, `piper-tts` — watchtowera tam nie ma. Brak wpisu
w desired state ⇒ brak driftu ⇒ brak rekomendacji. Ponad 11 tys. eventów dead-enduje.
Observer natomiast **zna** `lustro/watchtower` (incydent `inc-1786007596-lustro-watchtower`,
status `unhealthy`) — rozjazd dotyczy wyłącznie supervisora.
Wykluczone: `shadow_mode` (dotyczy tylko `HA_DIAG_SHADOW_MODE`, ścieżka HA-diag),
`duty_cycle` (tłumi wyłącznie liveness node'a), progi/cooldown (`containers_not_running`
jest w `CONTAINER_RESTART_TRIGGERS`, dedup po stabilnym ID).
Konsekwencja druga: akcja wstawiona ręcznie do `pending/` dla serwisu spoza desired state
żyje jeden cykl supervisora. `_cancel_resolved_pending_actions()` (`supervisor.py:560`)
skasował ją po 15 s z powodem `service_removed_from_desired_state`. Kasowanie dotyczy
**wyłącznie `pending/`** — `approved/` i `running/` są z założenia nietykalne. Dlatego
akcja ręczna dla watchtowera trafiła ostatecznie prosto do `approved/`.
---
## KROK 3 — dwa pełne cykle HITL
Wszystkie znaczniki UTC (CEST = +2). Pętla executora: 10 s. Pętla node-agenta: 60 s.
### Cykl 1 — `node-exporter` (ścieżka w pełni organiczna)
| Etap | Timestamp | Δ |
|---|---|---|
| `docker stop node-exporter` (trigger) | 10:22:49 | — |
| event → observer → incydent `inc-1786011821-lustro-node-exporter` | ~10:23:41 | +52 s |
| supervisor: `Generated recommendation``pending/` | 10:24:00.66 | +71 s |
| approval operatora (`mv` → `approved/`) | ~10:38:5x | — |
| executor: `Executing action``running/` | 10:39:00.650 | ≤10 s |
| executor: `Dispatched … to node-agent on lustro` | 10:39:00.657 | +7 ms |
| node-agent: rsync-pull + bramki + `docker restart` | 10:39:23.15 | +22,5 s |
| event `action_result` (`success: true`) | 10:39:23.293 | +0,14 s |
| executor: `completed` | 10:39:30.752 | +7,5 s |
**Approval → completed: 30,1 s.**
### Cykl 2 — `pi-watchtower-1` (akcja utworzona ręcznie, zatwierdzona przez operatora)
| Etap | Timestamp | Δ |
|---|---|---|
| operator zapisuje akcję do `approved/` | 11:08:38 | — |
| executor: `Executing action``running/` | 11:08:40.829 | +2,8 s |
| executor: `Dispatched … (container=pi-watchtower-1)` | 11:08:40.838 | +9 ms |
| node-agent: `Restarted container 'pi-watchtower-1'` | 11:08:51.002 | +10,2 s |
| event `action_result` (`success: true`) | 11:08:51.002 | — |
| executor: `completed` | 11:09:00.909 | +9,9 s |
**Approval → completed: 20,1 s.** Watchtower wrócił do crash-loopa — zgodnie z założeniem;
sukcesem było przejście pipeline'u i poprawny `action_result`, nie uzdrowienie kontenera.
### Bramki agenta
- **Whitelista typu** i **node scoping** — przeszły; logują się tylko przy odrzuceniu,
więc dowodem przejścia jest sama egzekucja.
- **Self-restart guard** — nie dotyczył (cel ≠ `node-agent`).
- **Idempotencja** — zadziałała na żywo i wielokrotnie:
`Action … already processed — skipping (idempotency)`, markery
`/opt/homelab/state/processed-actions/<action_id>.done`.
### Obserwacja uboczna: regeneracja i auto-cancel
O 10:39:48 (18 s po udanym restarcie) supervisor **wygenerował ponownie** akcję dla
node-exportera, bo world state jeszcze pokazywał `unhealthy` (opóźnienie observera).
O 10:40:49 sam ją skasował (`drift_resolved_auto`). Podwójnego restartu nie było, ale
istnieje ~60-sekundowe okno, w którym po udanej remediacji potrafi powstać duplikat.
---
## KROK 4a — rekomendacja ws. poluzowania bramek HITL
**Rekomendacja: jeszcze nie, ale wąskie poluzowanie jest obronialne po trzech warunkach.**
Za:
- Pipeline przeszedł end-to-end dwukrotnie, w tym raz w pełni organicznie (event →
observer → supervisor → approval → executor → node-agent → wynik).
- Czas maszynowy to 2030 s; wąskim gardłem jest wyłącznie człowiek (dziś ~15 i ~30 min).
- `container_restart` jest tanie i odwracalne, wykonanie jest scoped do node'a, whitelisty
jednego typu akcji i guardu self-restartu; egzekutor nigdy nie wchodzi na node po SSH.
- Kolejka sama się czyści: `drift_resolved_auto` kasuje akcje, które przestały być
potrzebne, więc opóźniony approval nie powoduje zbędnego restartu.
- Idempotencja obroniła się w warunkach bojowych (patrz defekt dispatch niżej).
Przeciw:
- Próbka: 2 wykonania, 1 node, 1 typ akcji, obie ścieżki udane. **Ani razu nie zaobserwowano
ścieżki porażki** (`success: false`), timeoutu akcji w `running/`, ani odrzucenia przez
bramkę node/whitelisty. Dowód dotyczy szczęśliwej ścieżki.
- Restart nie leczy przyczyn źródłowych. Watchtower ma 1000+ restartów dziennie — automat
restartowałby go w kółko, maskując problem. Bez budżetu restartów (np. max 3/24 h na
serwis, potem eskalacja do `alert_only`) auto-remediacja produkuje pętlę zamiast naprawy.
- Otwarty defekt dispatch (niżej) w trybie automatycznym oznacza, że jedynym zabezpieczeniem
przed powtórnym wykonaniem jest marker idempotencji per `action_id`. Wystarczy nowy
`action_id` na ten sam objaw, by restart poszedł ponownie.
- Okno duplikatu (~60 s) po udanej remediacji — dziś skasowane w porę, ale to kwestia
wyścigu, nie gwarancji.
- `_get_container_name()` po cichu zwraca nazwę serwisu, gdy brak `services/<svc>/docker-compose.yml`.
Dla watchtowera dałoby to `watchtower` zamiast `pi-watchtower-1` — akcja wygenerowana
organicznie zakończyłaby się `failed`. W trybie automatycznym to stały szum porażek.
Warunki wstępne do poluzowania:
1. Naprawa wycieku plików dispatch (niżej) — inaczej automat stoi na jednej bramce.
2. Budżet restartów per serwis + eskalacja do `alert_only` po jego wyczerpaniu.
3. Poluzowanie tylko dla `container_restart` i tylko dla serwisów obecnych w desired state;
`redeploy` i `disk_cleanup` zostają w pełnym HITL.
---
## KROK 4b — root cause crash-loopa watchtowera
Log kontenera, każde uruchomienie:
```
level=error msg="Error response from daemon: client version 1.25 is too old.
Minimum supported API version is 1.40, please upgrade your client to a newer version"
```
`pi-watchtower-1` (`containrrr/watchtower`, obraz `c352868a1654`, kontener utworzony
2025-04-15) rozmawia z socketem Dockera przez API 1.25. Demon na LUSTRO wymaga minimum
1.40 i odrzuca połączenie, watchtower kończy się `exit 1`, `restart=always` uruchamia go
ponownie — cykl ~60 s. Licznik restartów kasuje się przy nocnym `halt`/boot, stąd
„973 restarty" to dorobek jednego dnia pracy, a nie narastająca awaria.
Co by go naprawiło (do backlogu, **nie wykonane w tej sesji**):
1. `docker pull containrrr/watchtower:latest` + recreate — aktualne wydania negocjują
nowsze API. Najprostsze.
2. Obejście: `DOCKER_API_VERSION=1.41` w env kontenera.
3. **Preferowane:** usunąć watchtowera z LUSTRO. To relikt spoza GitOps, a automatyczne
podmienianie obrazów na edge'owym Pi kłóci się z modelem repo jako źródła prawdy.
Jeśli ma zostać — dopisać go do `hosts/lustro/services.yaml`, bo dopiero wtedy stanie
się widoczny dla supervisora.
Efekt uboczny do rozważenia niezależnie: watchtower generuje ~1440 eventów/dobę, które
nigdzie nie prowadzą, i jest głównym powodem, dla którego `events/lustro/` ma 11 tys. plików.
---
## Follow-upy
1. **Wyciek plików dispatch (nowy defekt, potwierdzony).** Executor tworzy
`actions/dispatch/<node>/` z uprawnieniami **755** (`aerbot:aerbot`), a rsync-pull leci
jako `oskar` (grupa `aerbot`) — brak prawa zapisu w katalogu, więc
`--remove-source-files` nie kasuje źródła. Dla porównania `dispatch/piha` ma 775.
Dodatkowo rsync zwraca wtedy kod 23, który node-agent traktuje jako benign
(`returncode not in (0, 23, 24)`) → **cicha porażka, zero ostrzeżeń**. Skutek: LUSTRO
re-pulluje te same akcje co 60 s i odbija się od bramki idempotencji — w nieskończoność.
Docstring `pull_dispatched_actions()` twierdzi, że plik jest kasowany po pobraniu; nie jest.
Fix: `mkdir(mode=0o775)` w executorze + osobna obsługa rc=23 przy `--remove-source-files`.
Do czasu naprawy na LUSTRO trwa zombie re-pull dwóch plików co 60 s.
2. `_get_container_name()` — cichy fallback na nazwę serwisu przy braku
`services/<svc>/docker-compose.yml`; produkuje akcje celujące w nieistniejące kontenery.
3. Okno ~60 s, w którym po udanej remediacji powstaje duplikat akcji (opóźnienie observera).
4. Root cause watchtowera — patrz KROK 4b.
5. Zdjęcie M1 (`NODE_TYPE=lte_node`) na SOLARII i VPS — do tego czasu zero cleanupu Dockera
na obu nodach.
6. 17 zwietrzałych akcji w `pending/` z czerwca i lipca (16× `alert-*`, `redeploy-vps-gokapi`)
— nikt ich nie zamyka, zaśmiecają kolejkę operatora.
7. `events/lustro/` — 11 tys. plików, rosnące głównie przez watchtowera.
## Pominięte / niepewne
- Bramki node-scoping i whitelisty typu potwierdzone **tylko pośrednio** (przez udaną
egzekucję), bez testu negatywnego.
- Ścieżka porażki (`action_result` z `success: false`) oraz timeout akcji w `running/`
nie zostały przetestowane.
- Gałąź prune obrazów wykonała się na zerze — `0 MB reclaimed` przy 0 dangling images
przed i po. Potwierdza, że kod się wykonuje, nie że potrafi cokolwiek odzyskać.
- Wycieknięte pliki dispatch na VPS usunięte ręcznie przez operatora na koniec sesji.
Sam defekt (uprawnienia 755 + połknięty rc=23) pozostaje — wyciek wróci przy następnej
akcji dispatchowanej na LUSTRO.
- Cykl HITL sprawdzony wyłącznie na LUSTRO. PIHA (jedyny inny node z aktywnym dispatch)
nie był testowany.
## Sprzątanie
- `prune-canary` — usunięty po weryfikacji (12:29:25).
- `prune-disposable` — usunięty przez sam cleanup, zgodnie z zamysłem testu.
- Obraz `alpine` (ściągnięty na potrzeby kanarków) — usunięty.
- `node-exporter` — działa, podniesiony **przez pipeline HITL**, nie ręcznie.
- `actions/dispatch/lustro/` na VPS — opróżniony przez operatora (zombie re-pull ustał).
- LUSTRO na koniec: `node-agent` (healthy), `node-exporter` (up), `piper-tts` (up),
`pi-watchtower-1` (restarting — bez zmian, świadomie).
### Narrative
> _user-provided summary_
---
## Session 15:25
Wdrożenie do runtime dwóch fixów zmergowanych na `master` (supervised, checkpointy
zatwierdzane przez operatora): dispatch `0o775` + rc=23 w executorze/node-agencie
(`52eca1c`) oraz zdjęcie mitygacji M1 na SOLARII i VPS (`1bab321`). Domyka follow-upy
#1 i #5 z sesji 13:20.
### Commits
Ta sesja **nie wprowadziła żadnych commitów** poza niniejszym logiem — deploy runtime,
zero zmian w kodzie. Wdrożone commity powstały wcześniej, na branchu
`task/dispatch-perms-m1`:
```
1bab321 revert(m1): zdjecie NODE_TYPE=lte_node na SOLARII i VPS po wdrozeniu R1
52eca1c fix(dispatch): inbox 0o775 + rsync rc=23 przestaje byc cichy
```
W trakcie sesji main checkout przesunął się o `75d9695 docs(recon): przyrostowka IMAP
gmail + fastmail` (druga sesja operatora, docs-only). Bez wpływu: `node_agent.py` ma to
samo `sha256 aec6cb03` w obu drzewach, więc SOLARIA — zdeployowana jeszcze z `1bab321`
nie rozjechała się z VPS-em deployowanym z `75d9695`.
### Files changed
Brak — drzewo robocze czyste przez całą sesję, poza tym plikiem.
### Deploys
| Node | Serwis | Przed | Po | Wynik |
|---|---|---|---|---|
| SOLARIA | node-agent | `node_agent.py` `fee079e8`, obraz `438111e2` | `aec6cb03` == repo HEAD, obraz `bc28a30a` | OK |
| VPS | node-agent | `node_agent.py` `c21967d3`, obraz `27be875d` | `aec6cb03` == repo HEAD | OK |
| VPS | control-plane | `executor.py` `5ca0490e`, 4 obrazy z 2026-08-05 | `1c3b569f` == repo HEAD, 4 obrazy przebudowane | OK |
Mechanizm: `scripts/deploy/deploy-service.sh --build-if-needed` dla node-agenta (ta sama
ścieżka co 2026-08-05). **Nie** użyto `scripts/deploy/deploy.sh <target>`: jest to
dyspozytor Saturn-side po SSH, deployujący *cały* node — na VPS ruszyłby npm, outline,
joplin i ai-cluster, czyli daleko poza zakres, a sesja toczyła się z SOLARII (`ssh solaria`
to połączenie do samego siebie).
`NODE_TYPE` po zdjęciu M1: SOLARIA `lte_node`**`ai_node`** (jawnie w override),
VPS `lte_node`**linia usunięta**, `NODE_TYPE=""` z base compose → `_resolve_node_type()`
zwraca `standard`. Log startowy potwierdza jedno i drugie (`type=ai_node`, `type=standard`),
czyli przewidywanie z commita `1bab321` co do pustego stringa było trafne.
Gate testowy: **pytest niedostępny w main checkoucie** (`.venv` bez pytest,
`~/.local/bin/pytest` ma zepsuty `_pytest`). Oparto się na wyniku sprzed merge'a
(node-agent 70 passed, control-plane 173 passed) plus `docker build` obu stacków przy
deployu. Follow-up 15:25/#4.
### Pierwszy cykl cleanup po zdjęciu M1
Na obu nodach marker `/opt/homelab/state/last-docker-cleanup` **nie istniał** (M1 blokował
zapis od 2026-08-04), więc `_cleanup_rate_ok()` zwrócił `True` i prune poszedł w pierwszym
cyklu, ~0,5 s po starcie — zgodnie z ostrzeżeniem w `1bab321`. Dlatego kolejność w każdym
kroku była: **najpierw tagi rollback, potem deploy.**
SOLARIA:
```
INFO - node-agent starting: node=solaria type=ai_node
INFO - Pruned dangling images (0 MB reclaimed)
INFO - No disposable stopped containers (kept 0 managed)
INFO - Pruned build cache (91 MB reclaimed)
```
VPS:
```
INFO - node-agent starting: node=vps type=standard
INFO - Pruned dangling images (0 MB reclaimed)
INFO - No disposable stopped containers (kept 0 managed)
INFO - Pruned build cache (239 MB reclaimed)
```
**Zero ubytków kontenerów na obu nodach** — `docker ps -a` przed vs po, diff nazw pusty
(SOLARIA 9/9, VPS 24/24). `humanai-mailer` i `humanai-landing` (bez definicji w repo)
nietknięte. Tagi `rollback-*` przeżyły prune.
Dwie prognozy przedwdrożeniowe wymagały korekty — obie z tego samego powodu, że
`docker images` pokazuje **rozmiar pozorny z warstwami współdzielonymi**, a nie realny
odzysk:
* **SOLARIA, „4 dangling ≈ 553 MB":** cztery obrazy faktycznie zniknęły (35 → 32, przy
+1 nowym buildzie), ale `SpaceReclaimed` = **0 MB**. Ich warstwy są współdzielone z
control-plane i kb-query. Realny odzysk obrazów ≈ 70 MB (17,89 → 17,82 GB); z build
cache (606,1 → 510,5 MB) łącznie ≈ 165 MB.
* **VPS, „1 dangling 395 MB":** ten obraz to **żywy `outline-postgres-1`** — untagged, ale
oznaczony `U` (in use). Docker odmawia usunięcia obrazu używanego przez kontener, więc
prune go nie ruszył i **nie miał prawa ruszyć**. Realny odzysk to wyłącznie build cache
239 MB (z 250,8 MB reclaimable).
Kontener `control-plane-ui` na SOLARII stoi w stanie `created` — poza zasięgiem prune'a
podwójnie: `_prune_stopped_containers()` listuje wyłącznie `status=exited`, a kontener ma
i tak label compose oraz `restart=unless-stopped`.
### Weryfikacja fixu dispatch end-to-end
Stan wejściowy zdjęty **przed** deployem control-plane (patrz follow-up 15:25/#1 — inaczej
`deploy-local.sh` by go zatarł):
| ścieżka | mode | |
|---|---|---|
| `actions/dispatch/piha` | 775 | historycznie działał |
| `actions/dispatch/lustro` | **755** | wyciek |
| `actions/deploy` | 755 | inbox deploy-runnera, bez podkatalogów |
**Korekta do sekcji „Sprzątanie" z sesji 13:20.** Zapis „`actions/dispatch/lustro/` na VPS
— opróżniony przez operatora (zombie re-pull ustał)" **nie odzwierciedla stanu
faktycznego**: o 13:16 oba pliki (z 10:39 i 11:08) nadal leżały w źródle, a LUSTRO
re-pullowało je co 60 s aż do 13:18:23. Katalog zdrenował się dopiero w wyniku poniższego
testu — i mógł, bo dopiero wtedy miał prawa `775`.
Mechanizm potwierdzony co do joty: inbox `aerbot:aerbot 755`, a ciągnie z niego
`VPS_EVENTS_USER=oskar` — uid **1002**, tylko *członek* grupy `aerbot` (gid 1000). Grupa ma
`r-x` bez `w`, więc `--remove-source-files` nie może zrobić unlinku:
```
13:15:13 INFO - Action container-restart-lustro-node-exporter already processed — skipping
13:15:13 INFO - Action container-restart-lustro-watchtower already processed — skipping
13:16:16 ... (to samo) 13:17:19 ... (to samo)
```
**Test HITL.** Cel zmieniony z `watchtower` na `node-exporter`: watchtower na LUSTRO jest
w crash-loopie (137 restartów, `restart=always` — root cause opisany w KROKU 4b sesji
13:20), więc nie dałby czystego sygnału. Nowe `action_id`
(`container-restart-lustro-node-exporter-dispatchfix`), bo recykling starego odbiłby się od
bramki idempotencji na LUSTRO i zawiesiłby akcję w `running` do timeoutu.
**Ścieżka: przez `approved/`, nie przez approval operatora.** Plik `pending` zapisany
13:16:56 UTC został auto-anulowany przez supervisora po **7 sekundach**
(`drift_resolved_auto` — `node-exporter` był zdrowy), zanim operator zdążył kliknąć. To
dokładnie zjawisko z sekcji „Obserwacja uboczna: regeneracja i auto-cancel" sesji 13:20,
tym razem szybsze (7 s vs 15 s). Zgodnie z ustaleniem przed testem druga kopia trafiła
prosto do `approved/`; cel testu to mechanika dispatchu, nie ścieżka approvalu. Pole
`status` w tej kopii zostało `cancelled` — bez znaczenia, executor kieruje się katalogiem,
nie polem.
Wynik — wszystkie sześć kryteriów spełnione:
1. **755 → 775** dokładnie w momencie zapisu przez executora (13:17:23) ✅
2. plik akcji **zniknął** ze źródła po pobraniu i **nie wrócił** w kolejnych cyklach ✅
3. **oba zaległe pliki zdrenowane przy okazji**`dispatch/lustro/` całkowicie pusty ✅
4. spam `already processed — skipping` **ustał**: dwa pełne cykle (13:19:25, 13:20:28) z
zerem trafień ✅
5. akcja `completed` przez `action_result` po 70 s, `node-exporter` z nowym uptime ✅
6. zero ERROR/WARNING w executorze, observerze, supervisorze i node-agentach — poza znanym,
niezwiązanym crash-loopem watchtowera na LUSTRO ✅
`13:18:23 INFO - Restarted container 'node-exporter' for action
container-restart-lustro-node-exporter-dispatchfix`
**Czego ten test NIE dowiódł.** Druga połowa fixu — klasyfikacja rc=23 z WARNING-iem — na
LUSTRO nie pojechała: node-agent tam ma nadal `fee079e8` (ten sam sha, który SOLARIA miała
przed dzisiejszym deployem). `grep -c "rc=23"` = 0 w logach LUSTRO dlatego, że stary kod
fizycznie nie umie tego zalogować, a nie dlatego, że jest dobrze. Follow-up 15:25/#3.
### Stan tagów rollback
| Node | Tag | Image ID |
|---|---|---|
| SOLARIA | `node-agent:rollback-pre-dispatchfix` | `438111e2` |
| SOLARIA | `node-agent:rollback-pre-r1` (z 2026-08-05) | `c80a711f` |
| VPS | `node-agent:rollback-pre-dispatchfix` | `27be875d` |
| VPS | `control-plane-executor:rollback-pre-dispatchfix` | `b878d835` |
| VPS | `control-plane-observer:rollback-pre-dispatchfix` | `4b79511d` |
| VPS | `control-plane-supervisor:rollback-pre-dispatchfix` | `357b70d1` |
| VPS | `control-plane-operator-ui:rollback-pre-dispatchfix` | `e522bbaa` |
Pierwsze tagi `rollback-*` na VPS w ogóle. Otagowano wszystkie cztery obrazy control-plane,
nie tylko executora — `deploy-local.sh` przebudowuje cały stack, więc każdy potrzebuje celu
rollbacku.
### Follow-upy (sesja 15:25)
1. **`deploy-local.sh` robi rekurencyjny `chown` + `chmod` na całym `/opt/homelab`** — do
przeglądu, czy to w ogóle pożądane. Oba branche by dziś zadziałały: `sudo chown -R
1000:1000` (wyzwalacz: `events/solaria/evt-solaria-1785946768-node_health-node.json`) i
`sudo chmod -R 775` (wyzwalacze: `actions/dispatch/lustro`, `actions/deploy`). Ten drugi
ustawiłby `dispatch/lustro` na 775 z zupełnie innego powodu niż fix w executorze,
zacierając stan wejściowy testu, i nadałby 775 *plikom* w całym drzewie runtime (stąd
`-rwxrwxr-x` na heartbeatach i JSON-ach akcji). **Świadoma decyzja operatora: nie
naprawiać ręcznie.** Control-plane zdeployowano samym krokiem compose (`up -d --build
--force-recreate`) — bez sudo, więc rozjazdy chown/chmod zostały nietknięte i czekają na
przegląd. Dodatkowo `sudo` na VPS wymaga hasła, więc kanoniczna ścieżka i tak kończyłaby
się handoffem (exit 5).
2. **Lokalny control-plane na SOLARII** (executor/observer/supervisor/ui, `dispatch/solaria/`
z 2026-07-22) ma nadal executor z bugiem `0o755`. Nie jest dyspozytorem floty — log to
same `Starting executor loop` — ale to drugi, niezdeployowany egzemplarz tego samego
kodu. Osobna sesja.
3. **LUSTRO i PIHA bez dzisiejszego node-agenta.** LUSTRO: `fee079e8`. Bez fixu rc=23
nadal *połykają* nieudany unlink — dziś to nie boli, bo executor już nie tworzy złych
inboxów, ale każdy przyszły wyciek o innej przyczynie znowu będzie niewidoczny.
4. **Brak pytest w main checkoucie na SOLARII**`deploy.sh <target>` przewróciłby się na
gate (exit 2) bez `--no-gate`. Do naprawy, zanim ktoś sięgnie po kanoniczną ścieżkę
deployu z tej maszyny.
5. **Duplikat akcji testowej**`container-restart-lustro-node-exporter-dispatchfix.json`
leży jednocześnie w `completed/` (właściwy przebieg) i w `cancelled/` (kopia z
auto-anulowania sprzed approvalu). Pozostawione bez zmian — kosmetyka, żaden zapis
produkcyjny nie był potrzebny.
### Narrative
> _user-provided summary_

View file

@ -1,44 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-26
links: []
---
# Sesja 2026-08-26 — incydent kb-mail-sync: 20 dni ciszy, wykryte i naprawione
## Timeline
- **Wykrycie** — przy sanity checku po 3 tygodniach bez nadzoru (kontynuacja sesji 16:00,
patrz `docs/sessions/2026-08-26.md`): `mail-imap-sync` nie zapisał żadnej koperty od
2026-08-06 18:01 (pierwszy i jedyny udany tick automatu) mimo że timer tykał godzinowo
przez 20 dni. ~548 maili zaległości.
- **Diagnoza**`PermissionError` na `save_eml`: katalogi archiwum `2026/08` powstały
`root:root` z ręcznych `sudo`-runów 06.08, timer działa jako `oskar`. Kursor nie
przeskakiwał niezapisanej wiadomości (zgodnie z projektem) — stąd cisza bez utraty poczty,
ale też bez sygnału.
- **Naprawa zapisu**`chown -R oskar:oskar` na archiwum + dwa ręczne ticki: run#1
170 inserted/379 perm-errors (zaległości sprzed chowna), run#2 378 inserted/155
archive_exists/0 errors — pełne odzyskanie.
- **Reload Prometheusa** — przez CC z SOLARII (SSH na VPS): `promtool check rules` OK,
`POST /-/reload`, weryfikacja `/api/v1/rules` (wszystkie trzy grupy żywe), metryka
scrape'owana i świeża, `/api/v1/alerts` czyste, `brain-watchdog`→Telegram wpięty
poprawnie (`PROMETHEUS_URL` OK, kontener healthy).
Pełny opis root cause (oba, niezależne) i rekomendacje R1R3:
[kb/incidents/2026-08-26-mail-sync-20-dni-ciszy.md](../../kb/incidents/2026-08-26-mail-sync-20-dni-ciszy.md).
## Stan
Przyrostówka **znów żywa** i **po raz pierwszy faktycznie monitorowana** — reguła
`KbMailSyncStale` istniała w repo od 06.08, ale dopiero teraz jest realnie załadowana w
Prometheusie. Wcześniej alert nie mógł wystrzelić niezależnie od tego, jak długo trwałaby
awaria zapisu.
## Plan sprzed incydentu — przesunięty
Rotacja haseł (`kb-postgres` + teraz też `TG_TOKEN`, wyciekł w tej sesji przez `cat .env`
diagnostyczny — czwarty przypadek tej klasy) i recon Fazy 5 (wiki-kompilat) — obie pozycje
przesunięte na następną sesję, nie ruszone dzisiaj poza samą diagnozą.

View file

@ -1,207 +0,0 @@
## Session 16:00
Recon floty po 3 tygodniach bez nadzoru (ostatnia sesja 2026-08-06) + sesja
naprawcza: (A) usunięcie watchtowera z LUSTRO jako reliktu spoza GitOps,
(B) higiena kolejek `actions/pending` i zawieszonych incydentów `active` na VPS.
SUPERVISED — checkpointy A/B/C zatwierdzane przez operatora, backup przed
każdą operacją destrukcyjną.
### Commits
Ta sesja nie wprowadziła żadnych commitów w kodzie poza niniejszym logiem —
wyłącznie operacje na nodach (LUSTRO, VPS). Jeden niepowiązany commit
(`003f83d`, kb-publish skill) doszedł na `master` od równoległej sesji
operatora w międzyczasie — poza zakresem tej sesji.
```
(brak commitów tej sesji)
```
### Files changed
Brak zmian w repo.
### Deploys / operacje na nodach
**Recon (read-only, wszystkie 4 węzły: SOLARIA, PIHA, VPS, LUSTRO):**
- Zero nowych commitów na `origin/master` przez 3 tygodnie.
- Soak test R1 (auto-cleanup node-agenta): PIHA 20/20 uruchomień 0 usunięć,
VPS 21/21 uruchomień 0 usunięć, SOLARIA 3/3 0 usunięć, LUSTRO 5/5 —
1 usunięcie (`prune-disposable`, celowy kanarek testowy z 08-06, zgodnie
z zamysłem). Zero ofiar wśród kontenerów chronionych. `rc=23` nie wystąpił
ani razu — fix `0o775` z sesji 08-06 trzyma.
- Zdiagnozowano ciągłą pętlę restartów `pi-watchtower-1` na LUSTRO (API
Docker 1.25 vs wymagane min. 1.40), trwającą nieprzerwanie od co najmniej
2026-08-06 04:31.
**Watchtower LUSTRO — usunięcie (checkpoint A→B, zatwierdzony):**
- Backup: `docker inspect` + `compose.yml` + pusty katalog `/home/pi/watchtower`
`/home/pi/watchtower-removal-backup-2026-08-26/` na LUSTRO.
- `docker stop` + `docker rm pi-watchtower-1`, `docker rmi containrrr/watchtower:latest`,
`/home/pi/compose.yml` (jedyne źródło autostartu — brak systemd/cron) przeniesiony
do backupu jako `.disabled`.
- Weryfikacja: `docker ps -a` na LUSTRO czyste; 7 min ciszy zdarzeń
`containers_not_running-watchtower` na VPS (wymagane min. 5 min).
**Higiena kolejek VPS (checkpoint C→wykonanie, zatwierdzony):**
- Backup: `tar czf /opt/homelab/backups/actions-incidents-2026-08-26.tgz`
(`actions/` + `world/incidents.json`), sha256 `ef9afbfa...`.
- 17/18 pending → `cancelled/` (`stale_manual_cleanup`): 16× stare
`alert-node-*`/`alert-ha-*` z czerwca + 1× shadow-mode HA-websocket z 13.08
(kolizja nazwy pliku z niepowiązanym wpisem z 08-06 — zapisany pod nową
nazwą `container-restart-piha-homeassistant-shadowmode-20260813.json`,
żeby nie nadpisać cudzej historii).
- `redeploy-vps-gokapi` **pozostawiony** — realna luka wdrożeniowa (desired
w `hosts/vps/services.yaml`, brak kontenera), nie cruft. Follow-up do
sesji deploy.
- 5 incydentów w `world/incidents.json` ręcznie przełączonych na `resolved`
(`piha-homeassistant`, `solaria-narty27`, `solaria-prune-canary`,
`lustro-prune-canary`, `lustro-watchtower`) — wszystkie zdiagnozowane jako
trwale osierocone (brak mechanizmu auto-resolve dla zniknionej/przeniesionej
usługi, patrz Narrative).
- Sekwencja bez wyścigu: `docker stop control-plane-observer` → edycja pliku
`docker start` → weryfikacja >15 s (kilka cykli flush) — bo `_save_world()`
nadpisuje `world/*.json` co 5 s z pamięci procesu, bez merge z dyskiem.
- Efekt uboczny własnego restartu: `inc-...-vps-observer` (1 wystąpienie) —
rozwiązał się sam w ~60 s (poprawny, nieosierocony przypadek).
- Stan końcowy: `active_incidents_count: 0`, `runtime-summary.json status: nominal`.
### Narrative
> _user-provided summary_
## Session 23:00
Deploy control-plane (observer + supervisor) na VPS: stale-resolve 24h +
flagi `resolve-requests` (commit `71a7af5`), unikalny `action_id`
`container_restart` z bare-id fallbackiem (commit `91db682`), usunięcie
gokapi z desired state (commit `74ff3ee`) — zmerdowane do `master` jako
`f155999` przez operatora tuż przed sesją. SUPERVISED — checkpoint A po
weryfikacji deployu, checkpoint B po teście ścieżki flagi.
### Commits
```
(brak commitów kodu tej sesji — wyłącznie deploy + test na produkcji;
log sesji poniżej dopisany bez pusha)
```
### Files changed
Brak zmian w repo poza niniejszym logiem.
### Deploys / operacje na nodach
**KROK 0 — sanity:** `git pull` na `~/homelab-codex-ws` (SOLARIA, główny
checkout) — już aktualny na `03441a1` (na wierzchu mergu `f155999`).
`git status`/`diff HEAD` czyste. Wcześniej w tej samej sesji (przed
mergem) `git log -1` pokazywał `4fa10f0` — merge jeszcze nie istniał;
zatrzymano się i poczekano na operatora zamiast mergować samodzielnie
(worktree-aware: merge to wyłącznie krok człowieka).
**KROK 1 — deploy control-plane (checkpoint A, zatwierdzony):**
- Rollback tagi: `control-plane-{executor,observer,operator-ui,supervisor}
:rollback-pre-resolvefix` — ten sam wzorzec co `:rollback-pre-dispatchfix`
z 08-06.
- `git pull origin master` na VPS (`003f83d` → `03441a1`, fast-forward),
`docker compose up -d --build --force-recreate`.
- `deploy-local.sh`'s auto-chown krok padł: brak TTY dla hasła sudo,
`/opt/homelab/backups` i `/opt/homelab/events/solaria/*``oskar:oskar`
zamiast `aerbot:aerbot` (1000). Sprawdzone: `actions/`, `world/`,
`state/`, `config/` (realna ścieżka zapisu control-plane) już poprawnie
`aerbot:aerbot 775` — ominięto self-heal, `docker compose` odpalony
bezpośrednio bez sudo. Mismatch na `backups/`/`events/solaria/*`
pozostawiony nietknięty (follow-up niżej).
- Weryfikacja: 4/4 kontenery `healthy`, 0 linii error/traceback/exception
od restartu. sha256 `observer.py` (mount `/repo`, żywy) i `supervisor.py`
(wypieczony `/app/src`, wymaga `--build`) == repo HEAD, potwierdzone
osobno przez `docker exec` w obu kontenerach.
- Po 3 cyklach reconcile: `active_incidents: 0`, `world/resolve-requests/`
utworzony przez observera (pusty).
- `redeploy-vps-gokapi` (pending od 07-09) auto-cancelled po pierwszym
cyklu: `cancelled_reason: "service_removed_from_desired_state"` — bez
ponownego wygenerowania. `pending/` pozostał czysty (tylko niezwiązane
alerty HA z piha).
**KROK 2 — test ścieżki flagi na LUSTRO (checkpoint B, zatwierdzony):**
- `docker stop node-exporter` na LUSTRO → observer otworzył
`inc-1787777894-lustro-node-exporter`.
- Flaga: `touch world/resolve-requests/<id>` przez zwykłego SSH
usera **odrzucony permission denied** — katalog `755 aerbot:aerbot`,
brak zapisu grupowego mimo że `oskar` jest w grupie `aerbot`. Obejście:
`docker exec control-plane-observer touch ...` (proces w kontenerze
działa jako uid 1000 = właściciel katalogu). Follow-up niżej.
- Resolve w **1.01 s** od touch (limit ≤10s), `resolved_reason:
manual_operator`, flaga skasowana, `service.incident_id` wyczyszczony,
log INFO `"Manually resolving incident ... via resolve-request flag"`.
- Drift trwał dalej (kontener wciąż stopped) → supervisor wygenerował
`container-restart-lustro-node-exporter-1787777887` — **nowy format
id z COMMIT 2 potwierdzony na produkcji** — oraz równolegle
`redeploy-lustro-node-exporter` (bare id, ścieżka `unhealthy_service`
po wyczyszczeniu `incident_id`).
- Za decyzją operatora: `POST /action/mutate` na `127.0.0.1:18180`
(ten sam endpoint co UI/Telegram) — zatwierdzono restart, odrzucono
redeploy jako nadmiarowy.
- Executor zdispatchował realnie do LUSTRO; node-agent wykonał
`docker restart node-exporter` (log: `"Restarted container
'node-exporter' for action container-restart-lustro-node-exporter
-1787777887"`), akcja `completed`.
- Drift utrzymał się jeszcze chwilę po zatwierdzeniu → drugi, nowy
incydent (`inc-1787777955-...`) i druga, odrębna pending akcja
(`...-1787777950`, inny suffix `started_at`) — dokładnie oczekiwane
zachowanie "różny id przy nowym incydencie". Po powrocie zdrowia
auto-cancelled: `cancelled_reason: "drift_resolved_auto"`, bez
interwencji.
- Stan końcowy: `node-exporter` na LUSTRO `Up`, oba incydenty
`resolved`, `active_incidents: 0`, `pending/` czysty, 4/4 kontenery
control-plane nadal `healthy`, 0 error-ish linii w logach.
### Follow-upy
- **pytest env zepsuty na SOLARII**: `~/.local/bin/pytest` (brak
`_pytest`) i `homelab-codex-ws/.venv` (brak `pytest` w ogóle) oba
niedziałające; działa wyłącznie `/home/oskar/anaconda3/bin/pytest`
(7.4.4). Użyty do pełnego runu przed force-pushem poprawki COMMIT 2
(184 passed control-plane, 70 passed node-agent).
- **`world/resolve-requests/` permissions**: `755 aerbot:aerbot` zamiast
konwencji `775` używanej w `actions/`/`world/`/`state/`/`config/`.
Blokuje operatora SSH przed bezpośrednim `touch` flagi resolve —
manualna ścieżka z 71a7af5 ("operator drops a file") w praktyni wymaga
`docker exec`. Poprawić `chmod 775` / mode przy `os.makedirs` w
observer.py.
- **`backups/` i `events/solaria/*` ownership**: `oskar:oskar` zamiast
`aerbot:aerbot` — nie blokuje funkcjonalnie (czytelne dla "other"), ale
psuje self-heal chown w `deploy-local.sh` (próbuje rekurencyjnego sudo
chown całego `/opt/homelab` bez TTY/hasła). Do ręcznego wyczyszczenia
z hasłem sudo albo do zmiany self-heal na scoped (tylko katalogi
control-plane realnie potrzebuje) zamiast całego drzewa.
- **CLAUDE.md doc drift**: opisuje `events/YYYY-MM-DD/<node>/events.jsonl`,
rzeczywisty layout na VPS to płaskie `events/<node>/evt-*.json` (jeden
plik na zdarzenie, bez partycjonowania po dacie). Do poprawienia przy
najbliższej okazji.
- `redeploy-vps-gokapi` — zamknięty tym deployem (auto-cancelled), nie
wymaga już dalszego follow-upu z sesji 16:00.
### Narrative
> _user-provided summary_
## Session 23:06
### Commits
```
(brak commitów — sesja bez żadnej pracy poza natychmiastowym zamknięciem)
```
### Files changed
Brak zmian w repo.
### Deploys
None recorded
### Narrative
> _user-provided summary_

View file

@ -1,59 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-27
links:
- ../../kb/phases/kb-m5-faza5-wiki.md
- ../../kb/phases/kb-m5-faza3.md
- ../../kb/audits/wiki-kompilat-recon-2026-08-26.md
---
# Sesja 2026-08-27 — Faza 5: decyzje + Etap 1 + rekonсyliacja (ZAMKNIĘTY)
## Timeline
- **Sanity** — automat mailowy zdrowy po nocy (last_success świeży, gmail
max(ts) bieżący).
- **Formalizacja decyzji** (2df4f1d i wcześniejsze) — pakiet (a)-(h) audytu
`wiki-kompilat-recon-2026-08-26.md` ZATWIERDZONY w całości; inwariant 7
dopisany do faza3 §8.1 (izolacja retrievalu kompilacji od `source='wiki'`);
`check_okf.py` łapie bajty kontrolne (nowy check #12 + testy) — 4 NUL-e
usunięte z samego audytu.
- **Etap 1** (`task/wiki-etap1`, autonomiczny run CC ~25 min) — bootstrap +
3 strony proof (`sprawy/fll-2025-26`, `podmioty/mbank`,
`osoby/pawel-cesar-sanjuan-szklarz`). 40 sources + 47 przypisów
zweryfikowanych byte-for-byte, zero degradacji. Alias-resolution: 6 er
życia Pawła spójnie + 2 fałszywe pozytywy pod `paweld2.eu` wykryte i
udokumentowane jako wynik negatywny (kluczowy test mechanizmu PASS).
- **ODKRYCIE** — kb-wiki istniało na Forgejo od 2026-07-21 (5 stron: `pzu`,
`warta`, `ubezpieczenie-auto`, `fll-2025-26`, `wspólnota`) — proof
wykonany w fazie 3 (wątek "Kontynuacja wątku o KB", decyzja D7),
nieodnotowany w `kb/` ani w audycie. Recon 08-26 błędnie stwierdził
"remote nie istnieje" — sprawdził repo/pilota/bazę, nie odpytał Forgejo
bezpośrednio.
- **LEKCJA 1**: fakty wykonania muszą lądować w repo (lekcja 6 narty27 w
praktyce — zgubiliśmy całe repo na 5 tygodni).
- **LEKCJA 2**: recon zasobów zewnętrznych odpytuje źródło wprost, nie
wnioskuje z planów.
- **Rekonсyliacja** — porównanie dwóch niezależnych kompilacji `fll-2025-26`
(lipiec paperless-only vs sierpień paperless+gmail): komplementarne, zero
sprzeczności w faktach wspólnych; konwergencja metodologiczna (obie sesje
ten sam chunk 277/278, obie odmówiły potwierdzenia nieczytelnego OCR).
Scalenie: 21 envelope, 38 par sources, 45 przypisów re-zweryfikowanych;
naprawiony wadliwy lipcowy przypis; konwencja "Brak danych w KB"
sformalizowana. Potem mbank+paweł przeniesione do kanonicznego repo,
konwencje (d)-(f) scalone, walidator w repo. Stan końcowy kb-wiki@Forgejo
`a540a99`: 7 stron, lint 74/74 sources + 150/150 inline zielono.
- **kb-wiki remote** przepięty HTTPS→SSH (port 222).
- **Kandydat Etapu 2** wykryty SQL-em: `podmioty/future-minds` (organizator
FLL, 8+ dokumentów).
- **Follow-upy bez zmian**: rotacja sekretów (kb-postgres + `TG_TOKEN`
NADAL WISI), fix UID SEARCH, PDF-y, charset, R1/R2 z incydentu 26.08.
## Stan Fazy 5
- Etap 1 ✓ (2026-08-27). Next: Etap 2 — skala do 17+1 encji, lint w kodzie,
skan charset/NUL, decyzja o integracji `source='wiki'` w retrievalu
(inwariant 7 obowiązuje).

View file

@ -1,23 +0,0 @@
## Session 13:49
### Commits
a97cec0 merge: task/drobne-fixy (resolve-requests 775, prune out of health-monitor, events doc, redeploy action_id)
1dca438 fix(supervisor): apply started_at suffix to redeploy action_id too
40d78ce docs: fix events layout drift in CLAUDE.md
9a86843 fix(monitor): remove unfiltered docker container prune from health-monitor.sh
89f75c3 fix(observer): make world/resolve-requests/ group-writable
### Files changed
CLAUDE.md | 2 +-
scripts/monitor/health-monitor.sh | 110 +++------------------
scripts/observer/observer.py | 12 +++
services/control-plane/src/supervisor.py | 40 ++++----
.../control-plane/tests/test_incident_lifecycle.py | 17 ++++
.../tests/test_supervisor_action_id_uniqueness.py | 50 +++++++++-
6 files changed, 109 insertions(+), 122 deletions(-)
### Deploys
- control-plane → VPS: tagged 4/4 images `:rollback-pre-drobnefixy`, `git pull` (03441a1→a97cec0) + `docker compose up -d --build --force-recreate` (direct, no deploy-local.sh, per 26.08 precedent). Result: 4/4 healthy, zero error/traceback in logs since restart, sha256 of observer.py and supervisor.py match HEAD, `world/resolve-requests/` mode 775 confirmed, incidents.json stable (md5 unchanged) over 3 observer cycles. 30s permission test: flag `test-perms-123` dropped via plain `ssh` as `oskar` (no docker exec) was picked up and deleted in ~5s with `WARNING - Resolve-request flag for unknown incident test-perms-123 — removing flag` — 775 confirmed working in practice.
### Narrative
> _user-provided summary_

Some files were not shown because too many files have changed in this diff Show more