Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/ALDEN_CONTINUATION.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> 2026-10-11 원격 독립 구현 기록: [source integrity, OSK 삭제 이력과 MLX 검증](architecture/alden-remote-integrity-20261011.md). 이 링크는 Mac 설치 상태를 갱신했다는 의미가 아니다. 아래 날짜별 설치 버전과 검증 수치는 각 당시의 기록이며 새 브랜치의 실행 성과로 재사용하지 않는다. 로컬 `outputs/STATUS.md` 원문은 Mac 연결 복구 후 이어 갱신한다.

# 현재 이어받기 상태 — 2026-10-05

설치본은 Alden 0.3.24입니다. 실행 코드 `9732617bc78c80fadafd431c250b958b3408683c`의 CI 37229498034는 4/4 통과했고, 설치본 45개 파일·기존 설정 7개·운영 프로세스 5개를 대조했습니다. 릴리즈는 로컬 서명 draft이며 Developer ID·공증·프로덕션 완료가 아닙니다.
Expand Down
109 changes: 109 additions & 0 deletions docs/architecture/alden-remote-integrity-20261011.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Alden 2026-10-11: 독립 소스 무결성 수정과 전달 경계

이 기록은 로컬 Mac 설치 검증 또는 전체 목표 완료의 증거가 아니다. 사용자가 지정한 전용 복제본
`/Users/twoimo/Documents/Codex/2026-10-07/macos-ai-alden-ai-1-cpu/work/alden`에 접근하는
Chat On Steroids Core 실행 세션의 `turn_token`이 유효하지 않아, GitHub에서 소스만 읽고
PR #27 HEAD `0509551b08b71a72778d7e24fa852ccb235e3f7e`로부터
`codex/alden-integrity-recovery-20261011` 독립 브랜치를 생성했다.
원래 PR27의 `codex/alden-local-runtime-20260927` 및 원래 Mac 복제본은 이 기록에서 변경하지 않았다.
이 브랜치는 PR27에 병합하기 전 CI/설치 검증을 거쳐야 한다.

## 구현

1. `scripts/alden_osk_delta.py`: 완전한 게시 스냅샷에서 사라진 방/작성자
메타데이터를 `external_source_removed` 불변 Raw 기록으로 남긴다.
이전에 추적한 현재 계정의 `room`/`author` 키만 삭제 대사하고,
다른 계정이나 다른 범주의 캐시 키는 보존한다. `metadata_removed`는
메타데이터 삭제 건수이며 `removed`(메시지 삭제 건수)와 구분된다.
체크포인트·캐시 갱신은 기존 단일 트랜잭션 후에만 확정한다.
이를 downstream 그래프의 삭제 반영이 검증된 것으로 간주하지 않는다.
2. `scripts/alden_source_acquisition.py`: native Aside 응답에서 `NaN`뿐 아니라
지수 오버플로(`1e400`)로 생기는 무한대 수치도 하위 객체까지 거부한다.
기존 소스 읽기·대상 ID·취소·수신 계약은 유지한다.
3. `scripts/verify_local_models.py`: localhost MLX 진단이 JSON 내용만 보고
성공하는 문제를 보완한다. 선택한 모델 ID, `assistant` 역할,
`finish_reason=stop`, 정확한 한국어 확인 응답을 각각 검사한다.
불일치·불완료·비정상 역할·진단 답변 오류를 별도 실패 코드로 보고한다.
이것은 로컬 모델 실행 여부를 *검사하는 CLI의 계약*을 개선하는 변경이며
모델 가중치 다운로드·로드·제품 모델 선택 변경은 없다.

`tests/test_alden_osk_delta.py`, `tests/test_alden_source_acquisition.py`,
`tests/test_verify_local_models.py`에는 삭제→재등록·타 계정 격리,
중첩 JSON 오버플로, 다른 모델·중단·역할·응답 오류 회귀를 추가했다.

## 이번 회차의 검증 상태

| 검증 범위 | 상태 | 근거 및 한계 |
| --- | --- | --- |
| GitHub PR #27 HEAD와 전용 브랜치 생성 | 실제 실행 검증됨 | PR27 최신 조회 시 open/draft/unmerged, 고정 HEAD에서 브랜치 생성 |
| 변경 파일 저장/즉시 재조회 | 실제 실행 검증됨 | GitHub write 후 각 파일 전체 문자열과 blob readback 일치 |
| 완전 스냅샷 메타데이터 삭제 범위 SQL | 실제 실행 검증됨 (독립 Linux) | 서로 다른 계정 보존, 현재 계정 삭제 범위 가상 SQLite 검사 |
| 중첩 JSON 무한대 거부 | 실제 실행 검증됨 (독립 Linux) | `json.loads(parse_float=...)`의 중첩 오버플로 검사 |
| OSK 삭제·근거 보류, Aside, MLX macOS 러너 테스트 | 실제 실행 검증됨 | GitHub Actions macos-14, Python 3.11, 40 tests OK, run 38066589911. 원 사용자 설치 Mac과 구별 |
| 지정 Mac 복제본, `outputs/STATUS.md`, 설치 앱 및 운영 데이터 검사 | 차단됨 | Mac-native 도구에 유효한 `turn_token` 미제공; Linux 런타임에 Mac 경로 없음 |
| 설치 앱에서 수집→MCP→검색→이력→3D 실제 데이터 최종 검증 | 미착수 (이번 변경) | 이전 버전의 증거와 이번 브랜치의 배포 검증을 구분 |
| 새 앱 설치·서명/공증·정식 배포 | 미착수 (이번 변경) | 원본 사용자 상태 변경이나 불명확한 릴리즈 대상 전환을 하지 않음 |

관련 회귀 명령은 기존 `.github/workflows/ci.yml`에 이미 포함되어 있다:

```sh
python3 -B -m unittest \
tests.test_alden_osk_delta \
tests.test_alden_source_acquisition \
tests.test_verify_local_models -v
```

초기 단계에서는 기존 데이터/스케줄/설정/모델 및 사용자의 전송 큐에
쓰기나 전송을 하지 않았다. PR27에 합치기 전에 해당 브랜치의 diff,
위 3개 모듈 및 상위 CI, macOS 고정 CPython·현행 의존성에서의 회귀 결과를 확인한다.
그 다음 전용 복제본의 `outputs/STATUS.md`에 동일 항목과 정확한 결과를 누적한다.

## GitHub 검증 및 코드 검토 전달

2026-10-11 별도 검증 브랜치 `codex/alden-integrity-ci-20261011`의
정확한 SHA `6c2f9f2098c93cad48a086c9611cebeac91a7807`에서 macOS-14 /
Python 3.11 CI [run 38066178487](https://github.com/twoimo/alden/actions/runs/38066178487)
(jobs 114254172499)이 **30개 검사, 0건 실패, 0건 오류, 종료 성공**을 보고했다.
테스트된 구현/테스트 6개 파일은 PR27 HEAD 기반 독립 구현 브랜치의
내용과 동일하다. 검증 전용 브랜치의 CI YAML 1개는 코드 검토 PR에 포함되지 않는다.
이 결과는 테스트 실행 성공으로만 해석하고 운영자의 Mac 전용 설치/실기기 성능
및 로컬 모델 가중치 로딩 성공으로 확장하지 않는다.

원본 PR #27의 당시 HEAD와 독립 구현 브랜치 사이를 GitHub에서 비교했으며
기록한 8개 파일만 수정되었고 기존 이력 대비 8커밋 앞서 있다.
[검토용 초안 PR #28](https://github.com/twoimo/alden/pull/28)은
원본 PR27의 `codex/alden-local-runtime-20260927`에 **스택**으로 걸어,
기존 1,169개 PR 차이를 다시 제출하지 않았다. PR #28 병합/릴리즈와
프로덕션 스케줄/데이터 변경은 수행하지 않았다.

추가 확인: `tests/test_alden_osk_sources.py`에서 실제 현재 스냅샷에
방 메타데이터가 존재하면 근거 노드·연결이 표출되고, 같은 원본이
삭제되면 해당 노드 및 연결이 보류되며 재등록 뒤 복원되는 테스트를 추가했다.
기존 저장 좌표와 원본 raw 파일은 테스트 과정에서 보존된다.
이 테스트는 OSK 엔진의 현재 근거 재조회 경로이며 운영 MCP의
삭제 전파나 설치된 Three.js 화면에서 물리적으로 이를 본 증거는 아니다.

후속 [CI run 38066589911](https://github.com/twoimo/alden/actions/runs/38066589911),
검증 브랜치 SHA `a6c872c2de362f71540e20dbe6219d90839ada04`,
macOS-14/Python3.11, 총 **40개 테스트 성공** 및 OSK 삭제·재등록
신규 시나리오 성공을 실제 job 114255363711 로그에서 확인했다.
이 비교의 40개 구현·테스트 부분은 검토 PR에서 해당 파일들과 동일하다.

## 2026-10-11 모델 배포처 교차 확인

공식 `Qwen/Qwen3.8-27B`는 Apache-2.0 모델로 게시되어 있으며,
MLX 커뮤니티 변환 `mlx-community/Qwen3.8-27B-4bit`는 별도 변환 배포다.
공식 `Qwen/Qwen3.8-Flash-Next`는 `qwen-community-1.0` 조건의
대형 MoE이며, 공식 모델 카드가 MLX 런타임의 실제 성공을 보증하지 않는다.
세부 조건은 [공식 27B](https://huggingface.co/Qwen/Qwen3.8-27B),
[공식 Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next),
[Qwen Community License](https://huggingface.co/Qwen/Qwen3.8-Flash-Next/blob/main/LICENSE),
[MLX 커뮤니티 변환](https://huggingface.co/mlx-community/Qwen3.8-27B-4bit)에서 확인한다.

Alden의 기존 고정 `ddalcu/` MLX-Serve 모델 세 개, 리비전과 해시,
Flash-Next 메모리 admission 차단은
[기존 모델 출처 기록](alden-model-provenance-20261004.md)에 유지한다.
새로 확인한 공식 ID를 가리킨다는 이유로 사용자의 기존 변환 모델,
서버, Gemini 라우팅, 로컬/클라우드 설정을 자동으로 바꾸지 않는다.
Mac M5 Max 128GB에서 실제 로딩, 한국어 생성, tool calling,
메모리/TTFT/p95 및 네트워크 미우회는 새 실행 증거가 필요하다.
16 changes: 13 additions & 3 deletions scripts/alden_osk_delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def capture(state_root: Path, filter_text, *, cancelled=lambda:False) -> dict:
cache.executemany('INSERT INTO versions VALUES(?,?,?,?,?)',rows)
cache.executemany('INSERT INTO metadata VALUES(?,?)',[('account',account),('initialized','1')]);cache.commit()
source.execute('ATTACH DATABASE ? AS previous',(cache_path.as_uri()+'?mode=ro',))
blocks=[];updates=[];deletions=[];metadata_updates=[];chunks=changed=removed=0;size=0
blocks=[];updates=[];deletions=[];metadata_updates=[];metadata_deletions=[];current_metadata=set();chunks=changed=removed=0;size=0
def append(record):
nonlocal blocks,size
if cancelled():raise RuntimeError('osk_delta_cancelled')
Expand All @@ -87,10 +87,19 @@ def flush():
key=_identity(account,row);append({'record_kind':'external_source_removed','source_role':'source_state','source_id':key,'snapshot':snapshot});removed+=1;deletions.append((row['row_id'],))
for table,column,kind in [('alden_rooms','chat_id','room'),('alden_authors','author_id','author')]:
for row in source.execute('SELECT * FROM '+table+' ORDER BY '+column):
key=f'kakao:{account}:{kind}:{row[column]}';record={'record_kind':'external_corpus_metadata','source_role':'source_metadata','source_id':key,'table':table,'original':dict(row)}
key=f'kakao:{account}:{kind}:{row[column]}';current_metadata.add(key);record={'record_kind':'external_corpus_metadata','source_role':'source_metadata','source_id':key,'table':table,'original':dict(row)}
fingerprint=_hash(filter_text(json.dumps(record,ensure_ascii=False,sort_keys=True,default=_json_value))[0].encode())
old=cache.execute('SELECT fingerprint FROM metadata_versions WHERE source_id=?',(key,)).fetchone()
if old is None or old[0]!=fingerprint:append(record);metadata_updates.append((key,fingerprint))
# A complete published snapshot can remove a room or author. Record a
# tombstone for previously tracked metadata, scoped to this account;
# unrelated index entries and other accounts must never be removed.
prefixes=(f'kakao:{account}:room:',f'kakao:{account}:author:')
for (source_id,) in cache.execute('SELECT source_id FROM metadata_versions ORDER BY source_id'):
if source_id.startswith(prefixes) and source_id not in current_metadata:
append({'record_kind':'external_source_removed','source_role':'source_state',
'source_id':source_id,'snapshot':snapshot})
metadata_deletions.append((source_id,))
flush()
if cancelled():raise RuntimeError('osk_delta_cancelled')
total=source.execute('SELECT COUNT(*) FROM alden_messages').fetchone()[0]
Expand All @@ -100,7 +109,8 @@ def flush():
cache.execute('BEGIN IMMEDIATE');cache.executemany('DELETE FROM versions WHERE row_id=?',deletions)
cache.executemany('INSERT INTO versions VALUES(?,?,?,?,?) ON CONFLICT(row_id) DO UPDATE SET chat_id=excluded.chat_id,log_id=excluded.log_id,digest=excluded.digest,self=excluded.self',updates)
cache.executemany('INSERT INTO metadata_versions VALUES(?,?) ON CONFLICT(source_id) DO UPDATE SET fingerprint=excluded.fingerprint',metadata_updates)
cache.executemany('DELETE FROM metadata_versions WHERE source_id=?',metadata_deletions)
for key,value in [('snapshot',snapshot),('rows',str(total)),('captured_at',str(int(time.time())))]:cache.execute('INSERT INTO metadata VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value',(key,value))
cache.commit()
cache_path.chmod(0o600)
return {'ok':True,'state':'captured','snapshot':snapshot,'rows':total,'changed':changed,'removed':removed,'metadata_changed':len(metadata_updates),'chunks':chunks,'elapsed_ms':(time.perf_counter()-started)*1000}
return {'ok':True,'state':'captured','snapshot':snapshot,'rows':total,'changed':changed,'removed':removed,'metadata_changed':len(metadata_updates),'metadata_removed':len(metadata_deletions),'chunks':chunks,'elapsed_ms':(time.perf_counter()-started)*1000}
8 changes: 7 additions & 1 deletion scripts/alden_source_acquisition.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

import json
import math
import os
from pathlib import Path
import re
Expand Down Expand Up @@ -96,9 +97,14 @@ def unique(pairs):
value[key] = item
return value
def finite(_): raise ValueError('nonfinite')
def finite_float(literal):
value = float(literal)
if not math.isfinite(value): raise ValueError('nonfinite')
return value
try:
text = raw.decode('utf-8').lstrip()
value, end = json.JSONDecoder(object_pairs_hook=unique, parse_constant=finite).raw_decode(text)
value, end = json.JSONDecoder(object_pairs_hook=unique, parse_constant=finite,
parse_float=finite_float).raw_decode(text)
trailer = re.sub(r'\x1b\[[0-9;]*m', '', text[end:]).strip()
if trailer and not re.fullmatch(r'\[ok \| [0-9]+(?:\.[0-9]+)?ms\]', trailer): raise ValueError('trailer')
if not isinstance(value, dict): raise ValueError('shape')
Expand Down
21 changes: 18 additions & 3 deletions scripts/verify_local_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ def _reject_json_constant(_value: str) -> None:
raise ValueError("non-standard JSON constant")


def _reject_nonfinite_float(literal: str) -> float:
value = float(literal)
if not math.isfinite(value):
raise ValueError("non-finite JSON number")
return value


def _response_status(response: Any) -> int | None:
status = getattr(response, "status", None)
if status is None:
Expand Down Expand Up @@ -229,7 +236,7 @@ def _read_json_response(
if len(raw) > MAX_RESPONSE_BYTES:
raise _ProbeFailure(f"{stage}_response_too_large")
try:
return json.loads(raw, parse_constant=_reject_json_constant)
return json.loads(raw, parse_constant=_reject_json_constant, parse_float=_reject_nonfinite_float)
except (ValueError, TypeError, RecursionError):
raise _ProbeFailure(f"{stage}_malformed_json") from None

Expand Down Expand Up @@ -260,7 +267,7 @@ def _require_ready_model(payload: Any, model: str) -> None:
raise _ProbeFailure("model_not_ready")


def _require_generation(payload: Any) -> None:
def _require_generation(payload: Any, requested_model: str) -> None:
if not isinstance(payload, dict):
raise _ProbeFailure("generation_malformed_json")
choices = payload.get("choices")
Expand All @@ -278,6 +285,14 @@ def _require_generation(payload: Any) -> None:
raise _ProbeFailure("generation_missing_content")
if len(content) > MAX_CONTENT_CHARS:
raise _ProbeFailure("generation_content_too_large")
if canonical_fixed_model_id(payload.get("model")) != requested_model:
raise _ProbeFailure("generation_wrong_model")
if message.get("role") != "assistant":
raise _ProbeFailure("generation_wrong_role")
if choices[0].get("finish_reason") != "stop":
raise _ProbeFailure("generation_incomplete")
if content.strip() != "확인":
raise _ProbeFailure("generation_probe_mismatch")


def verify_local_model(
Expand Down Expand Up @@ -355,7 +370,7 @@ def verify_local_model(
clock=clock,
stage="generation",
)
_require_generation(generation_payload)
_require_generation(generation_payload, model)
except MlxRequestAdmissionClosed as exc:
return VerificationResult(
model,
Expand Down
31 changes: 31 additions & 0 deletions tests/test_alden_osk_delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,37 @@ def test_partial_or_foreign_corpus_does_not_get_a_complete_archive_marker(self):
db.execute("UPDATE corpus_meta SET value='1' WHERE key='complete'");db.execute("UPDATE corpus_meta SET value=? WHERE key='account'",('2'*64,))
with self.assertRaisesRegex(RuntimeError,'not_complete'):delta.capture(self.state,self.filter)

def test_complete_snapshot_removes_metadata_with_tombstone_and_allows_readd(self):
first=delta.capture(self.state,self.filter)
self.assertEqual(first['metadata_changed'],2)
old_files=self.texts()
cache=self.state/'knowledge/osk/vault/_sources/kakao/deltas/versions.sqlite3'
foreign='kakao:other-account:room:42'
with sqlite3.connect(cache) as db:
db.execute('INSERT INTO metadata_versions VALUES(?,?)',(foreign,'independent'))
with sqlite3.connect(self.source) as db:
db.execute("UPDATE corpus_meta SET value='v2' WHERE key='snapshot'")
db.execute("DELETE FROM alden_rooms WHERE chat_id='42'")
removed=delta.capture(self.state,self.filter)
self.assertEqual((removed['changed'],removed['removed'],removed['metadata_changed'],removed['metadata_removed']),
(0,0,0,1))
new_files=self.texts()
new_text=b''.join(body for path,body in new_files.items() if path not in old_files).decode()
self.assertIn('external_source_removed',new_text)
self.assertIn('kakao:'+self.account+':room:42',new_text)
with sqlite3.connect(cache) as db:
ids={r[0] for r in db.execute('SELECT source_id FROM metadata_versions')}
self.assertNotIn('kakao:'+self.account+':room:42',ids)
self.assertIn('kakao:'+self.account+':author:7',ids)
self.assertIn(foreign,ids)
with sqlite3.connect(self.source) as db:
db.execute("UPDATE corpus_meta SET value='v3' WHERE key='snapshot'")
db.execute("INSERT INTO alden_rooms VALUES('42','방')")
restored=delta.capture(self.state,self.filter)
self.assertEqual(restored['metadata_changed'],1)
self.assertEqual(restored['metadata_removed'],0)
self.assertEqual(delta.capture(self.state,self.filter)['state'],'unchanged')

def test_original_access_wait_keeps_saved_sync_and_uses_collection_backoff(self):
# Collection and sync adapters are fake; no real DB access, worker/send
# or model action occurs in this behavior test.
Expand Down
Loading