feat: add deterministic audiovisual pet world and TUI Watch

Consolidate the canonical pet telemetry, world, score, replay and checkpoint
sources with portable web, Rust, Swift and Kotlin hosts. Embed the same core
in foreground Watch, preserve unknown coverage and native checkpoint files,
and supply reproducible build and QA instructions.

The whale is the home form of a field of dots. Richer work-driven forms are
follow-up work, not a claim that this foundation completes the product.

Validation:
- Fresh source archive: pet typecheck/build, 33 tests passed, 0 failed.
- Fresh source archive npm test: 66 package + 12 SDK + 446 web passed, 0 failed.
- npm run check:web: passed production build with public GitHub release fetch.
- TS/Rust/Swift: 190 matching checkpoints; standalone ratatui build passed.
- Packaged Apple checkpoint/storage/host workflows: 8 passed, 0 failed.
- Legacy local Whalesong consumer check: 306 passed, 0 failed.

Full Rust suite, hosted CI, authenticated live attachment and release
qualification remain separate, unclaimed evidence. See pet/QA.md.

Refs #6109
This commit is contained in:
CodeWhale Bot committed 2026-09-12 06:06:01 -07:00
1 parent 94f0ae50d6
commit 5450b09ff3
131 files changed
+22834 -170

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
name: Pet conformance
on:
pull_request:
paths:
- 'pet/**'
- 'crates/tui/src/tui/pet_watch/**'
- 'crates/tui/src/tui/ambient_life/pet_*'
- 'crates/tui/src/tui/ambient_life/whale-points.tsv'
- 'npm/runtime-sdk/**'
- '.github/workflows/pet.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: pet-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
portable:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
cache-dependency-path: pet/package-lock.json
- uses: dtolnay/rust-toolchain@stable
- run: npm --prefix pet ci --ignore-scripts
- run: npm --prefix pet run check
- run: npm --prefix pet run sync
- name: Embedded sources match canonical core
run: git diff --exit-code -- crates/tui/src/tui/pet_watch/pet-native.js pet/ios/Resources
- run: cargo fetch --locked --manifest-path pet/tui/Cargo.toml
- name: TypeScript and Rust parity, including reduced motion
run: ./pet/verify.sh --no-swift
+4
View File
@@ -166,3 +166,7 @@ CODEWHALE_0_9_0_CUTOVER.md
# computer-use plugin smoke receipts (per-run local evidence)
crates/tui/plugins/computer-use/receipts/
# Portable pet build entry points are source.
!pet/**/*.sh
!pet/verify.sh
Generated
+1
View File
@@ -1173,6 +1173,7 @@ dependencies = [
"reqwest 0.13.4",
"rio-vt",
"rmcp",
"rquickjs",
"rusqlite",
"rustls",
"rusty_alloc-api",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch ha restaurat l’hàbitat d’aquesta sessió. S’espera telemetria nova.",
"PetWatchStorageUnavailable": "Watch no ha pogut restaurar ni desar aquest hàbitat. S’han conservat els fitxers existents. Fes servir {command} per exportar aquesta execució.",
"PetWatchExported": "Reproducció de Watch desada: {path}",
"PetWatchExportFailed": "No s’ha pogut desar la reproducció de Watch. L’enregistrament continua a la memòria.",
"PetWatchExportQueued": "S’està desant la reproducció de Watch…",
"PetWatchExportUnavailable": "Obre Watch en una sessió desada o espera que acabi l’exportació actual.",
"PetWatchTitle": "OBSERVA",
"PetUnobserved": "sense observació",
"PetDozing": "endormiscat",
"PetWatchUnavailable": "La telemetria de la mascota està en pausa. Torna a obrir Observa per reintentar-ho.",
"SessionArchiveExported": "Sessió exportada",
"SessionArchiveSizes": "elements / mida sense comprimir / arxiu",
"SessionArchiveNoArtifacts": "No s’han trobat fitxers d’artefactes regulars en aquesta sessió.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch hat den Lebensraum dieser Sitzung wiederhergestellt. Warte auf neue Telemetrie.",
"PetWatchStorageUnavailable": "Watch konnte diesen Lebensraum nicht wiederherstellen oder speichern. Vorhandene Dateien wurden beibehalten. Mit {command} kannst du diesen Lauf exportieren.",
"PetWatchExported": "Watch-Aufzeichnung gespeichert: {path}",
"PetWatchExportFailed": "Die Watch-Aufzeichnung konnte nicht gespeichert werden. Sie bleibt im Arbeitsspeicher.",
"PetWatchExportQueued": "Watch-Aufzeichnung wird gespeichert…",
"PetWatchExportUnavailable": "Öffne Watch in einer gespeicherten Sitzung oder warte auf den laufenden Export.",
"PetWatchTitle": "BEOBACHTEN",
"PetUnobserved": "unbeobachtet",
"PetDozing": "dösend",
"PetWatchUnavailable": "Die Haustier-Telemetrie pausiert. Öffne Beobachten erneut, um es noch einmal zu versuchen.",
"SessionArchiveExported": "Sitzung exportiert",
"SessionArchiveSizes": "Einträge / unkomprimierte Größe / Archivgröße",
"SessionArchiveNoArtifacts": "Keine regulären Artefaktdateien für diese Sitzung gefunden.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch restored this session’s habitat. Waiting for fresh telemetry.",
"PetWatchStorageUnavailable": "Watch could not restore or save this habitat. Existing files were kept. Use {command} to export this run.",
"PetWatchExported": "Watch replay saved: {path}",
"PetWatchExportFailed": "Watch replay could not be saved. The recording remains in memory.",
"PetWatchExportQueued": "Saving Watch replay…",
"PetWatchExportUnavailable": "Open Watch in a saved session, or wait for the current export.",
"PetWatchTitle": "WATCH",
"PetUnobserved": "unobserved",
"PetDozing": "dozing",
"PetWatchUnavailable": "Pet telemetry paused. Reopen Watch to retry.",
"SessionArchiveExported": "Exported session",
"SessionArchiveSizes": "members / uncompressed / archive",
"SessionArchiveNoArtifacts": "No regular artifacts found for this session.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch restauró el hábitat de esta sesión. Esperando telemetría nueva.",
"PetWatchStorageUnavailable": "Watch no pudo restaurar ni guardar este hábitat. Se conservaron los archivos existentes. Usa {command} para exportar esta ejecución.",
"PetWatchExported": "Reproducción de Watch guardada: {path}",
"PetWatchExportFailed": "No se pudo guardar la reproducción de Watch. La grabación sigue en la memoria.",
"PetWatchExportQueued": "Guardando reproducción de Watch…",
"PetWatchExportUnavailable": "Abre Watch en una sesión guardada o espera a que termine la exportación actual.",
"PetWatchTitle": "OBSERVAR",
"PetUnobserved": "sin observación",
"PetDozing": "dormitando",
"PetWatchUnavailable": "La telemetría de la mascota está en pausa. Vuelve a abrir Observar para reintentar.",
"SessionArchiveExported": "Sesión exportada",
"SessionArchiveSizes": "elementos / tamaño sin comprimir / archivo",
"SessionArchiveNoArtifacts": "No se encontraron archivos de artefactos regulares en esta sesión.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch a restauré l’habitat de cette session. En attente de nouvelles données de télémétrie.",
"PetWatchStorageUnavailable": "Watch n’a pas pu restaurer ou enregistrer cet habitat. Les fichiers existants ont été conservés. Utilisez {command} pour exporter cette exécution.",
"PetWatchExported": "Enregistrement Watch sauvegardé : {path}",
"PetWatchExportFailed": "Impossible de sauvegarder l’enregistrement Watch. Il reste en mémoire.",
"PetWatchExportQueued": "Sauvegarde de l’enregistrement Watch…",
"PetWatchExportUnavailable": "Ouvrez Watch dans une session enregistrée ou attendez la fin de l’exportation en cours.",
"PetWatchTitle": "OBSERVER",
"PetUnobserved": "non observé",
"PetDozing": "assoupi",
"PetWatchUnavailable": "La télémétrie de la mascotte est en pause. Rouvrez Observer pour réessayer.",
"SessionArchiveExported": "Session exportée",
"SessionArchiveSizes": "éléments / taille non compressée / archive",
"SessionArchiveNoArtifacts": "Aucun fichier d’artefact ordinaire trouvé pour cette session.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch ने इस सत्र का आवास बहाल कर दिया है। नई टेलीमेट्री की प्रतीक्षा है।",
"PetWatchStorageUnavailable": "Watch इस आवास को बहाल या सहेज नहीं सका। मौजूदा फ़ाइलें सुरक्षित हैं। इस रन को निर्यात करने के लिए {command} इस्तेमाल करें।",
"PetWatchExported": "Watch रिकॉर्डिंग सहेजी गई: {path}",
"PetWatchExportFailed": "Watch रिकॉर्डिंग सहेजी नहीं जा सकी। रिकॉर्डिंग अभी मेमोरी में है।",
"PetWatchExportQueued": "Watch रिकॉर्डिंग सहेजी जा रही है…",
"PetWatchExportUnavailable": "सहेजे गए सत्र में Watch खोलें या मौजूदा निर्यात पूरा होने की प्रतीक्षा करें।",
"PetWatchTitle": "देखें",
"PetUnobserved": "अवलोकन नहीं हुआ",
"PetDozing": "ऊँघ रहा है",
"PetWatchUnavailable": "पालतू की टेलीमेट्री रुकी हुई है। फिर से कोशिश करने के लिए देखें दोबारा खोलें।",
"SessionArchiveExported": "सत्र निर्यात किया गया",
"SessionArchiveSizes": "प्रविष्टियाँ / असंपीड़ित आकार / संग्रह का आकार",
"SessionArchiveNoArtifacts": "इस सत्र में कोई सामान्य आर्टिफ़ैक्ट फ़ाइल नहीं मिली।",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch memulihkan habitat sesi ini. Menunggu telemetri baru.",
"PetWatchStorageUnavailable": "Watch tidak dapat memulihkan atau menyimpan habitat ini. Berkas yang ada tetap disimpan. Gunakan {command} untuk mengekspor proses ini.",
"PetWatchExported": "Rekaman Watch disimpan: {path}",
"PetWatchExportFailed": "Rekaman Watch tidak dapat disimpan. Rekaman tetap ada di memori.",
"PetWatchExportQueued": "Menyimpan rekaman Watch…",
"PetWatchExportUnavailable": "Buka Watch dalam sesi tersimpan atau tunggu ekspor saat ini selesai.",
"PetWatchTitle": "AMATI",
"PetUnobserved": "belum teramati",
"PetDozing": "terlelap",
"PetWatchUnavailable": "Telemetri hewan peliharaan dijeda. Buka kembali Amati untuk mencoba lagi.",
"SessionArchiveExported": "Sesi diekspor",
"SessionArchiveSizes": "jumlah entri / ukuran tanpa kompresi / arsip",
"SessionArchiveNoArtifacts": "Tidak ditemukan berkas artefak biasa untuk sesi ini.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch がこのセッションの生息環境を復元しました。新しいテレメトリを待っています。",
"PetWatchStorageUnavailable": "Watch はこの生息環境を復元または保存できませんでした。既存のファイルは保持されています。{command} で今回の記録をエクスポートできます。",
"PetWatchExported": "Watch の記録を保存しました: {path}",
"PetWatchExportFailed": "Watch の記録を保存できませんでした。記録はメモリ内に残っています。",
"PetWatchExportQueued": "Watch の記録を保存中…",
"PetWatchExportUnavailable": "保存済みのセッションで Watch を開くか、現在のエクスポートが完了するまでお待ちください。",
"PetWatchTitle": "観察",
"PetUnobserved": "未観測",
"PetDozing": "うたた寝",
"PetWatchUnavailable": "ペットの計測が一時停止しました。観察を開き直すと再試行できます。",
"SessionArchiveExported": "セッションをエクスポートしました",
"SessionArchiveSizes": "項目数 / 非圧縮サイズ / アーカイブサイズ",
"SessionArchiveNoArtifacts": "このセッションに通常のアーティファクトファイルはありません。",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch가 이 세션의 서식지를 복원했습니다. 새 원격 측정 데이터를 기다립니다.",
"PetWatchStorageUnavailable": "Watch가 이 서식지를 복원하거나 저장하지 못했습니다. 기존 파일은 유지되었습니다. {command}로 이번 실행을 내보내세요.",
"PetWatchExported": "Watch 기록 저장됨: {path}",
"PetWatchExportFailed": "Watch 기록을 저장하지 못했습니다. 기록은 메모리에 남아 있습니다.",
"PetWatchExportQueued": "Watch 기록 저장 중…",
"PetWatchExportUnavailable": "저장된 세션에서 Watch를 열거나 현재 내보내기가 끝날 때까지 기다리세요.",
"PetWatchTitle": "관찰",
"PetUnobserved": "미관측",
"PetDozing": "졸고 있음",
"PetWatchUnavailable": "펫 원격 측정이 일시 중지되었습니다. 관찰을 다시 열어 재시도하세요.",
"SessionArchiveExported": "세션을 내보냈습니다",
"SessionArchiveSizes": "항목 수 / 압축 전 크기 / 아카이브 크기",
"SessionArchiveNoArtifacts": "이 세션에 일반 산출물 파일이 없습니다.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "O Watch restaurou o habitat desta sessão. Aguardando nova telemetria.",
"PetWatchStorageUnavailable": "O Watch não conseguiu restaurar ou salvar este habitat. Os arquivos existentes foram mantidos. Use {command} para exportar esta execução.",
"PetWatchExported": "Gravação do Watch salva: {path}",
"PetWatchExportFailed": "Não foi possível salvar a gravação do Watch. Ela permanece na memória.",
"PetWatchExportQueued": "Salvando gravação do Watch…",
"PetWatchExportUnavailable": "Abra o Watch em uma sessão salva ou aguarde a exportação atual.",
"PetWatchTitle": "OBSERVAR",
"PetUnobserved": "sem observação",
"PetDozing": "cochilando",
"PetWatchUnavailable": "A telemetria do pet foi pausada. Reabra Observar para tentar novamente.",
"SessionArchiveExported": "Sessão exportada",
"SessionArchiveSizes": "itens / tamanho sem compressão / arquivo",
"SessionArchiveNoArtifacts": "Nenhum artefato regular encontrado nesta sessão.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch восстановил среду этой сессии. Ожидание новой телеметрии.",
"PetWatchStorageUnavailable": "Watch не удалось восстановить или сохранить эту среду. Существующие файлы сохранены. Используйте {command}, чтобы экспортировать этот запуск.",
"PetWatchExported": "Запись Watch сохранена: {path}",
"PetWatchExportFailed": "Не удалось сохранить запись Watch. Она остаётся в памяти.",
"PetWatchExportQueued": "Сохранение записи Watch…",
"PetWatchExportUnavailable": "Откройте Watch в сохранённой сессии или дождитесь завершения текущего экспорта.",
"PetWatchTitle": "НАБЛЮДАТЬ",
"PetUnobserved": "нет наблюдений",
"PetDozing": "дремлет",
"PetWatchUnavailable": "Телеметрия питомца приостановлена. Снова откройте «Наблюдать», чтобы повторить попытку.",
"SessionArchiveExported": "Сеанс экспортирован",
"SessionArchiveSizes": "записи / размер без сжатия / размер архива",
"SessionArchiveNoArtifacts": "Обычные файлы артефактов для этого сеанса не найдены.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch відновив середовище цієї сесії. Очікування нової телеметрії.",
"PetWatchStorageUnavailable": "Watch не вдалося відновити або зберегти це середовище. Наявні файли збережено. Скористайтеся {command}, щоб експортувати цей запуск.",
"PetWatchExported": "Запис Watch збережено: {path}",
"PetWatchExportFailed": "Не вдалося зберегти запис Watch. Він залишається в пам’яті.",
"PetWatchExportQueued": "Збереження запису Watch…",
"PetWatchExportUnavailable": "Відкрийте Watch у збереженій сесії або дочекайтеся завершення поточного експорту.",
"PetWatchTitle": "СПОСТЕРІГАТИ",
"PetUnobserved": "немає спостережень",
"PetDozing": "дрімає",
"PetWatchUnavailable": "Телеметрію улюбленця призупинено. Знову відкрийте «Спостерігати», щоб повторити спробу.",
"SessionArchiveExported": "Сеанс експортовано",
"SessionArchiveSizes": "записи / розмір без стиснення / розмір архіву",
"SessionArchiveNoArtifacts": "Звичайних файлів артефактів для цього сеансу не знайдено.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch đã khôi phục môi trường của phiên này. Đang chờ dữ liệu đo mới.",
"PetWatchStorageUnavailable": "Watch không thể khôi phục hoặc lưu môi trường này. Các tệp hiện có được giữ nguyên. Dùng {command} để xuất lần chạy này.",
"PetWatchExported": "Đã lưu bản ghi Watch: {path}",
"PetWatchExportFailed": "Không thể lưu bản ghi Watch. Bản ghi vẫn còn trong bộ nhớ.",
"PetWatchExportQueued": "Đang lưu bản ghi Watch…",
"PetWatchExportUnavailable": "Mở Watch trong một phiên đã lưu hoặc đợi quá trình xuất hiện tại hoàn tất.",
"PetWatchTitle": "QUAN SÁT",
"PetUnobserved": "chưa quan sát",
"PetDozing": "ngủ gật",
"PetWatchUnavailable": "Đã tạm dừng dữ liệu thú cưng. Mở lại Quan sát để thử lại.",
"SessionArchiveExported": "Đã xuất phiên",
"SessionArchiveSizes": "số mục / dung lượng chưa nén / tệp lưu trữ",
"SessionArchiveNoArtifacts": "Không tìm thấy tệp sản phẩm thông thường trong phiên này.",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch 已恢复此会话的栖息地,正在等待新的遥测数据。",
"PetWatchStorageUnavailable": "Watch 无法恢复或保存此栖息地。现有文件已保留。使用 {command} 导出本次运行。",
"PetWatchExported": "Watch 回放已保存:{path}",
"PetWatchExportFailed": "无法保存 Watch 回放,记录仍保留在内存中。",
"PetWatchExportQueued": "正在保存 Watch 回放…",
"PetWatchExportUnavailable": "请在已保存的会话中打开 Watch,或等待当前导出完成。",
"PetWatchTitle": "观察",
"PetUnobserved": "未观测",
"PetDozing": "打盹",
"PetWatchUnavailable": "宠物遥测已暂停。重新打开观察以重试。",
"SessionArchiveExported": "会话已导出",
"SessionArchiveSizes": "条目数 / 未压缩大小 / 归档大小",
"SessionArchiveNoArtifacts": "此会话没有常规产物文件。",
+10
View File
@@ -1,4 +1,14 @@
{
"PetWatchRestored": "Watch 已還原此工作階段的棲息地,正在等待新的遙測資料。",
"PetWatchStorageUnavailable": "Watch 無法還原或儲存此棲息地。現有檔案已保留。使用 {command} 匯出本次執行。",
"PetWatchExported": "Watch 重播已儲存:{path}",
"PetWatchExportFailed": "無法儲存 Watch 重播,記錄仍保留在記憶體中。",
"PetWatchExportQueued": "正在儲存 Watch 重播…",
"PetWatchExportUnavailable": "請在已儲存的工作階段中開啟 Watch,或等待目前的匯出完成。",
"PetWatchTitle": "觀察",
"PetUnobserved": "未觀測",
"PetDozing": "打盹",
"PetWatchUnavailable": "寵物遙測已暫停。重新開啟觀察以重試。",
"SessionArchiveExported": "工作階段已匯出",
"SessionArchiveSizes": "項目數 / 未壓縮大小 / 封存大小",
"SessionArchiveNoArtifacts": "此工作階段沒有一般產物檔案。",
+20
View File
@@ -772,6 +772,16 @@ pub enum MessageId {
CmdStructcopyClipboardFailed,
CmdStructcopyReceiptTooLarge,
CmdFleetDescription,
PetWatchTitle,
PetUnobserved,
PetDozing,
PetWatchUnavailable,
PetWatchRestored,
PetWatchStorageUnavailable,
PetWatchExported,
PetWatchExportFailed,
PetWatchExportQueued,
PetWatchExportUnavailable,
CmdLaneDescription,
CmdWorkflowDescription,
CmdWorkflowsDescription,
@@ -3080,6 +3090,16 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CmdStructcopyClipboardFailed,
MessageId::CmdStructcopyReceiptTooLarge,
MessageId::CmdFleetDescription,
MessageId::PetWatchTitle,
MessageId::PetUnobserved,
MessageId::PetDozing,
MessageId::PetWatchUnavailable,
MessageId::PetWatchRestored,
MessageId::PetWatchStorageUnavailable,
MessageId::PetWatchExported,
MessageId::PetWatchExportFailed,
MessageId::PetWatchExportQueued,
MessageId::PetWatchExportUnavailable,
MessageId::CmdLaneDescription,
MessageId::CmdWorkflowDescription,
MessageId::CmdWorkflowsDescription,
+1
View File
@@ -72,6 +72,7 @@ ratatui = { version = "=0.30.2", features = ["unstable-rendered-line-info"] }
regex = "1.11"
reqwest = { workspace = true, features = ["blocking", "stream", "form", "http2"] }
rusqlite.workspace = true
rquickjs.workspace = true
rmcp = { version = "3.2.0", default-features = false, features = ["auth", "client"] }
rustls.workspace = true
qrcode = { version = "0.14", default-features = false }
+23 -8
View File
@@ -163,14 +163,7 @@ pub fn write_session_relative_immutable(
session_artifact_absolute_path(session_id, relative_path).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "invalid session artifact path")
})?;
let root = artifact_sessions_root()
.ok_or_else(|| io::Error::other("session artifact root unavailable"))?;
std::fs::create_dir_all(&root)?;
let destination = crate::fleet::files::WorkspaceFile::open(
&root,
&PathBuf::from(session_id).join(relative_path),
true,
)?;
let destination = open_session_relative(session_id, relative_path, true)?;
match destination.publish(content) {
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {
@@ -192,6 +185,28 @@ pub fn write_session_relative_immutable(
Ok(absolute_path)
}
/// The same confined session directory for mutable sidecars and immutable
/// artifacts. The saved-session owner remains responsible for session state.
pub(crate) fn open_session_relative(
session_id: &str,
relative_path: &Path,
create: bool,
) -> io::Result<crate::fleet::files::WorkspaceFile> {
session_artifact_absolute_path(session_id, relative_path).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "invalid session artifact path")
})?;
let root = artifact_sessions_root()
.ok_or_else(|| io::Error::other("session artifact root unavailable"))?;
if create {
std::fs::create_dir_all(&root)?;
}
crate::fleet::files::WorkspaceFile::open(
&root,
&PathBuf::from(session_id).join(relative_path),
create,
)
}
pub fn write_session_artifact_immutable(
session_id: &str,
artifact_id: &str,
@@ -613,8 +613,15 @@ pub fn screen(app: &mut App, target: ScreenMode, arg: Option<&str>) -> CommandRe
/// claim about a surface that cannot render.
pub fn sidebar(app: &mut App, arg: Option<&str>) -> CommandResult {
const USAGE: &str =
"Usage: /workbar [bottom|top|left|right|off|tasks|agents|context|pinned] [--save]";
"Usage: /workbar [bottom|top|left|right|off|tasks|agents|context|watch|pinned] [--save]";
let raw = arg.map(str::trim).unwrap_or("");
if raw.eq_ignore_ascii_case("watch export") {
return if app.pet_watch.export() {
CommandResult::message(tr(app.ui_locale, MessageId::PetWatchExportQueued))
} else {
CommandResult::error(tr(app.ui_locale, MessageId::PetWatchExportUnavailable))
};
}
let mut tokens = raw.split_whitespace().collect::<Vec<_>>();
let persist = matches!(tokens.last(), Some(&"--save" | &"-s"));
if persist {
@@ -654,6 +661,7 @@ pub fn sidebar(app: &mut App, arg: Option<&str>) -> CommandResult {
"context" | "session" => Some(crate::tui::work_surface::RailPanel::Context),
"git" | "branch" => Some(crate::tui::work_surface::RailPanel::Git),
"price" | "cost" => Some(crate::tui::work_surface::RailPanel::Price),
"watch" => Some(crate::tui::work_surface::RailPanel::Watch),
_ => None,
};
match (placement, panel) {
+1 -1
View File
@@ -76,7 +76,7 @@ static RAIL_INFO: CommandInfo = CommandInfo {
// /rail and /sidebar are the names users already know; both now drive
// the one workbar.
aliases: &["rail", "sidebar"],
usage: "/workbar [bottom|top|left|right|off|tasks|agents|context|pinned] [--save]",
usage: "/workbar [bottom|top|left|right|off|tasks|agents|context|watch|pinned] [--save]",
description_id: MessageId::CmdSidebarDescription,
};
static SETTINGS_INFO: CommandInfo = CommandInfo {
+4 -5
View File
@@ -13,11 +13,10 @@
//! host must supply — so it lands with the engine handle in Phase C/D, not
//! here.
//!
//! No runtime surface calls these projections yet: the first consumer is the
//! in-process engine handle that Phase D attaches the TUI and app-server to.
//! Until then the guard is the compile of this module itself, so dead-code
//! is allowed here on purpose rather than hidden behind a test cfg (which
//! would let `cargo build` pass with an unmapped variant).
//! The foreground pet observer consumes the event projection, retaining only
//! lifecycle metadata. Other projections remain compile-time parity guards;
//! dead-code is allowed here rather than hiding those guards behind a test cfg
//! (which would let `cargo build` pass with an unmapped variant).
#![allow(dead_code)]
use std::collections::BTreeMap;
+7 -7
View File
@@ -100,7 +100,7 @@ impl WorkspaceFile {
})
}
pub(super) fn open_update(&self, create: bool, append: bool) -> io::Result<File> {
pub(crate) fn open_update(&self, create: bool, append: bool) -> io::Result<File> {
self.open_with_flags(
libc::O_RDWR
| if create { libc::O_CREAT } else { 0 }
@@ -143,7 +143,7 @@ impl WorkspaceFile {
self.atomic_write(bytes, false)
}
pub(super) fn replace(&self, bytes: &[u8]) -> io::Result<()> {
pub(crate) fn replace(&self, bytes: &[u8]) -> io::Result<()> {
self.atomic_write(bytes, true)
}
@@ -283,7 +283,7 @@ impl WorkspaceFile {
})
}
pub(super) fn open_update(&self, create: bool, append: bool) -> io::Result<File> {
pub(crate) fn open_update(&self, create: bool, append: bool) -> io::Result<File> {
use std::os::windows::fs::OpenOptionsExt;
let file = std::fs::OpenOptions::new()
.read(true)
@@ -317,7 +317,7 @@ impl WorkspaceFile {
self.atomic_write(bytes, false)
}
pub(super) fn replace(&self, bytes: &[u8]) -> io::Result<()> {
pub(crate) fn replace(&self, bytes: &[u8]) -> io::Result<()> {
self.atomic_write(bytes, true)
}
@@ -511,10 +511,10 @@ impl WorkspaceFile {
pub(super) fn sibling(&self, _: &str) -> io::Result<Self> {
unreachable!()
}
pub(super) fn open_update(&self, _: bool, _: bool) -> io::Result<File> {
pub(crate) fn open_update(&self, _: bool, _: bool) -> io::Result<File> {
unreachable!()
}
pub(super) fn replace(&self, _: &[u8]) -> io::Result<()> {
pub(crate) fn replace(&self, _: &[u8]) -> io::Result<()> {
unreachable!()
}
pub(crate) fn open_file(&self) -> io::Result<File> {
@@ -527,7 +527,7 @@ impl WorkspaceFile {
/// Compare already opened lock handles; a replaced lock must never create two
/// independent critical sections for the same live ledger.
pub(super) fn same_file(left: &File, right: &File) -> io::Result<bool> {
pub(crate) fn same_file(left: &File, right: &File) -> io::Result<bool> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
+3
View File
@@ -149,6 +149,9 @@ mod tool_inspection;
mod tool_output_receipts;
mod tools;
mod tui;
/// Portable, dependency-free dot-whale core and conformance helpers.
/// The terminal and external renderers share this implementation.
pub use tui::ambient_life::pet_sim as pet;
mod turn_route_plan;
mod utils;
mod vision;
+2
View File
@@ -650,6 +650,7 @@ fn normalize_rail_panel(value: &str) -> &'static str {
"context" => "context",
"git" => "git",
"price" => "price",
"watch" => "watch",
// `pinned` folded into the tasks view (2026-09-02 dock views).
_ => "tasks",
}
@@ -1511,6 +1512,7 @@ impl Settings {
| "context"
| "git"
| "price"
| "watch"
| "pinned"
) {
anyhow::bail!(
+23 -111
View File
@@ -47,8 +47,8 @@
//! bounded by [`JELLY_MAX_TEXT_DODGE_COLS`].
//!
//! Under reduced motion there is no ambient life at all: `ocean::life_presence`
//! returns 0 and [`paint_marks`] returns before writing a cell. Marks are still
//! *built* (the budget counters stay honest), just never painted.
//! returns 0 and rendering exits before building marks or initializing pet
//! tapes. Reduced motion spends no simulation work on invisible creatures.
//!
//! `render_ambient_life` returns per-frame budget counters
//! ([`AmbientFrameStats`]): marks built always splits exactly into painted +
@@ -65,6 +65,13 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::tui::ocean::{self, OceanColumn};
#[path = "ambient_life/pet_cameo.rs"]
mod pet_cameo;
#[path = "ambient_life/pet_sim.rs"]
pub mod pet_sim;
#[path = "ambient_life/pet_widget.rs"]
pub mod pet_widget;
/// Depth layers for parallax. Nearer life is larger, faster, and more visible.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Depth {
@@ -140,16 +147,6 @@ impl LifeDensity {
pub const AMBIENT_MIN_WIDTH: u16 = crate::tui::ocean::AMBIENT_MIN_WIDTH;
pub const AMBIENT_MIN_HEIGHT: u16 = crate::tui::ocean::AMBIENT_MIN_HEIGHT;
/// Whale cameo state: brief breach → spout → fluke → submerge.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WhaleCameoPhase {
Hidden,
Breach,
Spout,
Fluke,
Submerge,
}
/// Snapshot of ambient positions for one frame (memoized once per draw).
#[derive(Debug, Clone)]
struct FrameMarks {
@@ -168,7 +165,7 @@ struct AmbientMark {
style_mod: Option<Modifier>,
/// Time-varying glow in `[0, 1]`: the mark's ink is lerped from the
/// painted water toward full ink at this amount. `None` renders the
/// plain ink (legacy behavior for the whale cameo).
/// plain habitat ink.
brightness: Option<f32>,
}
@@ -185,14 +182,11 @@ pub struct AmbientFrameStats {
pub cells_written: u32,
}
/// Hard upper bound on marks built in one frame: 7 fish + 1 jellyfish × 4
/// parts (2 dome rows + 2 tentacles) + 2 bubbles + 2 whale-cameo cells = 15,
/// plus headroom. This is a test-gate ceiling asserted against
/// [`AmbientFrameStats::marks_built`], not a runtime clamp: the population is
/// bounded by construction, and this constant is what fails the build if a
/// future change makes it unbounded.
/// Bounded school (7), jellyfish (4), bubbles (2), plus at most three
/// 18-by-6 dot-whale widgets including their labels. No particle allocations
/// or simulation steps occur per paint after the fixed cameo tapes are cached.
#[cfg(test)]
pub const MAX_FRAME_MARKS: u32 = 24;
pub const MAX_FRAME_MARKS: u32 = 13 + pet_cameo::MAX_MARKS;
/// Optional pointer reaction for fish dart / bubble rise.
#[derive(Debug, Clone, Copy, Default)]
@@ -262,20 +256,8 @@ impl AmbientActivity {
((elapsed_ms as f64) * f64::from(speed)) as u128
}
}
/// A sub-agent run surfaces as a pod: the completion cameo becomes three
/// whales at staggered offsets instead of one.
fn pod_cameo(self) -> bool {
self == Self::Subagents
}
fn pod_offsets(self) -> &'static [i16] {
if self.pod_cameo() { &[-5, 0, 5] } else { &[0] }
}
}
const WHALE_CAMEO_MS: u128 = 2_400;
/// Render ambient life into empty water cells of `area`.
///
/// Returns per-frame budget counters for tests and debug tooling; the
@@ -292,7 +274,11 @@ pub fn render_ambient_life(
whale: WhaleCameo,
activity: AmbientActivity,
) -> AmbientFrameStats {
if area.width < AMBIENT_MIN_WIDTH || area.height < AMBIENT_MIN_HEIGHT {
if area.width < AMBIENT_MIN_WIDTH
|| area.height < AMBIENT_MIN_HEIGHT
|| !presence.is_finite()
|| presence <= 0.0
{
return AmbientFrameStats::default();
}
@@ -306,10 +292,11 @@ pub fn render_ambient_life(
// Positions always ride the live monotonic clock; `presence` fades the
// marks in and out, so the animated/static boundary eases instead of
// snapping fish between t=0 and their mid-path positions.
let frame = build_frame_marks(
area, elapsed_ms, density, lines, cursor, whale, activity, &mut stats,
);
let frame = build_frame_marks(area, elapsed_ms, density, lines, cursor, &mut stats);
paint_marks(area, buf, inks, lines, &frame, presence, &mut stats);
pet_cameo::paint(
area, buf, inks.0, lines, presence, whale, activity, &mut stats,
);
stats
}
@@ -320,8 +307,6 @@ fn build_frame_marks(
density: LifeDensity,
lines: &[Line<'static>],
cursor: AmbientCursor,
whale: WhaleCameo,
activity: AmbientActivity,
stats: &mut AmbientFrameStats,
) -> FrameMarks {
let mut marks = Vec::with_capacity(48);
@@ -599,68 +584,6 @@ fn build_frame_marks(
});
}
// --- Rare whale cameo (completion only) ---
if let Some(cameo_ms) = whale.elapsed_ms.filter(|ms| *ms < WHALE_CAMEO_MS) {
for (pod_index, offset) in activity.pod_offsets().iter().enumerate() {
// Pod members ride the same cameo breath, staggered so a sub-agent
// completion reads as a group surfacing rather than one whale.
let pod_cameo_ms = cameo_ms.saturating_add((pod_index as u128) * 240);
if pod_cameo_ms >= WHALE_CAMEO_MS {
continue;
}
let phase = whale_cameo_phase(pod_cameo_ms);
if phase == WhaleCameoPhase::Hidden {
continue;
}
// Smooth continuous forward swimming drift across cameo
let cameo_frac = pod_cameo_ms as f64 / WHALE_CAMEO_MS as f64;
let drift = (cameo_frac * 3.0).round() as u16;
let ax = whale
.anchor_x
.saturating_add_signed(*offset)
.saturating_add(drift)
.saturating_sub(area.x)
.min(area.width.saturating_sub(4));
let ay = whale
.anchor_y
.saturating_sub(area.y)
.min(area.height.saturating_sub(2));
let (glyph, y_off) = match phase {
WhaleCameoPhase::Breach => ("≈≈>", 0u16),
WhaleCameoPhase::Spout => ("≈≈>", 0),
WhaleCameoPhase::Fluke => ("~", 1),
WhaleCameoPhase::Submerge => ("·", 1),
WhaleCameoPhase::Hidden => ("", 0),
};
let whale_glow = {
let s = (cameo_frac * std::f64::consts::PI).sin();
(0.65 + 0.35 * s) as f32
};
if !glyph.is_empty() {
marks.push(AmbientMark {
x: ax,
y: ay.saturating_add(y_off).min(area.height.saturating_sub(1)),
glyph,
jellyfish: None,
depth: Depth::Foreground,
style_mod: None,
brightness: Some(whale_glow),
});
if phase == WhaleCameoPhase::Spout && ay > 0 {
marks.push(AmbientMark {
x: ax.saturating_add(1).min(area.width.saturating_sub(1)),
y: ay.saturating_sub(1),
glyph: "˚",
jellyfish: None,
depth: Depth::Foreground,
style_mod: Some(Modifier::DIM),
brightness: Some(whale_glow),
});
}
}
}
}
stats.marks_built = marks.len() as u32;
FrameMarks { marks }
}
@@ -1161,17 +1084,6 @@ fn fish_body(facing_right: bool, lead: bool) -> &'static str {
}
}
#[must_use]
pub fn whale_cameo_phase(elapsed_ms: u128) -> WhaleCameoPhase {
match elapsed_ms {
0..400 => WhaleCameoPhase::Breach,
400..1_000 => WhaleCameoPhase::Spout,
1_000..1_700 => WhaleCameoPhase::Fluke,
1_700..WHALE_CAMEO_MS => WhaleCameoPhase::Submerge,
_ => WhaleCameoPhase::Hidden,
}
}
/// Subtle caustic shimmer applied to empty water cells when the field would
/// otherwise read as a static ramp. Cheap: one phase lookup per cell, only
/// when `animated` and density allows.
@@ -0,0 +1,131 @@
//! Replaces the `≈≈>` cameo with the shared dot-whale widget. The existing
//! ocean completion clock is the only trigger. This is an authored completion
//! gesture, not an event-v1 bucketer or an estimate of hidden agent activity.
use super::pet_sim::{ChannelId, PetSim, PetState};
use super::pet_widget::PetWidget;
use super::{AmbientActivity, AmbientFrameStats, WhaleCameo, is_open_water, ocean};
use ratatui::{buffer::Buffer, layout::Rect, style::Color, text::Line, widgets::Widget};
use std::sync::LazyLock;
const WIDTH: u16 = 18;
const HEIGHT: u16 = 6;
pub(super) const DURATION_MS: u128 = 2_400;
const FPS: u128 = 30;
const FRAMES: usize = (DURATION_MS * FPS / 1_000) as usize;
#[cfg(test)]
pub(super) const MAX_MARKS: u32 = 3 * WIDTH as u32 * HEIGHT as u32;
struct CameoFrame {
buffer: Buffer,
marks: u32,
}
// A bounded raster cache, not mutable simulation history. Sampling backwards,
// skipping draws, or rendering another session cannot change a frame. Two
// fixed 72-frame tapes; no RNG beyond the core's existing particle stream.
static SOLO: LazyLock<Vec<CameoFrame>> = LazyLock::new(|| frames(ChannelId::Other));
static POD: LazyLock<Vec<CameoFrame>> = LazyLock::new(|| frames(ChannelId::Agent));
fn frames(channel: ChannelId) -> Vec<CameoFrame> {
let mut sim = PetSim::whale();
// A completion is observed, but it does not assert a model/tool category.
// The pod has three persistent slots here; it is not three more swarms.
let state = PetState {
channel,
activity: 0.12,
coherence: 0.95,
..PetState::rest()
};
(0..FRAMES)
.map(|_| {
sim.step(1.0 / FPS as f64, &state, true, 1.0);
let mut buffer = Buffer::empty(Rect::new(0, 0, WIDTH, HEIGHT));
PetWidget {
sim: &sim,
state: &state,
}
.render(buffer.area, &mut buffer);
let marks = buffer.content.iter().filter(|c| c.symbol() != " ").count() as u32;
CameoFrame { buffer, marks }
})
.collect()
}
#[allow(clippy::too_many_arguments)]
pub(super) fn paint(
area: Rect,
buf: &mut Buffer,
ink: Color,
lines: &[Line<'_>],
presence: f32,
whale: WhaleCameo,
activity: AmbientActivity,
stats: &mut AmbientFrameStats,
) {
let Some(age) = whale.elapsed_ms.filter(|age| *age < DURATION_MS) else {
return;
};
if presence <= 0.0 {
return;
}
let pod = activity == AmbientActivity::Subagents;
let tape = if pod { &*POD } else { &*SOLO };
let slots: &[i32] = if pod { &[-1, 0, 1] } else { &[0] };
for (index, slot) in slots.iter().enumerate() {
let age = age + index as u128 * 240;
if age >= DURATION_MS {
continue;
}
let frame = &tape[(age * FPS / 1_000) as usize];
let progress = age as f64 / DURATION_MS as f64;
let x = i32::from(whale.anchor_x) - i32::from(area.x) - i32::from(WIDTH / 2)
+ slot * i32::from(WIDTH)
+ (progress * 3.0).round() as i32;
let y = i32::from(whale.anchor_y) - i32::from(area.y) - i32::from(HEIGHT / 2);
stats.marks_built += frame.marks;
// Do not clamp off-screen pod members into one overlapping creature.
if x < 0
|| y < 0
|| x + i32::from(WIDTH) > i32::from(area.width)
|| y + i32::from(HEIGHT) > i32::from(area.height)
{
stats.marks_clipped += frame.marks;
continue;
}
let (x, y) = (x as u16, y as u16);
let marks = || {
frame
.buffer
.content
.iter()
.enumerate()
.filter(|(_, c)| c.symbol() != " ")
};
// Atomically withhold body and label. A word touching one fluke must
// never leave a severed animal or an unsupported label in the water.
if marks()
.any(|(i, _)| !is_open_water(lines, x + i as u16 % WIDTH, y + i as u16 / WIDTH, 1))
{
stats.marks_skipped_text += frame.marks;
continue;
}
let glow = (0.65 + 0.35 * (progress * std::f64::consts::PI).sin()) as f32;
for (i, source) in marks() {
let point = (area.x + x + i as u16 % WIDTH, area.y + y + i as u16 / WIDTH);
let Some(cell) = buf.cell_mut(point) else {
stats.marks_clipped += 1;
continue;
};
// The existing theme owns all habitat ink. No category colour,
// especially Failure red, is introduced into completion chrome.
// Ambient ink is already calibrated against the ocean. Applying
// the standalone canvas alpha again would dim it twice.
let alpha = (glow * presence).clamp(0.0, 1.0);
let fg = ocean::mix_colors(cell.bg, ink, alpha);
cell.set_symbol(source.symbol()).set_fg(fg);
stats.marks_painted += 1;
stats.cells_written += 1;
}
}
}
+760
View File
@@ -0,0 +1,760 @@
//! pet_sim.rs — the Codewhale pet core, Rust port.
//!
//! A faithful, dependency-free port of PetSim.ts (which ports grammar.js's
//! consort engine). Same 980-point body from whale-points.tsv, same
//! mulberry32(0xC0FFEE) jitter, same gait field and spring integration, same
//! colour/hollow/brightness encoding. If a tape produces the same digest here
//! as in TypeScript or Swift, every surface drew the same whale.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(usize)]
pub enum ChannelId {
Reasoning,
Tool,
Memory,
Code,
Filesystem,
Network,
Browser,
Communication,
Agent,
Orchestration,
Error,
Human,
#[default]
Other,
}
impl ChannelId {
pub const fn as_str(self) -> &'static str {
match self {
Self::Reasoning => "reasoning",
Self::Tool => "tool",
Self::Memory => "memory",
Self::Code => "code",
Self::Filesystem => "filesystem",
Self::Network => "network",
Self::Browser => "browser",
Self::Communication => "communication",
Self::Agent => "agent",
Self::Orchestration => "orchestration",
Self::Error => "error",
Self::Human => "human",
Self::Other => "other",
}
}
}
impl std::fmt::Display for ChannelId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(usize)]
pub enum Archetype {
Gyre,
Strike,
Cross,
Pod,
Tear,
Address,
#[default]
Drift,
}
impl Archetype {
pub const fn as_str(self) -> &'static str {
match self {
Self::Gyre => "gyre",
Self::Strike => "strike",
Self::Cross => "cross",
Self::Pod => "pod",
Self::Tear => "tear",
Self::Address => "address",
Self::Drift => "drift",
}
}
}
impl std::fmt::Display for Archetype {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl ChannelId {
pub fn from_key(key: &str) -> Option<Self> {
CHANNELS
.iter()
.find(|c| c.key.as_str() == key)
.map(|c| c.key)
}
}
pub const N_CHANNELS: usize = 13;
#[derive(Clone, Copy, Default)]
pub struct PetState {
pub activity: f64,
pub coherence: f64,
pub attention: f64,
pub channel: ChannelId,
pub observed: f64,
pub roam_x: f64,
pub roam_y: f64,
pub flip: f64,
pub lit: f64,
}
impl PetState {
pub fn rest() -> Self {
PetState {
activity: 0.35,
coherence: 0.8,
attention: 0.0,
channel: ChannelId::Reasoning,
observed: 1.0,
roam_x: 0.0,
roam_y: 0.0,
flip: 1.0,
lit: 1.0,
}
}
}
#[derive(Clone, Copy)]
pub struct Channel {
pub key: ChannelId,
pub label: &'static str,
pub rgb: [u8; 3],
pub freq: f64,
pub sustained: bool,
pub arch: Archetype,
pub form: &'static str,
}
pub const CHANNELS: [Channel; N_CHANNELS] = [
Channel {
key: ChannelId::Reasoning,
label: "Model / reasoning",
rgb: [0x73, 0xc9, 0xb5],
freq: 130.81,
sustained: true,
arch: Archetype::Gyre,
form: "gyre · rolling",
},
Channel {
key: ChannelId::Tool,
label: "Tool calls",
rgb: [0x74, 0xaa, 0xdd],
freq: 261.63,
sustained: false,
arch: Archetype::Strike,
form: "strike · reaching",
},
Channel {
key: ChannelId::Memory,
label: "Memory / RAG",
rgb: [0xb6, 0xa7, 0x7f],
freq: 195.99,
sustained: true,
arch: Archetype::Gyre,
form: "gyre · scanning",
},
Channel {
key: ChannelId::Code,
label: "Code execution",
rgb: [0x9b, 0x9e, 0xd7],
freq: 164.81,
sustained: false,
arch: Archetype::Strike,
form: "strike · along the body",
},
Channel {
key: ChannelId::Filesystem,
label: "Filesystem",
rgb: [0x92, 0xb9, 0xc9],
freq: 440.00,
sustained: false,
arch: Archetype::Strike,
form: "strike · fanning",
},
Channel {
key: ChannelId::Network,
label: "Network / API",
rgb: [0xd3, 0xac, 0x74],
freq: 523.25,
sustained: false,
arch: Archetype::Cross,
form: "crossing · one way",
},
Channel {
key: ChannelId::Browser,
label: "Browser / computer",
rgb: [0x9e, 0xa9, 0xdf],
freq: 349.23,
sustained: false,
arch: Archetype::Cross,
form: "crossing · a sweep",
},
Channel {
key: ChannelId::Communication,
label: "Agent messages",
rgb: [0x83, 0xc5, 0xc9],
freq: 293.66,
sustained: false,
arch: Archetype::Cross,
form: "crossing · two ways",
},
Channel {
key: ChannelId::Agent,
label: "Subagent activity",
rgb: [0xb0, 0x9a, 0xcb],
freq: 220.00,
sustained: true,
arch: Archetype::Pod,
form: "pod · peers",
},
Channel {
key: ChannelId::Orchestration,
label: "Orchestration",
rgb: [0x6c, 0x87, 0x98],
freq: 98.00,
sustained: true,
arch: Archetype::Pod,
form: "pod · hub",
},
Channel {
key: ChannelId::Error,
label: "Errors / exceptions",
rgb: [0xe7, 0x91, 0x86],
freq: 185.00,
sustained: false,
arch: Archetype::Tear,
form: "torn · irregular",
},
Channel {
key: ChannelId::Human,
label: "Human interaction",
rgb: [0xc2, 0xb7, 0x87],
freq: 391.99,
sustained: false,
arch: Archetype::Address,
form: "it turns to face you",
},
Channel {
key: ChannelId::Other,
label: "Unclassified",
rgb: [0x73, 0x84, 0x92],
freq: 146.83,
sustained: false,
arch: Archetype::Drift,
form: "drifting · unformed",
},
];
const UNKNOWN_RGB: [f64; 3] = [0x73 as f64, 0x84 as f64, 0x92 as f64];
const REST_RGB: [f64; 3] = [122.0, 214.0, 240.0];
fn lerp(a: f64, b: f64, t: f64) -> f64 {
a + (b - a) * t
}
fn clamp(v: f64, a: f64, b: f64) -> f64 {
v.min(b).max(a)
}
/// mulberry32 — the same 32-bit sequence as every other port.
pub struct Mulberry32 {
a: u32,
}
impl Mulberry32 {
pub fn new(seed: u32) -> Self {
Mulberry32 { a: seed }
}
pub fn next(&mut self) -> f64 {
self.a = self.a.wrapping_add(0x6D2B79F5);
let mut t = self.a;
t = (t ^ (t >> 15)).wrapping_mul(t | 1);
t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
((t ^ (t >> 14)) as f64) / 4294967296.0
}
}
#[derive(Clone, Copy, Default)]
pub struct Particle {
pub x: f64,
pub y: f64,
pub vx: f64,
pub vy: f64,
pub s: f64,
pub jx: f64,
pub jy: f64,
pub pod: u32,
pub hx: f64,
pub hy: f64,
pub ang: f64,
pub rad: f64,
pub tail: f64,
pub tx: f64,
pub ty: f64,
}
#[derive(Clone, Copy, Default)]
pub struct Frame {
pub r: f64,
pub g: f64,
pub b: f64,
pub alpha: f64,
pub hollow: bool,
pub channel: ChannelId,
pub arch: Archetype,
pub work: f64,
}
/// The gait field — a velocity field on the body, not a silhouette.
/// Ported line-for-line from PetSim.ts gaitTarget().
fn gait_target(
q: &Particle,
t: f64,
act: f64,
coh: f64,
att: f64,
key: ChannelId,
work: f64,
) -> (f64, f64) {
let omega = lerp(4.6, 5.2 + act * 2.8, work);
let breath = 1.0 + (t * 1.85).sin() * lerp(0.048, 0.018, work);
let flex = (q.ang * 2.05 + t * omega).sin()
* lerp(0.042, 0.016 + act * 0.028, work)
* (0.18 + 0.82 * q.tail);
let mut px = (q.ang + flex).cos() * q.rad * breath;
let mut py = (q.ang + flex).sin() * q.rad * breath;
px += (t * 0.33).sin() * lerp(0.030, 0.014, work);
py += (t * 0.21).cos() * lerp(0.018, 0.010, work);
if work < 0.02 {
return (px, py);
}
let arch = CHANNELS[key as usize].arch;
let mut gx = px;
let mut gy = py;
match arch {
Archetype::Gyre => {
if key == ChannelId::Memory {
let pulse =
1.0 + (t * (2.4 + act * 1.6) - q.rad * 11.0).sin() * (0.15 + act * 0.10);
gx *= pulse;
gy *= pulse;
} else {
let roll = (t * (1.05 + act * 0.35)).sin() * (0.48 + act * 0.32);
let (c, sn) = (roll.cos(), roll.sin());
gx = px * c - py * sn * 0.88;
gy = px * sn * 0.88 + py * c;
}
}
Archetype::Strike => {
if key == ChannelId::Tool {
let rate = 2.7 + act * 2.1;
let lunge = (t * rate).sin().max(0.0).powi(2);
gx += lunge * 0.11;
if q.s > 0.60 {
let reach = (t * rate + q.pod as f64 * 0.92).sin().max(0.0).powi(4)
* (0.30 + act * 0.24);
gx += q.ang.cos() * reach;
gy += q.ang.sin() * reach;
}
} else if key == ChannelId::Code {
let rate = 3.2 + act * 1.8;
let wave = (t * rate - q.tail * 7.5).sin();
let bump = 0.11 + act * 0.08;
gx += q.ang.cos() * wave * bump;
gy += q.ang.sin() * wave * bump * 1.2;
gx += wave.max(0.0) * 0.07;
} else {
let rate = 2.15 + act * 1.5;
let side = (q.pod % 2) as f64 * 2.0 - 1.0;
let w = (t * rate + q.pod as f64 * 0.72).sin().max(0.0).powi(2);
gx += w * 0.055;
gy += side * w * (0.17 + act * 0.13);
}
}
Archetype::Cross => {
if key == ChannelId::Browser {
let band = ((t * (0.55 + act * 0.35)) % 1.0) * 1.28 - 0.64;
let in_band = (1.0 - (q.hy - band).abs() / 0.08).max(0.0);
gx += in_band * (0.24 + act * 0.10);
gy += in_band * 0.02;
} else {
let two = key == ChannelId::Communication;
let courier = q.s < if two { 0.44 } else { 0.32 };
if courier {
let dir = if two {
if q.s < 0.22 { 1.0 } else { -1.0 }
} else {
1.0
};
let u = (t * (0.38 + act * 0.36) + q.s * 5.2) % 1.0;
let going = if u < 0.5 { u * 2.0 } else { 2.0 - u * 2.0 };
let e = going * going * (3.0 - 2.0 * going);
gx = lerp(q.hx, dir * 0.80, e);
gy =
q.hy * (1.0 - e * 0.38) + (going * std::f64::consts::PI).sin() * 0.11 * dir;
}
}
}
Archetype::Pod => {
let n = 6u32;
let k = q.pod % n;
let hub = key == ChannelId::Orchestration && k == 0;
let spread = 0.30 + act * 0.11;
let orbit = t * (0.55 + act * 0.28);
if hub {
gx = px * 0.70;
gy = py * 0.70;
} else {
let slots = if key == ChannelId::Orchestration {
n - 1
} else {
n
};
let a = (if key == ChannelId::Orchestration {
k as f64 - 1.0
} else {
k as f64
}) * (std::f64::consts::PI * 2.0 / slots as f64)
+ orbit;
let sc = 0.34;
gx = q.hx * sc + a.cos() * spread * 1.28;
gy = q.hy * sc + a.sin() * spread * 0.80;
}
}
Archetype::Tear => {
let side = if q.hx + q.hy < 0.0 { -1.0 } else { 1.0 };
gx += side * (0.24 + (1.0 - coh) * 0.16);
gy += side * 0.15;
gx += (t * 11.4 + q.s * 40.0).sin() * (0.045 + act * 0.05);
gy += (t * 9.2 + q.s * 31.0).cos() * (0.040 + act * 0.045);
}
Archetype::Address => {
let face = 0.90 + att * 0.08;
let th: f64 = 0.70;
let z = (q.s - 0.5) * 0.42;
let mut ax = q.hx * th.cos() + z * th.sin();
let mut ay = q.hy;
let disc = 0.48 * face;
ax = lerp(ax, q.ang.cos() * (0.36_f64).min(q.rad + 0.06) * 0.95, disc);
ay = lerp(ay, q.ang.sin() * (0.36_f64).min(q.rad + 0.06) * 1.08, disc);
let grow = 1.20 + (t * 1.65).sin() * 0.055;
gx = ax * grow;
gy = ay * grow;
}
_ => {
let mill = 0.13 + (1.0 - coh) * 0.10;
gx = q.hx * 0.52 + (t * 0.72 + q.jx).sin() * mill;
gy = q.hy * 0.52 + (t * 0.54 + q.jy).cos() * mill;
}
}
(lerp(px, gx, work), lerp(py, gy, work))
}
fn still_t(key: ChannelId) -> f64 {
match key {
ChannelId::Reasoning => 1.15,
ChannelId::Memory => 0.42,
ChannelId::Tool => 0.30,
ChannelId::Code => 0.18,
ChannelId::Filesystem => 0.48,
ChannelId::Network => 0.72,
ChannelId::Browser => 0.95,
ChannelId::Communication => 0.58,
ChannelId::Agent => 1.25,
ChannelId::Orchestration => 0.85,
ChannelId::Error => 0.35,
ChannelId::Human => 0.05,
ChannelId::Other => 0.90,
}
}
pub struct PetSim {
pub p: Vec<Particle>,
phase: f64,
clock: f64,
tear: f64,
prev: ChannelId,
col: [f64; 3],
pub frame: Frame,
}
impl PetSim {
/// Authored body embedded once; all Rust surfaces use these same points.
pub fn whale() -> Self {
let points: Vec<(f64, f64)> = include_str!("whale-points.tsv")
.lines()
.map(|line| {
let (x, y) = line.split_once('\t').expect("baked whale point");
(x.parse().expect("baked x"), y.parse().expect("baked y"))
})
.collect();
Self::new(&points, 0xC0FFEE)
}
// These rounded constants are part of the cross-language tape contract.
#[allow(clippy::approx_constant)]
pub fn new(points: &[(f64, f64)], seed: u32) -> Self {
let mut rng = Mulberry32::new(seed);
let p = points
.iter()
.enumerate()
.map(|(i, &(hx, hy))| {
let mut q = Particle {
x: hx,
y: hy,
vx: 0.0,
vy: 0.0,
s: rng.next(),
jx: rng.next() * 6.283,
jy: rng.next() * 6.283,
pod: (i % 6) as u32,
hx,
hy,
..Default::default()
};
q.tx = hx;
q.ty = hy;
q.ang = hy.atan2(hx);
q.rad = hx.hypot(hy);
q.tail = clamp(((-hx - hy) * 0.5 + 0.22) / 0.62, 0.0, 1.0);
q
})
.collect();
let cur = ChannelId::Reasoning;
PetSim {
p,
phase: 0.0,
clock: 0.0,
tear: 0.0,
prev: cur,
col: REST_RGB,
frame: Frame {
r: REST_RGB[0],
g: REST_RGB[1],
b: REST_RGB[2],
alpha: 0.3,
hollow: false,
channel: ChannelId::Reasoning,
arch: Archetype::Gyre,
work: 0.0,
},
}
}
/// Advance the sim by dt seconds under `state`. Identical math to PetSim.ts.
pub fn step(&mut self, dt: f64, state: &PetState, motion: bool, sensitivity: f64) {
let s = |v: f64| lerp(0.5, v, sensitivity);
let act = s(state.activity);
let coh = s(state.coherence);
let att = s(state.attention);
let seen = s(state.observed);
let mot = if motion { 1.0 } else { 0.0 };
self.phase += dt * (0.18 + act * 0.55) * mot;
if motion {
self.clock += dt;
}
let shown = state.channel;
let ch = CHANNELS[shown as usize];
let work = clamp((act - 0.16) / 0.18, 0.0, 1.0);
let wander = lerp(0.32, 1.0, (1.0 - coh).powf(1.15));
if shown != self.prev {
if shown == ChannelId::Error {
self.tear = 1.0;
}
self.prev = shown;
}
self.tear = if motion {
(self.tear - dt * 1.6).max(0.0)
} else {
0.0
};
let split = (1.0 - coh).powf(1.6) * 0.16 + self.tear * 0.10;
let blur = (1.0 - coh).powf(1.45) * 0.22 + self.tear * 0.18;
let pull = if motion { 2.2 + coh * 5.2 } else { 18.0 };
let t_gait = if motion { self.clock } else { still_t(ch.key) };
for q in self.p.iter_mut() {
if motion {
q.jx += dt * (0.40 + act * 1.1);
q.jy += dt * (0.34 + act * 0.9);
}
let (gx, gy) = gait_target(q, t_gait, act, coh, att, ch.key, work);
let pod_ang = q.pod as f64 * 1.047 + self.phase * 0.22;
let tx = gx + (q.jx + q.s * 9.0).sin() * blur * wander + pod_ang.cos() * split;
let ty = gy + (q.jy + q.s * 7.0).cos() * blur * wander + pod_ang.sin() * split * 0.55;
q.tx = tx;
q.ty = ty;
if !motion {
q.x = tx;
q.y = ty;
q.vx = 0.0;
q.vy = 0.0;
continue;
}
q.vx += (tx - q.x) * pull * dt;
q.vy += (ty - q.y) * pull * dt;
q.vx *= 0.90;
q.vy *= 0.90;
let speed = if motion { 2.6 } else { 8.0 };
q.x += q.vx * dt * speed;
q.y += q.vy * dt * speed;
}
let want = if work > 0.35 {
[
CHANNELS[shown as usize].rgb[0] as f64,
CHANNELS[shown as usize].rgb[1] as f64,
CHANNELS[shown as usize].rgb[2] as f64,
]
} else {
REST_RGB
};
let k = if motion { (dt * 2.6).min(1.0) } else { 1.0 };
for c in 0..3 {
self.col[c] += (lerp(UNKNOWN_RGB[c], want[c], seen) - self.col[c]) * k;
}
let lit = clamp(state.lit, 0.0, 1.0);
let alpha = (0.22 + act * 0.10)
* lerp(0.50, 1.0, coh)
* lerp(0.55, 1.0, seen)
* lerp(0.35, 1.0, lit);
self.frame = Frame {
r: self.col[0],
g: self.col[1],
b: self.col[2],
alpha: (alpha * 1.85).min(0.92),
hollow: seen < 0.92,
channel: ch.key,
arch: ch.arch,
work,
};
}
}
/// Body-space → renderer-space, same as PetSim.ts layout().
pub struct Layout {
pub scale: f64,
pub flip_x: f64,
pub ox: f64,
pub oy: f64,
pub dot: f64,
}
pub fn layout(w: f64, h: f64, state: &PetState) -> Layout {
let att = state.attention;
let scale = (w * 0.52).min(h * 0.92) * (1.0 + att * 0.07);
Layout {
scale,
flip_x: state.flip,
ox: w / 2.0 + state.roam_x * w * 0.30,
oy: h / 2.0 + state.roam_y * h * 0.30 + h * att * 0.05,
dot: (1.6_f64).max(w.min(h) * 0.0092) * (1.0 + att * 0.18),
}
}
/// Conformance digest: quantize the field onto a 64×32 grid over
/// [-0.66, 0.66]², then FNV-1a the counts plus the frame encoding.
pub fn digest(sim: &PetSim) -> String {
const W: usize = 64;
const H: usize = 32;
let mut grid = [0u8; W * H];
for q in &sim.p {
let cx = ((q.x + 0.66) / 1.32 * W as f64).floor() as i32;
let cy = ((q.y + 0.66) / 1.32 * H as f64).floor() as i32;
if cx >= 0 && cx < W as i32 && cy >= 0 && cy < H as i32 {
let i = cy as usize * W + cx as usize;
grid[i] = grid[i].saturating_add(1).min(255);
}
}
let mut h: u64 = 0xcbf29ce484222325;
let mut mix = |b: u64| {
h ^= b & 0xff;
h = h.wrapping_mul(0x100000001b3);
};
for &v in &grid {
mix(v as u64);
}
mix(sim.frame.r.round() as u64);
mix(sim.frame.g.round() as u64);
mix(sim.frame.b.round() as u64);
mix((sim.frame.alpha * 255.0).round() as u64);
mix(if sim.frame.hollow { 1 } else { 0 });
format!("{:016x}", h)
}
// ---------------------------------------------------------------------------
// Braille raster — the terminal's renderer medium. Each cell is a 2×4 dot
// matrix (Unicode U+2800 + bits). Hollow frames stamp odd particles only:
// the body is still shown, visibly not asserted — the ASCII hollow.
pub const BRAILLE_BITS: [[u8; 2]; 4] = [[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80]];
/// Rasterize into a W×H grid of braille cells. Returns packed braille bits.
pub fn braille(sim: &PetSim, cells_w: usize, cells_h: usize, state: &PetState) -> Vec<u8> {
let lay = layout(cells_w as f64 * 2.0, cells_h as f64 * 4.0, state);
let mut grid = vec![0u8; cells_w * cells_h];
for (i, q) in sim.p.iter().enumerate() {
if sim.frame.hollow && i % 2 == 1 {
continue;
}
let dx = lay.ox + q.x * lay.scale * lay.flip_x;
let dy = lay.oy + q.y * lay.scale;
let dx = dx.round() as i32;
let dy = dy.round() as i32;
if dx < 0 || dy < 0 {
continue;
}
let (dcx, dcy) = (dx as usize / 2, dy as usize / 4);
if dcx >= cells_w || dcy >= cells_h {
continue;
}
grid[dcy * cells_w + dcx] |= BRAILLE_BITS[(dy as usize) % 4][(dx as usize) % 2];
}
grid
}
/// Text form of a braille grid — for snapshots and conformance output.
pub fn braille_text(grid: &[u8], cells_w: usize, cells_h: usize) -> String {
let mut out = String::new();
for y in 0..cells_h {
for x in 0..cells_w {
let b = grid[y * cells_w + x];
out.push(if b == 0 {
' '
} else {
char::from_u32(0x2800 + b as u32).unwrap_or(' ')
});
}
out.push('\n');
}
out
}
#[cfg(test)]
#[test]
fn constant_input_is_fully_still_in_every_channel() {
for channel in CHANNELS {
let mut sim = PetSim::whale();
let state = PetState {
channel: channel.key,
activity: 0.8,
coherence: 0.4,
..PetState::rest()
};
sim.step(1.0 / 30.0, &state, false, 1.0);
let before = digest(&sim);
for _ in 0..180 {
sim.step(1.0 / 30.0, &state, false, 1.0);
}
assert_eq!(before, digest(&sim), "{}", channel.key);
}
}
@@ -0,0 +1,81 @@
//! The shared braille renderer, promoted from the portable pet study's TUI.
//! It owns no clock, telemetry, or motion. The habitat owns placement and ink.
use super::pet_sim::{PetSim, PetState, braille};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Color, Style},
widgets::{Paragraph, Widget, Wrap},
};
use unicode_width::UnicodeWidthStr;
pub struct PetWidget<'a> {
pub sim: &'a PetSim,
pub state: &'a PetState,
}
impl Widget for PetWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let frame = self.sim.frame;
let label = format!(
"{} · {}{}",
frame.channel,
frame.arch,
if frame.hollow { " · unobserved" } else { "" }
);
// The creature and its non-colour cue are one unit. A narrow surface
// withholds both instead of silently dropping uncertainty or the gait.
if area.height < 4 || usize::from(area.width) < label.chars().count() {
return;
}
let tank = Rect {
height: area.height - 1,
..area
};
let grid = braille(
self.sim,
tank.width as usize,
tank.height as usize,
self.state,
);
let fg = Color::Rgb(
(frame.r * frame.alpha).clamp(0.0, 255.0) as u8,
(frame.g * frame.alpha).clamp(0.0, 255.0) as u8,
(frame.b * frame.alpha).clamp(0.0, 255.0) as u8,
);
render_grid(area, buf, &grid, &label, Style::default().fg(fg));
}
}
/// Shared paint path for the Rust cameo and the embedded world's live raster.
/// A tiny viewport keeps the complete text cue before spending cells on dots.
pub(crate) fn render_grid(area: Rect, buf: &mut Buffer, grid: &[u8], label: &str, style: Style) {
if area.width == 0 || area.height == 0 {
return;
}
if label.width() > usize::from(area.width) || area.height < 4 {
Paragraph::new(label)
.style(style)
.alignment(Alignment::Center)
.wrap(Wrap { trim: false })
.render(area, buf);
return;
}
for y in 0..area.height - 1 {
for x in 0..area.width {
let bits = grid
.get(usize::from(y) * usize::from(area.width) + usize::from(x))
.copied()
.unwrap_or(0);
if bits != 0
&& let Some(cell) = buf.cell_mut((area.x + x, area.y + y))
{
let glyph = char::from_u32(0x2800 + u32::from(bits)).expect("braille");
cell.set_symbol(&glyph.to_string()).set_style(style);
}
}
}
let x = area.x + (area.width - label.width() as u16) / 2;
buf.set_stringn(x, area.bottom() - 1, label, usize::from(area.width), style);
}
+180 -22
View File
@@ -1,6 +1,178 @@
use super::*;
use ratatui::text::Span;
fn dot_cameo(area: Rect, age: u128, activity: AmbientActivity) -> (Buffer, AmbientFrameStats) {
let mut buf = Buffer::empty(area);
let mut stats = AmbientFrameStats::default();
pet_cameo::paint(
area,
&mut buf,
Color::Cyan,
&[],
1.0,
WhaleCameo {
elapsed_ms: Some(age),
anchor_x: area.x + area.width / 2,
anchor_y: area.y + area.height / 2,
},
activity,
&mut stats,
);
(buf, stats)
}
#[test]
fn dot_cameo_is_bounded_and_independent_of_draw_history() {
let area = Rect::new(7, 11, 80, 24);
let (first, stats) = dot_cameo(area, 500, AmbientActivity::Baseline);
assert!(stats.marks_painted > 0);
dot_cameo(area, 1_900, AmbientActivity::Subagents);
dot_cameo(Rect::new(0, 0, 140, 40), 100, AmbientActivity::Baseline);
assert_eq!(first, dot_cameo(area, 500, AmbientActivity::Baseline).0);
assert_ne!(first, dot_cameo(area, 900, AmbientActivity::Baseline).0);
assert_eq!(
dot_cameo(area, 2_400, AmbientActivity::Baseline).1,
AmbientFrameStats::default()
);
assert_eq!(
dot_cameo(area, u128::MAX, AmbientActivity::Subagents).1,
AmbientFrameStats::default()
);
}
#[test]
fn dot_cameo_withholds_whole_body_when_label_touches_wide_text() {
let area = Rect::new(7, 11, 80, 24);
let (clear, clear_stats) = dot_cameo(area, 500, AmbientActivity::Baseline);
let label_cell = clear
.content
.iter()
.position(|cell| cell.symbol() == "o")
.unwrap();
let (x, y) = (
label_cell % area.width as usize,
label_cell / area.width as usize,
);
let mut lines = vec![Line::from(""); area.height as usize];
lines[y] = Line::from(format!("{}鲸", " ".repeat(x)));
let mut buf = Buffer::empty(area);
buf.set_line(area.x, area.y + y as u16, &lines[y], area.width);
let before = buf.clone();
let mut stats = AmbientFrameStats::default();
pet_cameo::paint(
area,
&mut buf,
Color::Cyan,
&lines,
1.0,
WhaleCameo {
elapsed_ms: Some(500),
anchor_x: area.x + 40,
anchor_y: area.y + 12,
},
AmbientActivity::Baseline,
&mut stats,
);
assert_eq!(
before, buf,
"a label collision must withhold every particle too"
);
assert_eq!(stats.marks_skipped_text, clear_stats.marks_built);
assert_eq!(stats.marks_painted, 0);
}
#[test]
fn narrow_pod_members_are_withheld_instead_of_stacked() {
let area = Rect::new(0, 0, 40, 12);
let (buf, stats) = dot_cameo(area, 500, AmbientActivity::Subagents);
let text: String = buf.content.iter().map(|cell| cell.symbol()).collect();
assert_eq!(text.matches("agent · pod").count(), 1);
assert!(stats.marks_clipped > 0);
assert_eq!(stats.marks_built, stats.marks_painted + stats.marks_clipped);
}
#[test]
fn reduced_motion_builds_and_paints_no_ambient_creatures() {
let area = Rect::new(0, 0, 80, 24);
let mut buf = Buffer::empty(area);
let before = buf.clone();
let stats = render_ambient_life(
area,
&mut buf,
(Color::Cyan, Color::Blue),
&[],
900,
0.0,
AmbientCursor::default(),
WhaleCameo {
elapsed_ms: Some(500),
anchor_x: 40,
anchor_y: 16,
},
AmbientActivity::Subagents,
);
assert_eq!(buf, before);
assert_eq!(stats, AmbientFrameStats::default());
}
#[test]
fn pet_settle_clock_does_not_extend_the_ocean_light_pulse() {
let area = Rect::new(0, 0, 80, 24);
let ramp = ocean::OceanRamp::for_theme(&codewhale_palette::UNDERWATER_UI_THEME).unwrap();
let column = |age| {
OceanColumn::new(
ramp,
area,
900,
age,
crate::tui::underwater::ShellPhase::Done,
false,
500,
40,
)
};
let settled = column(None);
let pet_tail = column(Some(900));
assert_eq!(pet_tail.completion_elapsed_ms(), Some(900));
assert_eq!(pet_tail.ramp_fingerprint(), settled.ramp_fingerprint());
for y in 0..area.height {
assert_eq!(pet_tail.color_at_y(y), settled.color_at_y(y));
}
}
#[test]
fn pet_widget_keeps_unknown_distinct_from_sleep_and_keeps_its_label() {
use pet_sim::{ChannelId, PetSim, PetState};
use ratatui::widgets::Widget;
let mut sim = PetSim::whale();
let mut state = PetState {
channel: ChannelId::Other,
lit: 0.3,
..PetState::rest()
};
sim.step(1.0 / 30.0, &state, false, 1.0);
assert!(!sim.frame.hollow, "sleep is still observed");
state.observed = 0.15;
sim.step(1.0 / 30.0, &state, false, 1.0);
assert!(sim.frame.hollow);
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
pet_widget::PetWidget {
sim: &sim,
state: &state,
}
.render(buf.area, &mut buf);
let text: String = buf.content.iter().map(|cell| cell.symbol()).collect();
assert!(text.contains("other · drift · unobserved"));
let mut narrow = Buffer::empty(Rect::new(0, 0, 18, 6));
let before = narrow.clone();
pet_widget::PetWidget {
sim: &sim,
state: &state,
}
.render(narrow.area, &mut narrow);
assert_eq!(narrow, before, "never truncate away the uncertainty cue");
}
#[test]
fn ambient_min_dimensions_allow_small_windows() {
const {
@@ -17,15 +189,6 @@ fn occupied_text_bounds_skips_string_join() {
assert!(end > start);
}
#[test]
fn whale_cameo_is_brief() {
assert_eq!(whale_cameo_phase(0), WhaleCameoPhase::Breach);
assert_eq!(whale_cameo_phase(500), WhaleCameoPhase::Spout);
assert_eq!(whale_cameo_phase(1_200), WhaleCameoPhase::Fluke);
assert_eq!(whale_cameo_phase(2_000), WhaleCameoPhase::Submerge);
assert_eq!(whale_cameo_phase(3_000), WhaleCameoPhase::Hidden);
}
#[test]
fn density_scales_with_area() {
assert_eq!(
@@ -57,8 +220,6 @@ fn frame_at(t: u128) -> FrameMarks {
LifeDensity::from_area(area),
&[],
AmbientCursor::default(),
WhaleCameo::default(),
AmbientActivity::Baseline,
&mut stats,
)
}
@@ -85,7 +246,11 @@ fn subagent_activity_surfaces_a_whale_pod() {
);
buf.content
.iter()
.filter(|cell| cell.symbol().contains('≈'))
.filter(|cell| {
cell.symbol()
.chars()
.any(|ch| ('\u{2801}'..='\u{28ff}').contains(&ch))
})
.map(|cell| cell.symbol().to_string())
.collect()
};
@@ -371,8 +536,6 @@ fn a_jellyfish_needs_water_deep_enough_to_hold_it_and_the_school() {
LifeDensity::from_area(area),
&lines,
AmbientCursor::default(),
WhaleCameo::default(),
AmbientActivity::Baseline,
&mut stats,
);
for mark in &frame.marks {
@@ -418,8 +581,6 @@ fn sparse_water_gets_a_compact_jellyfish() {
LifeDensity::from_area(area),
&[],
AmbientCursor::default(),
WhaleCameo::default(),
AmbientActivity::Baseline,
&mut stats,
);
for mark in &frame.marks {
@@ -473,8 +634,6 @@ fn motion_is_a_deterministic_function_of_elapsed_time() {
LifeDensity::from_area(area),
&[],
AmbientCursor::default(),
WhaleCameo::default(),
AmbientActivity::Baseline,
&mut stats,
)
};
@@ -562,9 +721,8 @@ fn frame_stats_account_for_every_mark() {
#[test]
fn frame_stats_stay_within_the_render_budget() {
// Worst case on the largest Rich field with the whale cameo active:
// 7 fish + 2 jellies x 5 parts + 2 bubbles + 2 cameo cells. Anything
// more is a leak in the O(1)-per-frame budget.
// Largest Rich field with three bounded dot-whale widgets. Every
// visible dot cell and label character participates in the same budget.
let area = Rect::new(0, 0, 160, 40);
let mut buf = Buffer::empty(area);
let whale = WhaleCameo {
@@ -581,7 +739,7 @@ fn frame_stats_stay_within_the_render_budget() {
1.0,
AmbientCursor::default(),
whale,
AmbientActivity::Baseline,
AmbientActivity::Subagents,
);
assert!(
stats.marks_built <= MAX_FRAME_MARKS,
@@ -0,0 +1,980 @@
-0.100000 -0.390000
-0.040000 -0.390000
-0.090000 -0.385000
-0.030000 -0.385000
-0.125000 -0.380000
-0.065000 -0.380000
0.000000 -0.380000
-0.135000 -0.375000
-0.075000 -0.375000
-0.010000 -0.375000
-0.185000 -0.370000
-0.120000 -0.370000
-0.055000 -0.370000
0.005000 -0.370000
-0.185000 -0.365000
-0.125000 -0.365000
-0.060000 -0.365000
0.000000 -0.365000
-0.210000 -0.360000
-0.145000 -0.360000
-0.085000 -0.360000
-0.020000 -0.360000
0.040000 -0.360000
-0.175000 -0.355000
-0.110000 -0.355000
-0.050000 -0.355000
0.015000 -0.355000
-0.215000 -0.350000
-0.150000 -0.350000
-0.085000 -0.350000
-0.025000 -0.350000
0.040000 -0.350000
-0.195000 -0.345000
-0.130000 -0.345000
-0.070000 -0.345000
-0.005000 -0.345000
-0.235000 -0.340000
-0.175000 -0.340000
-0.110000 -0.340000
-0.050000 -0.340000
0.015000 -0.340000
-0.220000 -0.335000
-0.160000 -0.335000
-0.095000 -0.335000
-0.035000 -0.335000
0.030000 -0.335000
-0.220000 -0.330000
-0.155000 -0.330000
-0.090000 -0.330000
-0.030000 -0.330000
0.035000 -0.330000
-0.215000 -0.325000
-0.150000 -0.325000
-0.085000 -0.325000
-0.025000 -0.325000
-0.270000 -0.320000
-0.210000 -0.320000
-0.145000 -0.320000
-0.080000 -0.320000
-0.020000 -0.320000
-0.265000 -0.315000
-0.205000 -0.315000
-0.140000 -0.315000
-0.080000 -0.315000
-0.015000 -0.315000
-0.260000 -0.310000
-0.200000 -0.310000
-0.135000 -0.310000
-0.075000 -0.310000
-0.010000 -0.310000
-0.250000 -0.305000
-0.190000 -0.305000
-0.125000 -0.305000
-0.065000 -0.305000
-0.305000 -0.300000
-0.240000 -0.300000
-0.180000 -0.300000
-0.115000 -0.300000
-0.055000 -0.300000
-0.285000 -0.295000
-0.225000 -0.295000
-0.160000 -0.295000
-0.095000 -0.295000
-0.035000 -0.295000
0.035000 -0.295000
-0.295000 -0.290000
-0.230000 -0.290000
-0.165000 -0.290000
-0.105000 -0.290000
-0.040000 -0.290000
0.020000 -0.290000
0.085000 -0.290000
-0.270000 -0.285000
-0.205000 -0.285000
-0.140000 -0.285000
-0.080000 -0.285000
-0.015000 -0.285000
0.045000 -0.285000
-0.320000 -0.280000
-0.255000 -0.280000
-0.195000 -0.280000
-0.130000 -0.280000
-0.070000 -0.280000
-0.005000 -0.280000
0.060000 -0.280000
-0.330000 -0.275000
-0.265000 -0.275000
-0.205000 -0.275000
-0.140000 -0.275000
-0.080000 -0.275000
-0.015000 -0.275000
0.050000 -0.275000
0.110000 -0.275000
-0.295000 -0.270000
-0.235000 -0.270000
-0.170000 -0.270000
-0.105000 -0.270000
-0.045000 -0.270000
0.020000 -0.270000
0.080000 -0.270000
0.145000 -0.270000
-0.290000 -0.265000
-0.225000 -0.265000
-0.160000 -0.265000
-0.100000 -0.265000
-0.035000 -0.265000
0.025000 -0.265000
0.090000 -0.265000
0.155000 -0.265000
-0.305000 -0.260000
-0.240000 -0.260000
-0.180000 -0.260000
-0.055000 -0.260000
0.010000 -0.260000
0.070000 -0.260000
0.135000 -0.260000
-0.340000 -0.255000
-0.275000 -0.255000
-0.215000 -0.255000
-0.150000 -0.255000
0.010000 -0.255000
0.070000 -0.255000
0.135000 -0.255000
-0.350000 -0.250000
-0.285000 -0.250000
-0.220000 -0.250000
-0.160000 -0.250000
0.030000 -0.250000
0.090000 -0.250000
0.155000 -0.250000
-0.325000 -0.245000
-0.265000 -0.245000
-0.200000 -0.245000
0.005000 -0.245000
0.070000 -0.245000
0.130000 -0.245000
-0.355000 -0.240000
-0.290000 -0.240000
-0.230000 -0.240000
0.000000 -0.240000
0.060000 -0.240000
0.125000 -0.240000
-0.360000 -0.235000
-0.300000 -0.235000
-0.235000 -0.235000
0.005000 -0.235000
0.070000 -0.235000
0.130000 -0.235000
-0.350000 -0.230000
-0.285000 -0.230000
-0.225000 -0.230000
0.035000 -0.230000
0.095000 -0.230000
0.160000 -0.230000
-0.320000 -0.225000
-0.260000 -0.225000
0.010000 -0.225000
0.070000 -0.225000
0.135000 -0.225000
-0.345000 -0.220000
-0.285000 -0.220000
-0.220000 -0.220000
0.060000 -0.220000
0.125000 -0.220000
-0.355000 -0.215000
-0.290000 -0.215000
-0.225000 -0.215000
0.065000 -0.215000
0.130000 -0.215000
-0.350000 -0.210000
-0.285000 -0.210000
0.025000 -0.210000
0.085000 -0.210000
0.150000 -0.210000
-0.330000 -0.205000
-0.265000 -0.205000
0.060000 -0.205000
0.120000 -0.205000
-0.345000 -0.200000
-0.285000 -0.200000
0.055000 -0.200000
0.115000 -0.200000
-0.345000 -0.195000
-0.280000 -0.195000
0.070000 -0.195000
-0.375000 -0.190000
-0.315000 -0.190000
-0.395000 -0.185000
-0.330000 -0.185000
-0.270000 -0.185000
-0.345000 -0.180000
-0.285000 -0.180000
-0.360000 -0.175000
-0.300000 -0.175000
-0.375000 -0.170000
-0.310000 -0.170000
-0.390000 -0.165000
-0.325000 -0.165000
-0.400000 -0.160000
-0.335000 -0.160000
-0.405000 -0.155000
-0.345000 -0.155000
-0.280000 -0.155000
-0.355000 -0.150000
-0.290000 -0.150000
-0.360000 -0.145000
-0.300000 -0.145000
-0.365000 -0.140000
-0.305000 -0.140000
-0.370000 -0.135000
-0.310000 -0.135000
-0.375000 -0.130000
-0.310000 -0.130000
-0.380000 -0.125000
-0.315000 -0.125000
-0.380000 -0.120000
-0.315000 -0.120000
-0.380000 -0.115000
-0.320000 -0.115000
-0.385000 -0.110000
-0.325000 -0.110000
-0.385000 -0.105000
-0.325000 -0.105000
-0.385000 -0.100000
-0.320000 -0.100000
-0.390000 -0.095000
-0.325000 -0.095000
-0.390000 -0.090000
-0.325000 -0.090000
-0.385000 -0.085000
-0.325000 -0.085000
-0.385000 -0.080000
-0.325000 -0.080000
-0.385000 -0.075000
-0.320000 -0.075000
-0.385000 -0.070000
-0.320000 -0.070000
-0.435000 -0.065000
-0.370000 -0.065000
0.255000 -0.065000
0.320000 -0.065000
0.385000 -0.065000
-0.390000 -0.060000
-0.325000 -0.060000
0.270000 -0.060000
0.335000 -0.060000
0.400000 -0.060000
-0.385000 -0.055000
-0.320000 -0.055000
0.250000 -0.055000
0.315000 -0.055000
0.380000 -0.055000
-0.415000 -0.050000
-0.350000 -0.050000
0.195000 -0.050000
0.260000 -0.050000
0.320000 -0.050000
0.385000 -0.050000
-0.410000 -0.045000
-0.350000 -0.045000
0.180000 -0.045000
0.240000 -0.045000
0.305000 -0.045000
0.370000 -0.045000
-0.440000 -0.040000
-0.375000 -0.040000
-0.315000 -0.040000
0.200000 -0.040000
0.260000 -0.040000
0.325000 -0.040000
0.390000 -0.040000
-0.420000 -0.035000
-0.355000 -0.035000
0.135000 -0.035000
0.200000 -0.035000
0.265000 -0.035000
0.325000 -0.035000
0.390000 -0.035000
-0.425000 -0.030000
-0.360000 -0.030000
0.120000 -0.030000
0.180000 -0.030000
0.245000 -0.030000
0.305000 -0.030000
0.370000 -0.030000
0.430000 -0.030000
-0.380000 -0.025000
-0.315000 -0.025000
0.145000 -0.025000
0.210000 -0.025000
0.270000 -0.025000
0.335000 -0.025000
0.400000 -0.025000
-0.415000 -0.020000
-0.350000 -0.020000
0.095000 -0.020000
0.160000 -0.020000
0.220000 -0.020000
0.285000 -0.020000
0.350000 -0.020000
0.410000 -0.020000
-0.400000 -0.015000
-0.340000 -0.015000
0.095000 -0.015000
0.160000 -0.015000
0.220000 -0.015000
0.285000 -0.015000
0.345000 -0.015000
0.410000 -0.015000
-0.405000 -0.010000
-0.345000 -0.010000
0.075000 -0.010000
0.135000 -0.010000
0.200000 -0.010000
0.260000 -0.010000
0.325000 -0.010000
0.390000 -0.010000
-0.430000 -0.005000
-0.365000 -0.005000
0.030000 -0.005000
0.095000 -0.005000
0.160000 -0.005000
0.220000 -0.005000
0.285000 -0.005000
0.345000 -0.005000
0.410000 -0.005000
-0.400000 0.000000
-0.340000 0.000000
0.045000 0.000000
0.105000 0.000000
0.170000 0.000000
0.230000 0.000000
0.295000 0.000000
0.360000 0.000000
0.420000 0.000000
-0.390000 0.005000
-0.330000 0.005000
0.045000 0.005000
0.110000 0.005000
0.170000 0.005000
0.235000 0.005000
0.295000 0.005000
0.360000 0.005000
0.420000 0.005000
-0.390000 0.010000
-0.325000 0.010000
0.025000 0.010000
0.090000 0.010000
0.150000 0.010000
0.215000 0.010000
0.280000 0.010000
0.340000 0.010000
0.405000 0.010000
-0.410000 0.015000
-0.345000 0.015000
-0.005000 0.015000
0.055000 0.015000
0.120000 0.015000
0.180000 0.015000
0.245000 0.015000
0.305000 0.015000
0.370000 0.015000
-0.440000 0.020000
-0.380000 0.020000
-0.315000 0.020000
0.005000 0.020000
0.070000 0.020000
0.135000 0.020000
0.195000 0.020000
0.260000 0.020000
0.320000 0.020000
0.385000 0.020000
-0.425000 0.025000
-0.365000 0.025000
-0.300000 0.025000
0.005000 0.025000
0.070000 0.025000
0.130000 0.025000
0.195000 0.025000
0.260000 0.025000
0.320000 0.025000
0.415000 0.025000
-0.400000 0.030000
-0.335000 0.030000
-0.040000 0.030000
0.020000 0.030000
0.085000 0.030000
0.145000 0.030000
0.210000 0.030000
0.270000 0.030000
0.335000 0.030000
-0.420000 0.035000
-0.360000 0.035000
-0.295000 0.035000
-0.025000 0.035000
0.040000 0.035000
0.105000 0.035000
0.165000 0.035000
0.230000 0.035000
0.290000 0.035000
0.430000 0.035000
-0.375000 0.040000
-0.315000 0.040000
-0.060000 0.040000
0.000000 0.040000
0.065000 0.040000
0.125000 0.040000
0.190000 0.040000
0.255000 0.040000
0.405000 0.040000
-0.400000 0.045000
-0.340000 0.045000
-0.100000 0.045000
-0.035000 0.045000
0.025000 0.045000
0.090000 0.045000
0.150000 0.045000
0.215000 0.045000
0.275000 0.045000
-0.430000 0.050000
-0.365000 0.050000
-0.305000 0.050000
-0.090000 0.050000
-0.030000 0.050000
0.035000 0.050000
0.100000 0.050000
0.160000 0.050000
0.225000 0.050000
0.285000 0.050000
-0.405000 0.055000
-0.340000 0.055000
-0.280000 0.055000
-0.085000 0.055000
-0.025000 0.055000
0.040000 0.055000
0.100000 0.055000
0.165000 0.055000
0.230000 0.055000
0.405000 0.055000
-0.395000 0.060000
-0.335000 0.060000
-0.270000 0.060000
-0.110000 0.060000
-0.050000 0.060000
0.015000 0.060000
0.075000 0.060000
0.140000 0.060000
0.205000 0.060000
0.265000 0.060000
-0.410000 0.065000
-0.350000 0.065000
-0.285000 0.065000
-0.180000 0.065000
-0.115000 0.065000
-0.050000 0.065000
0.010000 0.065000
0.075000 0.065000
0.135000 0.065000
0.200000 0.065000
0.265000 0.065000
-0.410000 0.070000
-0.345000 0.070000
-0.285000 0.070000
-0.220000 0.070000
-0.160000 0.070000
-0.095000 0.070000
-0.030000 0.070000
0.030000 0.070000
0.095000 0.070000
0.155000 0.070000
0.220000 0.070000
0.420000 0.070000
-0.375000 0.075000
-0.310000 0.075000
-0.250000 0.075000
-0.185000 0.075000
-0.120000 0.075000
-0.060000 0.075000
0.005000 0.075000
0.065000 0.075000
0.130000 0.075000
0.190000 0.075000
0.395000 0.075000
-0.395000 0.080000
-0.335000 0.080000
-0.270000 0.080000
-0.210000 0.080000
-0.145000 0.080000
-0.080000 0.080000
-0.020000 0.080000
0.045000 0.080000
0.105000 0.080000
0.170000 0.080000
0.230000 0.080000
-0.415000 0.085000
-0.350000 0.085000
-0.290000 0.085000
-0.225000 0.085000
-0.165000 0.085000
-0.100000 0.085000
-0.035000 0.085000
0.025000 0.085000
0.090000 0.085000
0.150000 0.085000
0.215000 0.085000
-0.425000 0.090000
-0.365000 0.090000
-0.300000 0.090000
-0.240000 0.090000
-0.175000 0.090000
-0.115000 0.090000
-0.050000 0.090000
0.015000 0.090000
0.075000 0.090000
0.140000 0.090000
0.200000 0.090000
0.410000 0.090000
-0.375000 0.095000
-0.315000 0.095000
-0.250000 0.095000
-0.190000 0.095000
-0.125000 0.095000
-0.060000 0.095000
0.000000 0.095000
0.065000 0.095000
0.125000 0.095000
0.190000 0.095000
0.410000 0.095000
-0.365000 0.100000
-0.300000 0.100000
-0.240000 0.100000
-0.175000 0.100000
-0.115000 0.100000
-0.050000 0.100000
0.015000 0.100000
0.075000 0.100000
0.140000 0.100000
0.200000 0.100000
-0.415000 0.105000
-0.355000 0.105000
-0.290000 0.105000
-0.225000 0.105000
-0.165000 0.105000
-0.100000 0.105000
-0.040000 0.105000
0.025000 0.105000
0.090000 0.105000
0.150000 0.105000
0.335000 0.105000
-0.400000 0.110000
-0.335000 0.110000
-0.270000 0.110000
-0.210000 0.110000
-0.145000 0.110000
-0.085000 0.110000
-0.020000 0.110000
0.040000 0.110000
0.105000 0.110000
0.170000 0.110000
0.395000 0.110000
-0.370000 0.115000
-0.310000 0.115000
-0.245000 0.115000
-0.180000 0.115000
-0.120000 0.115000
-0.055000 0.115000
0.005000 0.115000
0.070000 0.115000
0.135000 0.115000
0.195000 0.115000
-0.405000 0.120000
-0.345000 0.120000
-0.280000 0.120000
-0.220000 0.120000
-0.155000 0.120000
-0.090000 0.120000
-0.030000 0.120000
0.035000 0.120000
0.095000 0.120000
0.160000 0.120000
0.390000 0.120000
-0.375000 0.125000
-0.310000 0.125000
-0.250000 0.125000
-0.185000 0.125000
-0.125000 0.125000
-0.060000 0.125000
0.005000 0.125000
0.065000 0.125000
0.130000 0.125000
0.270000 0.125000
-0.395000 0.130000
-0.330000 0.130000
-0.270000 0.130000
-0.205000 0.130000
-0.145000 0.130000
-0.080000 0.130000
-0.015000 0.130000
0.045000 0.130000
0.120000 0.130000
0.180000 0.130000
-0.400000 0.135000
-0.340000 0.135000
-0.275000 0.135000
-0.210000 0.135000
-0.150000 0.135000
-0.085000 0.135000
-0.025000 0.135000
0.040000 0.135000
0.140000 0.135000
0.370000 0.135000
-0.380000 0.140000
-0.320000 0.140000
-0.255000 0.140000
-0.195000 0.140000
-0.130000 0.140000
-0.065000 0.140000
-0.005000 0.140000
0.100000 0.140000
0.160000 0.140000
-0.405000 0.145000
-0.340000 0.145000
-0.280000 0.145000
-0.215000 0.145000
-0.155000 0.145000
-0.090000 0.145000
-0.025000 0.145000
0.035000 0.145000
0.145000 0.145000
0.385000 0.145000
-0.355000 0.150000
-0.295000 0.150000
-0.230000 0.150000
-0.165000 0.150000
-0.105000 0.150000
-0.040000 0.150000
0.020000 0.150000
0.130000 0.150000
0.370000 0.150000
-0.365000 0.155000
-0.300000 0.155000
-0.240000 0.155000
-0.175000 0.155000
-0.110000 0.155000
-0.050000 0.155000
0.015000 0.155000
0.115000 0.155000
0.355000 0.155000
-0.380000 0.160000
-0.315000 0.160000
-0.250000 0.160000
-0.190000 0.160000
-0.125000 0.160000
-0.065000 0.160000
0.000000 0.160000
0.095000 0.160000
0.155000 0.160000
-0.390000 0.165000
-0.330000 0.165000
-0.265000 0.165000
-0.205000 0.165000
-0.140000 0.165000
-0.075000 0.165000
-0.015000 0.165000
0.050000 0.165000
0.110000 0.165000
0.350000 0.165000
-0.370000 0.170000
-0.310000 0.170000
-0.245000 0.170000
-0.185000 0.170000
-0.120000 0.170000
-0.055000 0.170000
0.005000 0.170000
0.070000 0.170000
0.130000 0.170000
0.370000 0.170000
-0.350000 0.175000
-0.285000 0.175000
-0.220000 0.175000
-0.160000 0.175000
-0.095000 0.175000
-0.035000 0.175000
0.030000 0.175000
0.095000 0.175000
0.295000 0.175000
-0.370000 0.180000
-0.310000 0.180000
-0.245000 0.180000
-0.185000 0.180000
-0.120000 0.180000
-0.055000 0.180000
0.005000 0.180000
0.070000 0.180000
0.130000 0.180000
-0.390000 0.185000
-0.325000 0.185000
-0.265000 0.185000
-0.200000 0.185000
-0.140000 0.185000
-0.075000 0.185000
-0.010000 0.185000
0.050000 0.185000
0.115000 0.185000
0.345000 0.185000
-0.350000 0.190000
-0.290000 0.190000
-0.225000 0.190000
-0.160000 0.190000
-0.100000 0.190000
-0.035000 0.190000
0.025000 0.190000
0.090000 0.190000
0.285000 0.190000
-0.365000 0.195000
-0.300000 0.195000
-0.240000 0.195000
-0.175000 0.195000
-0.110000 0.195000
-0.050000 0.195000
0.015000 0.195000
0.075000 0.195000
0.220000 0.195000
-0.380000 0.200000
-0.315000 0.200000
-0.250000 0.200000
-0.190000 0.200000
-0.125000 0.200000
-0.065000 0.200000
0.000000 0.200000
0.065000 0.200000
0.205000 0.200000
-0.380000 0.205000
-0.320000 0.205000
-0.255000 0.205000
-0.195000 0.205000
-0.130000 0.205000
-0.065000 0.205000
-0.005000 0.205000
0.060000 0.205000
0.200000 0.205000
0.350000 0.205000
-0.315000 0.210000
-0.255000 0.210000
-0.190000 0.210000
-0.130000 0.210000
-0.065000 0.210000
0.000000 0.210000
0.060000 0.210000
0.200000 0.210000
-0.375000 0.215000
-0.310000 0.215000
-0.250000 0.215000
-0.185000 0.215000
-0.120000 0.215000
-0.060000 0.215000
0.005000 0.215000
0.065000 0.215000
0.260000 0.215000
-0.360000 0.220000
-0.300000 0.220000
-0.235000 0.220000
-0.175000 0.220000
-0.110000 0.220000
-0.045000 0.220000
0.015000 0.220000
0.080000 0.220000
0.265000 0.220000
-0.345000 0.225000
-0.285000 0.225000
-0.220000 0.225000
-0.155000 0.225000
-0.095000 0.225000
-0.030000 0.225000
0.030000 0.225000
0.095000 0.225000
0.320000 0.225000
-0.325000 0.230000
-0.260000 0.230000
-0.200000 0.230000
-0.135000 0.230000
-0.075000 0.230000
-0.010000 0.230000
0.055000 0.230000
0.180000 0.230000
-0.355000 0.235000
-0.295000 0.235000
-0.230000 0.235000
-0.165000 0.235000
-0.105000 0.235000
-0.040000 0.235000
0.020000 0.235000
0.085000 0.235000
0.310000 0.235000
-0.315000 0.240000
-0.250000 0.240000
-0.190000 0.240000
-0.125000 0.240000
-0.065000 0.240000
0.000000 0.240000
0.065000 0.240000
0.240000 0.240000
-0.330000 0.245000
-0.270000 0.245000
-0.205000 0.245000
-0.140000 0.245000
-0.080000 0.245000
-0.015000 0.245000
0.045000 0.245000
0.230000 0.245000
-0.340000 0.250000
-0.275000 0.250000
-0.210000 0.250000
-0.150000 0.250000
-0.085000 0.250000
-0.020000 0.250000
0.045000 0.250000
0.225000 0.250000
-0.340000 0.255000
-0.275000 0.255000
-0.215000 0.255000
-0.150000 0.255000
-0.085000 0.255000
-0.020000 0.255000
0.045000 0.255000
0.215000 0.255000
-0.335000 0.260000
-0.275000 0.260000
-0.210000 0.260000
-0.145000 0.260000
-0.085000 0.260000
-0.015000 0.260000
0.045000 0.260000
0.220000 0.260000
-0.315000 0.265000
-0.255000 0.265000
-0.190000 0.265000
-0.130000 0.265000
-0.055000 0.265000
0.010000 0.265000
0.070000 0.265000
0.280000 0.265000
-0.285000 0.270000
-0.220000 0.270000
-0.160000 0.270000
-0.095000 0.270000
-0.020000 0.270000
0.040000 0.270000
0.255000 0.270000
-0.300000 0.275000
-0.235000 0.275000
-0.170000 0.275000
-0.110000 0.275000
-0.030000 0.275000
0.030000 0.275000
0.245000 0.275000
-0.295000 0.280000
-0.230000 0.280000
-0.165000 0.280000
-0.105000 0.280000
-0.025000 0.280000
0.035000 0.280000
0.250000 0.280000
-0.270000 0.285000
-0.210000 0.285000
-0.145000 0.285000
-0.065000 0.285000
0.000000 0.285000
0.170000 0.285000
0.270000 0.285000
-0.235000 0.290000
-0.175000 0.290000
-0.085000 0.290000
-0.025000 0.290000
0.040000 0.290000
0.245000 0.290000
-0.250000 0.295000
-0.185000 0.295000
-0.090000 0.295000
-0.025000 0.295000
0.040000 0.295000
0.230000 0.295000
-0.235000 0.300000
-0.175000 0.300000
-0.065000 0.300000
-0.005000 0.300000
0.145000 0.300000
0.255000 0.300000
-0.190000 0.305000
-0.070000 0.305000
-0.010000 0.305000
0.135000 0.305000
0.245000 0.305000
-0.070000 0.310000
-0.005000 0.310000
0.130000 0.310000
-0.130000 0.315000
-0.065000 0.315000
-0.005000 0.315000
0.180000 0.315000
-0.140000 0.320000
-0.075000 0.320000
-0.015000 0.320000
0.105000 0.320000
-0.150000 0.325000
-0.090000 0.325000
-0.025000 0.325000
0.080000 0.325000
0.210000 0.325000
-0.110000 0.330000
-0.050000 0.330000
0.145000 0.330000
0.205000 0.330000
-0.125000 0.335000
-0.060000 0.335000
0.000000 0.335000
0.190000 0.335000
-0.155000 0.340000
-0.090000 0.340000
-0.030000 0.340000
0.150000 0.340000
-0.190000 0.345000
-0.130000 0.345000
-0.065000 0.345000
-0.005000 0.345000
0.160000 0.345000
-0.170000 0.350000
-0.110000 0.350000
-0.045000 0.350000
0.110000 0.350000
-0.200000 0.355000
-0.140000 0.355000
-0.075000 0.355000
0.070000 0.355000
0.130000 0.355000
-0.155000 0.360000
-0.095000 0.360000
-0.030000 0.360000
0.085000 0.360000
0.145000 0.360000
-0.115000 0.365000
-0.055000 0.365000
0.010000 0.365000
0.075000 0.365000
-0.160000 0.370000
-0.095000 0.370000
-0.035000 0.370000
0.030000 0.370000
0.090000 0.370000
-0.110000 0.375000
-0.045000 0.375000
0.015000 0.375000
0.080000 0.375000
-0.095000 0.380000
-0.030000 0.380000
0.035000 0.380000
-0.095000 0.385000
-0.030000 0.385000
0.030000 0.385000
-0.040000 0.390000
1 -0.100000 -0.390000
2 -0.040000 -0.390000
3 -0.090000 -0.385000
4 -0.030000 -0.385000
5 -0.125000 -0.380000
6 -0.065000 -0.380000
7 0.000000 -0.380000
8 -0.135000 -0.375000
9 -0.075000 -0.375000
10 -0.010000 -0.375000
11 -0.185000 -0.370000
12 -0.120000 -0.370000
13 -0.055000 -0.370000
14 0.005000 -0.370000
15 -0.185000 -0.365000
16 -0.125000 -0.365000
17 -0.060000 -0.365000
18 0.000000 -0.365000
19 -0.210000 -0.360000
20 -0.145000 -0.360000
21 -0.085000 -0.360000
22 -0.020000 -0.360000
23 0.040000 -0.360000
24 -0.175000 -0.355000
25 -0.110000 -0.355000
26 -0.050000 -0.355000
27 0.015000 -0.355000
28 -0.215000 -0.350000
29 -0.150000 -0.350000
30 -0.085000 -0.350000
31 -0.025000 -0.350000
32 0.040000 -0.350000
33 -0.195000 -0.345000
34 -0.130000 -0.345000
35 -0.070000 -0.345000
36 -0.005000 -0.345000
37 -0.235000 -0.340000
38 -0.175000 -0.340000
39 -0.110000 -0.340000
40 -0.050000 -0.340000
41 0.015000 -0.340000
42 -0.220000 -0.335000
43 -0.160000 -0.335000
44 -0.095000 -0.335000
45 -0.035000 -0.335000
46 0.030000 -0.335000
47 -0.220000 -0.330000
48 -0.155000 -0.330000
49 -0.090000 -0.330000
50 -0.030000 -0.330000
51 0.035000 -0.330000
52 -0.215000 -0.325000
53 -0.150000 -0.325000
54 -0.085000 -0.325000
55 -0.025000 -0.325000
56 -0.270000 -0.320000
57 -0.210000 -0.320000
58 -0.145000 -0.320000
59 -0.080000 -0.320000
60 -0.020000 -0.320000
61 -0.265000 -0.315000
62 -0.205000 -0.315000
63 -0.140000 -0.315000
64 -0.080000 -0.315000
65 -0.015000 -0.315000
66 -0.260000 -0.310000
67 -0.200000 -0.310000
68 -0.135000 -0.310000
69 -0.075000 -0.310000
70 -0.010000 -0.310000
71 -0.250000 -0.305000
72 -0.190000 -0.305000
73 -0.125000 -0.305000
74 -0.065000 -0.305000
75 -0.305000 -0.300000
76 -0.240000 -0.300000
77 -0.180000 -0.300000
78 -0.115000 -0.300000
79 -0.055000 -0.300000
80 -0.285000 -0.295000
81 -0.225000 -0.295000
82 -0.160000 -0.295000
83 -0.095000 -0.295000
84 -0.035000 -0.295000
85 0.035000 -0.295000
86 -0.295000 -0.290000
87 -0.230000 -0.290000
88 -0.165000 -0.290000
89 -0.105000 -0.290000
90 -0.040000 -0.290000
91 0.020000 -0.290000
92 0.085000 -0.290000
93 -0.270000 -0.285000
94 -0.205000 -0.285000
95 -0.140000 -0.285000
96 -0.080000 -0.285000
97 -0.015000 -0.285000
98 0.045000 -0.285000
99 -0.320000 -0.280000
100 -0.255000 -0.280000
101 -0.195000 -0.280000
102 -0.130000 -0.280000
103 -0.070000 -0.280000
104 -0.005000 -0.280000
105 0.060000 -0.280000
106 -0.330000 -0.275000
107 -0.265000 -0.275000
108 -0.205000 -0.275000
109 -0.140000 -0.275000
110 -0.080000 -0.275000
111 -0.015000 -0.275000
112 0.050000 -0.275000
113 0.110000 -0.275000
114 -0.295000 -0.270000
115 -0.235000 -0.270000
116 -0.170000 -0.270000
117 -0.105000 -0.270000
118 -0.045000 -0.270000
119 0.020000 -0.270000
120 0.080000 -0.270000
121 0.145000 -0.270000
122 -0.290000 -0.265000
123 -0.225000 -0.265000
124 -0.160000 -0.265000
125 -0.100000 -0.265000
126 -0.035000 -0.265000
127 0.025000 -0.265000
128 0.090000 -0.265000
129 0.155000 -0.265000
130 -0.305000 -0.260000
131 -0.240000 -0.260000
132 -0.180000 -0.260000
133 -0.055000 -0.260000
134 0.010000 -0.260000
135 0.070000 -0.260000
136 0.135000 -0.260000
137 -0.340000 -0.255000
138 -0.275000 -0.255000
139 -0.215000 -0.255000
140 -0.150000 -0.255000
141 0.010000 -0.255000
142 0.070000 -0.255000
143 0.135000 -0.255000
144 -0.350000 -0.250000
145 -0.285000 -0.250000
146 -0.220000 -0.250000
147 -0.160000 -0.250000
148 0.030000 -0.250000
149 0.090000 -0.250000
150 0.155000 -0.250000
151 -0.325000 -0.245000
152 -0.265000 -0.245000
153 -0.200000 -0.245000
154 0.005000 -0.245000
155 0.070000 -0.245000
156 0.130000 -0.245000
157 -0.355000 -0.240000
158 -0.290000 -0.240000
159 -0.230000 -0.240000
160 0.000000 -0.240000
161 0.060000 -0.240000
162 0.125000 -0.240000
163 -0.360000 -0.235000
164 -0.300000 -0.235000
165 -0.235000 -0.235000
166 0.005000 -0.235000
167 0.070000 -0.235000
168 0.130000 -0.235000
169 -0.350000 -0.230000
170 -0.285000 -0.230000
171 -0.225000 -0.230000
172 0.035000 -0.230000
173 0.095000 -0.230000
174 0.160000 -0.230000
175 -0.320000 -0.225000
176 -0.260000 -0.225000
177 0.010000 -0.225000
178 0.070000 -0.225000
179 0.135000 -0.225000
180 -0.345000 -0.220000
181 -0.285000 -0.220000
182 -0.220000 -0.220000
183 0.060000 -0.220000
184 0.125000 -0.220000
185 -0.355000 -0.215000
186 -0.290000 -0.215000
187 -0.225000 -0.215000
188 0.065000 -0.215000
189 0.130000 -0.215000
190 -0.350000 -0.210000
191 -0.285000 -0.210000
192 0.025000 -0.210000
193 0.085000 -0.210000
194 0.150000 -0.210000
195 -0.330000 -0.205000
196 -0.265000 -0.205000
197 0.060000 -0.205000
198 0.120000 -0.205000
199 -0.345000 -0.200000
200 -0.285000 -0.200000
201 0.055000 -0.200000
202 0.115000 -0.200000
203 -0.345000 -0.195000
204 -0.280000 -0.195000
205 0.070000 -0.195000
206 -0.375000 -0.190000
207 -0.315000 -0.190000
208 -0.395000 -0.185000
209 -0.330000 -0.185000
210 -0.270000 -0.185000
211 -0.345000 -0.180000
212 -0.285000 -0.180000
213 -0.360000 -0.175000
214 -0.300000 -0.175000
215 -0.375000 -0.170000
216 -0.310000 -0.170000
217 -0.390000 -0.165000
218 -0.325000 -0.165000
219 -0.400000 -0.160000
220 -0.335000 -0.160000
221 -0.405000 -0.155000
222 -0.345000 -0.155000
223 -0.280000 -0.155000
224 -0.355000 -0.150000
225 -0.290000 -0.150000
226 -0.360000 -0.145000
227 -0.300000 -0.145000
228 -0.365000 -0.140000
229 -0.305000 -0.140000
230 -0.370000 -0.135000
231 -0.310000 -0.135000
232 -0.375000 -0.130000
233 -0.310000 -0.130000
234 -0.380000 -0.125000
235 -0.315000 -0.125000
236 -0.380000 -0.120000
237 -0.315000 -0.120000
238 -0.380000 -0.115000
239 -0.320000 -0.115000
240 -0.385000 -0.110000
241 -0.325000 -0.110000
242 -0.385000 -0.105000
243 -0.325000 -0.105000
244 -0.385000 -0.100000
245 -0.320000 -0.100000
246 -0.390000 -0.095000
247 -0.325000 -0.095000
248 -0.390000 -0.090000
249 -0.325000 -0.090000
250 -0.385000 -0.085000
251 -0.325000 -0.085000
252 -0.385000 -0.080000
253 -0.325000 -0.080000
254 -0.385000 -0.075000
255 -0.320000 -0.075000
256 -0.385000 -0.070000
257 -0.320000 -0.070000
258 -0.435000 -0.065000
259 -0.370000 -0.065000
260 0.255000 -0.065000
261 0.320000 -0.065000
262 0.385000 -0.065000
263 -0.390000 -0.060000
264 -0.325000 -0.060000
265 0.270000 -0.060000
266 0.335000 -0.060000
267 0.400000 -0.060000
268 -0.385000 -0.055000
269 -0.320000 -0.055000
270 0.250000 -0.055000
271 0.315000 -0.055000
272 0.380000 -0.055000
273 -0.415000 -0.050000
274 -0.350000 -0.050000
275 0.195000 -0.050000
276 0.260000 -0.050000
277 0.320000 -0.050000
278 0.385000 -0.050000
279 -0.410000 -0.045000
280 -0.350000 -0.045000
281 0.180000 -0.045000
282 0.240000 -0.045000
283 0.305000 -0.045000
284 0.370000 -0.045000
285 -0.440000 -0.040000
286 -0.375000 -0.040000
287 -0.315000 -0.040000
288 0.200000 -0.040000
289 0.260000 -0.040000
290 0.325000 -0.040000
291 0.390000 -0.040000
292 -0.420000 -0.035000
293 -0.355000 -0.035000
294 0.135000 -0.035000
295 0.200000 -0.035000
296 0.265000 -0.035000
297 0.325000 -0.035000
298 0.390000 -0.035000
299 -0.425000 -0.030000
300 -0.360000 -0.030000
301 0.120000 -0.030000
302 0.180000 -0.030000
303 0.245000 -0.030000
304 0.305000 -0.030000
305 0.370000 -0.030000
306 0.430000 -0.030000
307 -0.380000 -0.025000
308 -0.315000 -0.025000
309 0.145000 -0.025000
310 0.210000 -0.025000
311 0.270000 -0.025000
312 0.335000 -0.025000
313 0.400000 -0.025000
314 -0.415000 -0.020000
315 -0.350000 -0.020000
316 0.095000 -0.020000
317 0.160000 -0.020000
318 0.220000 -0.020000
319 0.285000 -0.020000
320 0.350000 -0.020000
321 0.410000 -0.020000
322 -0.400000 -0.015000
323 -0.340000 -0.015000
324 0.095000 -0.015000
325 0.160000 -0.015000
326 0.220000 -0.015000
327 0.285000 -0.015000
328 0.345000 -0.015000
329 0.410000 -0.015000
330 -0.405000 -0.010000
331 -0.345000 -0.010000
332 0.075000 -0.010000
333 0.135000 -0.010000
334 0.200000 -0.010000
335 0.260000 -0.010000
336 0.325000 -0.010000
337 0.390000 -0.010000
338 -0.430000 -0.005000
339 -0.365000 -0.005000
340 0.030000 -0.005000
341 0.095000 -0.005000
342 0.160000 -0.005000
343 0.220000 -0.005000
344 0.285000 -0.005000
345 0.345000 -0.005000
346 0.410000 -0.005000
347 -0.400000 0.000000
348 -0.340000 0.000000
349 0.045000 0.000000
350 0.105000 0.000000
351 0.170000 0.000000
352 0.230000 0.000000
353 0.295000 0.000000
354 0.360000 0.000000
355 0.420000 0.000000
356 -0.390000 0.005000
357 -0.330000 0.005000
358 0.045000 0.005000
359 0.110000 0.005000
360 0.170000 0.005000
361 0.235000 0.005000
362 0.295000 0.005000
363 0.360000 0.005000
364 0.420000 0.005000
365 -0.390000 0.010000
366 -0.325000 0.010000
367 0.025000 0.010000
368 0.090000 0.010000
369 0.150000 0.010000
370 0.215000 0.010000
371 0.280000 0.010000
372 0.340000 0.010000
373 0.405000 0.010000
374 -0.410000 0.015000
375 -0.345000 0.015000
376 -0.005000 0.015000
377 0.055000 0.015000
378 0.120000 0.015000
379 0.180000 0.015000
380 0.245000 0.015000
381 0.305000 0.015000
382 0.370000 0.015000
383 -0.440000 0.020000
384 -0.380000 0.020000
385 -0.315000 0.020000
386 0.005000 0.020000
387 0.070000 0.020000
388 0.135000 0.020000
389 0.195000 0.020000
390 0.260000 0.020000
391 0.320000 0.020000
392 0.385000 0.020000
393 -0.425000 0.025000
394 -0.365000 0.025000
395 -0.300000 0.025000
396 0.005000 0.025000
397 0.070000 0.025000
398 0.130000 0.025000
399 0.195000 0.025000
400 0.260000 0.025000
401 0.320000 0.025000
402 0.415000 0.025000
403 -0.400000 0.030000
404 -0.335000 0.030000
405 -0.040000 0.030000
406 0.020000 0.030000
407 0.085000 0.030000
408 0.145000 0.030000
409 0.210000 0.030000
410 0.270000 0.030000
411 0.335000 0.030000
412 -0.420000 0.035000
413 -0.360000 0.035000
414 -0.295000 0.035000
415 -0.025000 0.035000
416 0.040000 0.035000
417 0.105000 0.035000
418 0.165000 0.035000
419 0.230000 0.035000
420 0.290000 0.035000
421 0.430000 0.035000
422 -0.375000 0.040000
423 -0.315000 0.040000
424 -0.060000 0.040000
425 0.000000 0.040000
426 0.065000 0.040000
427 0.125000 0.040000
428 0.190000 0.040000
429 0.255000 0.040000
430 0.405000 0.040000
431 -0.400000 0.045000
432 -0.340000 0.045000
433 -0.100000 0.045000
434 -0.035000 0.045000
435 0.025000 0.045000
436 0.090000 0.045000
437 0.150000 0.045000
438 0.215000 0.045000
439 0.275000 0.045000
440 -0.430000 0.050000
441 -0.365000 0.050000
442 -0.305000 0.050000
443 -0.090000 0.050000
444 -0.030000 0.050000
445 0.035000 0.050000
446 0.100000 0.050000
447 0.160000 0.050000
448 0.225000 0.050000
449 0.285000 0.050000
450 -0.405000 0.055000
451 -0.340000 0.055000
452 -0.280000 0.055000
453 -0.085000 0.055000
454 -0.025000 0.055000
455 0.040000 0.055000
456 0.100000 0.055000
457 0.165000 0.055000
458 0.230000 0.055000
459 0.405000 0.055000
460 -0.395000 0.060000
461 -0.335000 0.060000
462 -0.270000 0.060000
463 -0.110000 0.060000
464 -0.050000 0.060000
465 0.015000 0.060000
466 0.075000 0.060000
467 0.140000 0.060000
468 0.205000 0.060000
469 0.265000 0.060000
470 -0.410000 0.065000
471 -0.350000 0.065000
472 -0.285000 0.065000
473 -0.180000 0.065000
474 -0.115000 0.065000
475 -0.050000 0.065000
476 0.010000 0.065000
477 0.075000 0.065000
478 0.135000 0.065000
479 0.200000 0.065000
480 0.265000 0.065000
481 -0.410000 0.070000
482 -0.345000 0.070000
483 -0.285000 0.070000
484 -0.220000 0.070000
485 -0.160000 0.070000
486 -0.095000 0.070000
487 -0.030000 0.070000
488 0.030000 0.070000
489 0.095000 0.070000
490 0.155000 0.070000
491 0.220000 0.070000
492 0.420000 0.070000
493 -0.375000 0.075000
494 -0.310000 0.075000
495 -0.250000 0.075000
496 -0.185000 0.075000
497 -0.120000 0.075000
498 -0.060000 0.075000
499 0.005000 0.075000
500 0.065000 0.075000
501 0.130000 0.075000
502 0.190000 0.075000
503 0.395000 0.075000
504 -0.395000 0.080000
505 -0.335000 0.080000
506 -0.270000 0.080000
507 -0.210000 0.080000
508 -0.145000 0.080000
509 -0.080000 0.080000
510 -0.020000 0.080000
511 0.045000 0.080000
512 0.105000 0.080000
513 0.170000 0.080000
514 0.230000 0.080000
515 -0.415000 0.085000
516 -0.350000 0.085000
517 -0.290000 0.085000
518 -0.225000 0.085000
519 -0.165000 0.085000
520 -0.100000 0.085000
521 -0.035000 0.085000
522 0.025000 0.085000
523 0.090000 0.085000
524 0.150000 0.085000
525 0.215000 0.085000
526 -0.425000 0.090000
527 -0.365000 0.090000
528 -0.300000 0.090000
529 -0.240000 0.090000
530 -0.175000 0.090000
531 -0.115000 0.090000
532 -0.050000 0.090000
533 0.015000 0.090000
534 0.075000 0.090000
535 0.140000 0.090000
536 0.200000 0.090000
537 0.410000 0.090000
538 -0.375000 0.095000
539 -0.315000 0.095000
540 -0.250000 0.095000
541 -0.190000 0.095000
542 -0.125000 0.095000
543 -0.060000 0.095000
544 0.000000 0.095000
545 0.065000 0.095000
546 0.125000 0.095000
547 0.190000 0.095000
548 0.410000 0.095000
549 -0.365000 0.100000
550 -0.300000 0.100000
551 -0.240000 0.100000
552 -0.175000 0.100000
553 -0.115000 0.100000
554 -0.050000 0.100000
555 0.015000 0.100000
556 0.075000 0.100000
557 0.140000 0.100000
558 0.200000 0.100000
559 -0.415000 0.105000
560 -0.355000 0.105000
561 -0.290000 0.105000
562 -0.225000 0.105000
563 -0.165000 0.105000
564 -0.100000 0.105000
565 -0.040000 0.105000
566 0.025000 0.105000
567 0.090000 0.105000
568 0.150000 0.105000
569 0.335000 0.105000
570 -0.400000 0.110000
571 -0.335000 0.110000
572 -0.270000 0.110000
573 -0.210000 0.110000
574 -0.145000 0.110000
575 -0.085000 0.110000
576 -0.020000 0.110000
577 0.040000 0.110000
578 0.105000 0.110000
579 0.170000 0.110000
580 0.395000 0.110000
581 -0.370000 0.115000
582 -0.310000 0.115000
583 -0.245000 0.115000
584 -0.180000 0.115000
585 -0.120000 0.115000
586 -0.055000 0.115000
587 0.005000 0.115000
588 0.070000 0.115000
589 0.135000 0.115000
590 0.195000 0.115000
591 -0.405000 0.120000
592 -0.345000 0.120000
593 -0.280000 0.120000
594 -0.220000 0.120000
595 -0.155000 0.120000
596 -0.090000 0.120000
597 -0.030000 0.120000
598 0.035000 0.120000
599 0.095000 0.120000
600 0.160000 0.120000
601 0.390000 0.120000
602 -0.375000 0.125000
603 -0.310000 0.125000
604 -0.250000 0.125000
605 -0.185000 0.125000
606 -0.125000 0.125000
607 -0.060000 0.125000
608 0.005000 0.125000
609 0.065000 0.125000
610 0.130000 0.125000
611 0.270000 0.125000
612 -0.395000 0.130000
613 -0.330000 0.130000
614 -0.270000 0.130000
615 -0.205000 0.130000
616 -0.145000 0.130000
617 -0.080000 0.130000
618 -0.015000 0.130000
619 0.045000 0.130000
620 0.120000 0.130000
621 0.180000 0.130000
622 -0.400000 0.135000
623 -0.340000 0.135000
624 -0.275000 0.135000
625 -0.210000 0.135000
626 -0.150000 0.135000
627 -0.085000 0.135000
628 -0.025000 0.135000
629 0.040000 0.135000
630 0.140000 0.135000
631 0.370000 0.135000
632 -0.380000 0.140000
633 -0.320000 0.140000
634 -0.255000 0.140000
635 -0.195000 0.140000
636 -0.130000 0.140000
637 -0.065000 0.140000
638 -0.005000 0.140000
639 0.100000 0.140000
640 0.160000 0.140000
641 -0.405000 0.145000
642 -0.340000 0.145000
643 -0.280000 0.145000
644 -0.215000 0.145000
645 -0.155000 0.145000
646 -0.090000 0.145000
647 -0.025000 0.145000
648 0.035000 0.145000
649 0.145000 0.145000
650 0.385000 0.145000
651 -0.355000 0.150000
652 -0.295000 0.150000
653 -0.230000 0.150000
654 -0.165000 0.150000
655 -0.105000 0.150000
656 -0.040000 0.150000
657 0.020000 0.150000
658 0.130000 0.150000
659 0.370000 0.150000
660 -0.365000 0.155000
661 -0.300000 0.155000
662 -0.240000 0.155000
663 -0.175000 0.155000
664 -0.110000 0.155000
665 -0.050000 0.155000
666 0.015000 0.155000
667 0.115000 0.155000
668 0.355000 0.155000
669 -0.380000 0.160000
670 -0.315000 0.160000
671 -0.250000 0.160000
672 -0.190000 0.160000
673 -0.125000 0.160000
674 -0.065000 0.160000
675 0.000000 0.160000
676 0.095000 0.160000
677 0.155000 0.160000
678 -0.390000 0.165000
679 -0.330000 0.165000
680 -0.265000 0.165000
681 -0.205000 0.165000
682 -0.140000 0.165000
683 -0.075000 0.165000
684 -0.015000 0.165000
685 0.050000 0.165000
686 0.110000 0.165000
687 0.350000 0.165000
688 -0.370000 0.170000
689 -0.310000 0.170000
690 -0.245000 0.170000
691 -0.185000 0.170000
692 -0.120000 0.170000
693 -0.055000 0.170000
694 0.005000 0.170000
695 0.070000 0.170000
696 0.130000 0.170000
697 0.370000 0.170000
698 -0.350000 0.175000
699 -0.285000 0.175000
700 -0.220000 0.175000
701 -0.160000 0.175000
702 -0.095000 0.175000
703 -0.035000 0.175000
704 0.030000 0.175000
705 0.095000 0.175000
706 0.295000 0.175000
707 -0.370000 0.180000
708 -0.310000 0.180000
709 -0.245000 0.180000
710 -0.185000 0.180000
711 -0.120000 0.180000
712 -0.055000 0.180000
713 0.005000 0.180000
714 0.070000 0.180000
715 0.130000 0.180000
716 -0.390000 0.185000
717 -0.325000 0.185000
718 -0.265000 0.185000
719 -0.200000 0.185000
720 -0.140000 0.185000
721 -0.075000 0.185000
722 -0.010000 0.185000
723 0.050000 0.185000
724 0.115000 0.185000
725 0.345000 0.185000
726 -0.350000 0.190000
727 -0.290000 0.190000
728 -0.225000 0.190000
729 -0.160000 0.190000
730 -0.100000 0.190000
731 -0.035000 0.190000
732 0.025000 0.190000
733 0.090000 0.190000
734 0.285000 0.190000
735 -0.365000 0.195000
736 -0.300000 0.195000
737 -0.240000 0.195000
738 -0.175000 0.195000
739 -0.110000 0.195000
740 -0.050000 0.195000
741 0.015000 0.195000
742 0.075000 0.195000
743 0.220000 0.195000
744 -0.380000 0.200000
745 -0.315000 0.200000
746 -0.250000 0.200000
747 -0.190000 0.200000
748 -0.125000 0.200000
749 -0.065000 0.200000
750 0.000000 0.200000
751 0.065000 0.200000
752 0.205000 0.200000
753 -0.380000 0.205000
754 -0.320000 0.205000
755 -0.255000 0.205000
756 -0.195000 0.205000
757 -0.130000 0.205000
758 -0.065000 0.205000
759 -0.005000 0.205000
760 0.060000 0.205000
761 0.200000 0.205000
762 0.350000 0.205000
763 -0.315000 0.210000
764 -0.255000 0.210000
765 -0.190000 0.210000
766 -0.130000 0.210000
767 -0.065000 0.210000
768 0.000000 0.210000
769 0.060000 0.210000
770 0.200000 0.210000
771 -0.375000 0.215000
772 -0.310000 0.215000
773 -0.250000 0.215000
774 -0.185000 0.215000
775 -0.120000 0.215000
776 -0.060000 0.215000
777 0.005000 0.215000
778 0.065000 0.215000
779 0.260000 0.215000
780 -0.360000 0.220000
781 -0.300000 0.220000
782 -0.235000 0.220000
783 -0.175000 0.220000
784 -0.110000 0.220000
785 -0.045000 0.220000
786 0.015000 0.220000
787 0.080000 0.220000
788 0.265000 0.220000
789 -0.345000 0.225000
790 -0.285000 0.225000
791 -0.220000 0.225000
792 -0.155000 0.225000
793 -0.095000 0.225000
794 -0.030000 0.225000
795 0.030000 0.225000
796 0.095000 0.225000
797 0.320000 0.225000
798 -0.325000 0.230000
799 -0.260000 0.230000
800 -0.200000 0.230000
801 -0.135000 0.230000
802 -0.075000 0.230000
803 -0.010000 0.230000
804 0.055000 0.230000
805 0.180000 0.230000
806 -0.355000 0.235000
807 -0.295000 0.235000
808 -0.230000 0.235000
809 -0.165000 0.235000
810 -0.105000 0.235000
811 -0.040000 0.235000
812 0.020000 0.235000
813 0.085000 0.235000
814 0.310000 0.235000
815 -0.315000 0.240000
816 -0.250000 0.240000
817 -0.190000 0.240000
818 -0.125000 0.240000
819 -0.065000 0.240000
820 0.000000 0.240000
821 0.065000 0.240000
822 0.240000 0.240000
823 -0.330000 0.245000
824 -0.270000 0.245000
825 -0.205000 0.245000
826 -0.140000 0.245000
827 -0.080000 0.245000
828 -0.015000 0.245000
829 0.045000 0.245000
830 0.230000 0.245000
831 -0.340000 0.250000
832 -0.275000 0.250000
833 -0.210000 0.250000
834 -0.150000 0.250000
835 -0.085000 0.250000
836 -0.020000 0.250000
837 0.045000 0.250000
838 0.225000 0.250000
839 -0.340000 0.255000
840 -0.275000 0.255000
841 -0.215000 0.255000
842 -0.150000 0.255000
843 -0.085000 0.255000
844 -0.020000 0.255000
845 0.045000 0.255000
846 0.215000 0.255000
847 -0.335000 0.260000
848 -0.275000 0.260000
849 -0.210000 0.260000
850 -0.145000 0.260000
851 -0.085000 0.260000
852 -0.015000 0.260000
853 0.045000 0.260000
854 0.220000 0.260000
855 -0.315000 0.265000
856 -0.255000 0.265000
857 -0.190000 0.265000
858 -0.130000 0.265000
859 -0.055000 0.265000
860 0.010000 0.265000
861 0.070000 0.265000
862 0.280000 0.265000
863 -0.285000 0.270000
864 -0.220000 0.270000
865 -0.160000 0.270000
866 -0.095000 0.270000
867 -0.020000 0.270000
868 0.040000 0.270000
869 0.255000 0.270000
870 -0.300000 0.275000
871 -0.235000 0.275000
872 -0.170000 0.275000
873 -0.110000 0.275000
874 -0.030000 0.275000
875 0.030000 0.275000
876 0.245000 0.275000
877 -0.295000 0.280000
878 -0.230000 0.280000
879 -0.165000 0.280000
880 -0.105000 0.280000
881 -0.025000 0.280000
882 0.035000 0.280000
883 0.250000 0.280000
884 -0.270000 0.285000
885 -0.210000 0.285000
886 -0.145000 0.285000
887 -0.065000 0.285000
888 0.000000 0.285000
889 0.170000 0.285000
890 0.270000 0.285000
891 -0.235000 0.290000
892 -0.175000 0.290000
893 -0.085000 0.290000
894 -0.025000 0.290000
895 0.040000 0.290000
896 0.245000 0.290000
897 -0.250000 0.295000
898 -0.185000 0.295000
899 -0.090000 0.295000
900 -0.025000 0.295000
901 0.040000 0.295000
902 0.230000 0.295000
903 -0.235000 0.300000
904 -0.175000 0.300000
905 -0.065000 0.300000
906 -0.005000 0.300000
907 0.145000 0.300000
908 0.255000 0.300000
909 -0.190000 0.305000
910 -0.070000 0.305000
911 -0.010000 0.305000
912 0.135000 0.305000
913 0.245000 0.305000
914 -0.070000 0.310000
915 -0.005000 0.310000
916 0.130000 0.310000
917 -0.130000 0.315000
918 -0.065000 0.315000
919 -0.005000 0.315000
920 0.180000 0.315000
921 -0.140000 0.320000
922 -0.075000 0.320000
923 -0.015000 0.320000
924 0.105000 0.320000
925 -0.150000 0.325000
926 -0.090000 0.325000
927 -0.025000 0.325000
928 0.080000 0.325000
929 0.210000 0.325000
930 -0.110000 0.330000
931 -0.050000 0.330000
932 0.145000 0.330000
933 0.205000 0.330000
934 -0.125000 0.335000
935 -0.060000 0.335000
936 0.000000 0.335000
937 0.190000 0.335000
938 -0.155000 0.340000
939 -0.090000 0.340000
940 -0.030000 0.340000
941 0.150000 0.340000
942 -0.190000 0.345000
943 -0.130000 0.345000
944 -0.065000 0.345000
945 -0.005000 0.345000
946 0.160000 0.345000
947 -0.170000 0.350000
948 -0.110000 0.350000
949 -0.045000 0.350000
950 0.110000 0.350000
951 -0.200000 0.355000
952 -0.140000 0.355000
953 -0.075000 0.355000
954 0.070000 0.355000
955 0.130000 0.355000
956 -0.155000 0.360000
957 -0.095000 0.360000
958 -0.030000 0.360000
959 0.085000 0.360000
960 0.145000 0.360000
961 -0.115000 0.365000
962 -0.055000 0.365000
963 0.010000 0.365000
964 0.075000 0.365000
965 -0.160000 0.370000
966 -0.095000 0.370000
967 -0.035000 0.370000
968 0.030000 0.370000
969 0.090000 0.370000
970 -0.110000 0.375000
971 -0.045000 0.375000
972 0.015000 0.375000
973 0.080000 0.375000
974 -0.095000 0.380000
975 -0.030000 0.380000
976 0.035000 0.380000
977 -0.095000 0.385000
978 -0.030000 0.385000
979 0.030000 0.385000
980 -0.040000 0.390000
+1
View File
@@ -1457,6 +1457,7 @@ pub struct App {
/// Ocean work-surface state. Kept separate from transcript/sidebar state
/// so the replacement shell can be removed or promoted as one unit.
pub work_surface: crate::tui::work_surface::WorkSurfaceState,
pub pet_watch: crate::tui::pet_watch::PetWatch,
/// Goal sub-state.
pub goal: HostGoalState,
/// Session sub-state (cost, tokens, telemetry).
+1
View File
@@ -731,6 +731,7 @@ impl App {
startup_input_unproven: false,
},
viewport: ViewportState::default(),
pet_watch: crate::tui::pet_watch::PetWatch::default(),
work_surface: {
let mut state = crate::tui::work_surface::WorkSurfaceState::with_layout(
work_surface_placement,
+1
View File
@@ -16,6 +16,7 @@ pub(crate) mod agent_details;
pub(crate) mod agent_focus;
pub(crate) mod agent_roster;
pub mod ambient_life;
pub mod pet_watch;
pub mod app;
pub mod approval;
pub mod auto_review;
+7 -2
View File
@@ -243,7 +243,10 @@ impl OceanColumn {
#[must_use]
pub fn color_at_y(self, y: u16) -> Color {
let row = y.saturating_sub(self.top).min(self.height - 1);
if let Some(elapsed) = self.completion_elapsed_ms {
if let Some(elapsed) = self
.completion_elapsed_ms
.filter(|elapsed| *elapsed < COMPLETION_BREATH_MS)
{
self.ramp
.color_at_completion_context(row, self.height, elapsed, self.context_percent)
} else {
@@ -318,7 +321,9 @@ impl OceanColumn {
height: self.height,
phase_tag: self.phase_tag(),
animated: self.animated,
completion_active: self.completion_elapsed_ms.is_some(),
completion_active: self
.completion_elapsed_ms
.is_some_and(|elapsed| elapsed < COMPLETION_BREATH_MS),
presence: self.presence,
context_percent: self.context_percent,
}
+354
View File
@@ -0,0 +1,354 @@
//! Foreground Engine observation in the existing work surface. The one
//! Whalesong bucketer/world runs in an isolated, bounded QuickJS worker.
//! Rendering and audio policy belong to this host; telemetry semantics do not.
use std::time::{Duration, Instant};
use codewhale_localization::{MessageId, tr};
use codewhale_palette::{ChromeInk, chrome_style};
use ratatui::{Frame, layout::Rect, style::Modifier};
use serde_json::{Value, json};
use crate::core::events::Event;
use crate::core::protocol_parity::{ProtocolIds, event_to_protocol};
use crate::tui::ambient_life::pet_sim::ChannelId;
use crate::tui::app::{App, StatusToastLevel};
use crate::tui::underwater::ShellPhase;
use crate::tui::work_surface::RailPanel;
mod persistence;
mod worker;
use worker::{Command, Notice, Raster, Worker};
#[derive(Default)]
pub struct PetWatch {
worker: Option<Worker>,
session: Option<String>,
origin: Option<Instant>,
last_tick: Option<Instant>,
failed: bool,
exporting: bool,
pub(crate) area: Option<Rect>,
raster: Option<Raster>,
}
impl PetWatch {
pub fn export(&mut self) -> bool {
if self.worker.is_none() || self.session.is_none() || self.failed || self.exporting {
return false;
}
self.send(Command::Export);
self.exporting = !self.failed;
self.exporting
}
pub fn retry(&mut self) {
if self.failed {
*self = Self::default();
}
}
pub fn observe(&mut self, event: &Event, session: Option<&str>, now: Instant) {
if self.session.as_deref() != session {
*self = Self::default();
return;
}
if self.worker.is_none() {
return;
}
if let Some(json) = metadata(event) {
let time_ms = self.origin.map_or(0.0, |origin| {
now.duration_since(origin).as_secs_f64() * 1000.0
});
self.send(Command::Observe { json, time_ms });
}
}
fn send(&mut self, command: Command) {
if self
.worker
.as_ref()
.is_some_and(|worker| worker.tx.try_send(command).is_err())
{
self.failed = true;
self.worker = None;
self.raster = None;
}
}
}
/// Existing protocol projection defines the variant names. This allowlist
/// removes payloads before the bounded worker queue; no model text escapes.
fn metadata(event: &Event) -> Option<String> {
if !matches!(
event,
Event::TurnStarted { .. }
| Event::TurnComplete { .. }
| Event::MessageStarted { .. }
| Event::MessageDelta { .. }
| Event::MessageComplete { .. }
| Event::ThinkingStarted { .. }
| Event::ThinkingDelta { .. }
| Event::ThinkingComplete { .. }
| Event::ToolCallStarted { .. }
| Event::ToolCallHeartbeat
| Event::ToolCallComplete { .. }
| Event::AgentSpawned { .. }
| Event::AgentProgress { .. }
| Event::AgentComplete { .. }
| Event::ApprovalRequired { .. }
| Event::UserInputRequired { .. }
| Event::Error { .. }
) {
return None;
}
let ids = ProtocolIds {
thread_id: "foreground".to_owned().into(),
session_id: "foreground".to_owned().into(),
};
let projected = serde_json::to_value(event_to_protocol(event, &ids)).ok()?;
let mut out = serde_json::Map::new();
for key in [
"event",
"index",
"channel",
"tool_call_id",
"tool_name",
"id",
"worker_status",
] {
if let Some(value) = projected.get(key) {
out.insert(key.to_owned(), value.clone());
}
}
if let Some(status) = projected.pointer("/activity/worker_status") {
out.insert("worker_status".into(), status.clone());
}
let failed = projected.get("status") == Some(&json!("failed"))
|| projected.get("worker_status") == Some(&json!("failed"))
|| projected.pointer("/activity/worker_status") == Some(&json!("failed"))
|| projected.pointer("/result/outcome") == Some(&json!("err"))
|| projected.pointer("/result/success") == Some(&Value::Bool(false));
if failed {
out.insert("failed".into(), Value::Bool(true));
}
let json = serde_json::to_string(&out).ok()?;
// Producer data is bounded before crossing the queue, including tool names.
(json.len() <= 16_384).then_some(json)
}
/// Called by the existing terminal event loop. No renderer reads the clock or
/// asks for a second draw loop. Still mode updates facts at bucket cadence.
pub fn tick(app: &mut App, now: Instant, obscured: bool) {
let visible = app.work_surface.panel == RailPanel::Watch
&& app.work_surface.last_area.is_some()
&& !app.work_surface.dismissed
&& !obscured;
let phase = ShellPhase::from_app(app);
let motion = visible
&& matches!(phase, ShellPhase::Working | ShellPhase::Verifying)
&& crate::tui::underwater::decorative_shell_motion_enabled(app);
let waiting = matches!(phase, ShellPhase::Waiting | ShellPhase::Approval);
let state = &mut app.pet_watch;
if state.session != app.current_session_id {
*state = PetWatch {
session: app.current_session_id.clone(),
..PetWatch::default()
};
}
if visible && state.worker.is_none() && !state.failed {
state.origin = Some(now);
match Worker::start(state.session.clone()) {
Ok(worker) => {
state.worker = Some(worker);
state.origin = Some(now);
state.last_tick = None;
}
Err(_) => state.failed = true,
}
}
let update = state
.worker
.as_ref()
.and_then(|worker| worker.latest.lock().ok().and_then(|mut slot| slot.take()));
match update {
Some(Ok(raster)) => {
if visible
&& state
.raster
.as_ref()
.is_none_or(|old| !old.same_picture(&raster))
{
app.needs_redraw = true;
}
state.raster = Some(raster);
}
Some(Err(())) => {
state.worker = None;
state.raster = None;
state.failed = true;
}
None => {}
}
// A slow or stalled worker cannot keep asserting its old observed frame.
// The UI's clock checks freshness; the renderer never reads wall time.
if state.raster.as_ref().is_some_and(|raster| {
state.origin.is_some_and(|origin| {
now.duration_since(origin).as_secs_f64() * 1000.0 - raster.host_time_ms > 800.0
})
}) {
state.raster = None;
if visible {
app.needs_redraw = true;
}
}
if state.worker.is_some()
&& state.last_tick.is_none_or(|last| {
now.duration_since(last) >= Duration::from_millis(if motion { 33 } else { 400 })
})
{
let elapsed = state.origin.map_or(0.0, |origin| {
now.duration_since(origin).as_secs_f64() * 1000.0
});
let area = state.area.unwrap_or(Rect::new(0, 0, 40, 8));
state.send(Command::Advance {
time_ms: elapsed,
motion,
waiting,
width: area.width.clamp(1, 512),
height: area.height.saturating_sub(1).clamp(1, 512),
});
state.last_tick = Some(now);
}
if state.failed && state.origin.take().is_some() {
app.push_status_toast(
tr(app.ui_locale, MessageId::PetWatchUnavailable).into_owned(),
StatusToastLevel::Warning,
Some(8_000),
);
app.needs_redraw = true;
}
let notices: Vec<_> = app
.pet_watch
.worker
.as_ref()
.map(|worker| worker.notices.try_iter().collect())
.unwrap_or_default();
for notice in notices {
let retain_receipt = matches!(&notice, Notice::Exported(_) | Notice::StorageUnavailable);
let (text, level) = match notice {
Notice::Restored => (
tr(app.ui_locale, MessageId::PetWatchRestored).into_owned(),
StatusToastLevel::Info,
),
Notice::StorageUnavailable => (
tr(app.ui_locale, MessageId::PetWatchStorageUnavailable)
.replace("{command}", "/workbar watch export"),
StatusToastLevel::Warning,
),
Notice::Exported(path) => {
app.pet_watch.exporting = false;
(
tr(app.ui_locale, MessageId::PetWatchExported)
.replace("{path}", &path.display().to_string()),
StatusToastLevel::Info,
)
}
Notice::ExportFailed => {
app.pet_watch.exporting = false;
(
tr(app.ui_locale, MessageId::PetWatchExportFailed).into_owned(),
StatusToastLevel::Warning,
)
}
};
if retain_receipt {
// Footer budgets can shorten notices. Explicit file receipts and
// recovery instructions must also remain readable in the transcript.
app.add_message(crate::tui::history::HistoryCell::System {
content: text.clone(),
});
}
app.push_status_toast(text, level, Some(12_000));
app.needs_redraw = true;
}
}
pub fn render(frame: &mut Frame, area: Rect, app: &mut App) {
app.pet_watch.area = Some(area);
let raster = app.pet_watch.raster.as_ref();
let hollow = raster.is_none_or(|r| r.hollow);
let channel = raster
.and_then(|r| ChannelId::from_key(&r.channel))
.unwrap_or(ChannelId::Other);
let arch = raster.map_or("drift", |r| r.arch.as_str());
let label = format!(
"{channel} · {arch}{}",
if hollow {
format!(" · {}", tr(app.ui_locale, MessageId::PetUnobserved))
} else if raster.is_some_and(|r| r.dozing) {
format!(" · {}", tr(app.ui_locale, MessageId::PetDozing))
} else {
String::new()
}
);
let ink = if hollow {
ChromeInk::Metadata
} else {
match channel {
ChannelId::Error => ChromeInk::Failure,
ChannelId::Human => ChromeInk::Waiting,
_ => ChromeInk::Active,
}
};
let mut style = chrome_style(&app.ui_theme, ink);
if raster.is_some_and(|r| r.lit < 0.5) {
style = style.add_modifier(Modifier::DIM);
}
let grid =
raster.filter(|r| r.width == area.width && r.height == area.height.saturating_sub(1));
crate::tui::ambient_life::pet_widget::render_grid(
area,
frame.buffer_mut(),
grid.map_or(&[], |r| r.cells.as_slice()),
&label,
style,
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn foreground_projection_keeps_lifecycle_and_excludes_private_payloads() {
let call = metadata(&Event::ToolCallStarted {
id: "call-a".into(),
name: "exec_command".into(),
input: json!({"command":"PRIVATE TOOL INPUT"}),
})
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&call).unwrap(),
json!({
"event":"tool_call_started", "tool_call_id":"call-a", "tool_name":"exec_command",
})
);
let thought = metadata(&Event::ThinkingDelta {
index: 2,
content: "PRIVATE REASONING".into(),
})
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&thought).unwrap(),
json!({"event":"response_delta","index":2,"channel":"reasoning"})
);
let message = metadata(&Event::MessageDelta {
index: 3,
content: "PRIVATE MESSAGE".into(),
})
.unwrap();
assert!(!message.contains("PRIVATE"));
assert!(!call.contains("PRIVATE"));
assert!(!thought.contains("PRIVATE"));
}
}
+177
View File
@@ -0,0 +1,177 @@
//! Session-owned pet sidecar, using the existing confined artifact I/O. A
//! writer lock plus content revision rejects concurrent or external edits.
use std::fs::File;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use crate::artifacts::{open_session_relative, write_session_relative_immutable};
use crate::fleet::files::{WorkspaceFile, same_file};
const MAX_BYTES: usize = 8 * 1024 * 1024;
const HABITAT: &str = "artifacts/pet/habitat.json";
pub struct Store {
data: WorkspaceFile,
lock: WorkspaceFile,
original_lock: File,
expected: Option<[u8; 32]>,
}
impl Store {
pub fn open(session: &str) -> io::Result<Self> {
let data = open_session_relative(session, Path::new(HABITAT), true)?;
let lock = open_session_relative(session, Path::new("artifacts/pet/habitat.lock"), true)?;
let original_lock = lock.open_update(true, false)?;
Ok(Self {
data,
lock,
original_lock,
expected: None,
})
}
fn with_lock<T>(&self, action: impl FnOnce() -> io::Result<T>) -> io::Result<T> {
let file = self.lock.open_update(false, false)?;
if !same_file(&file, &self.original_lock)? {
return Err(io::Error::other("Pet habitat lock was replaced"));
}
let mut lock = fd_lock::RwLock::new(file);
// Never wait behind another process in the world worker.
let _guard = lock.try_write()?;
action()
}
fn read(&self) -> io::Result<Option<Vec<u8>>> {
let file = match self.data.open_file() {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
let mut bytes = Vec::new();
file.take(MAX_BYTES as u64 + 1).read_to_end(&mut bytes)?;
if bytes.len() > MAX_BYTES {
return Err(io::Error::other("Pet habitat exceeds 8 MiB"));
}
Ok(Some(bytes))
}
pub fn load(&mut self) -> io::Result<Option<String>> {
let bytes = self.with_lock(|| self.read())?;
let text = bytes
.as_ref()
.map(|b| {
String::from_utf8(b.clone())
.map_err(|_| io::Error::other("Pet habitat is not UTF-8"))
})
.transpose()?;
self.expected = bytes.map(|b| Sha256::digest(b).into());
Ok(text)
}
pub fn save(&mut self, text: &str) -> io::Result<()> {
if text.len() > MAX_BYTES {
return Err(io::Error::other("Pet habitat exceeds 8 MiB"));
}
self.with_lock(|| {
let current = self.read()?.map(|b| <[u8; 32]>::from(Sha256::digest(b)));
if current != self.expected {
return Err(io::Error::other("Another writer changed the pet habitat"));
}
self.data.replace(text.as_bytes())
})?;
self.expected = Some(Sha256::digest(text.as_bytes()).into());
Ok(())
}
}
pub fn export(session: &str, text: &str) -> io::Result<PathBuf> {
if text.len() > MAX_BYTES {
return Err(io::Error::other("Pet replay exceeds 8 MiB"));
}
let relative = PathBuf::from(format!(
"artifacts/pet/replay-{}.json",
uuid::Uuid::new_v4()
));
write_session_relative_immutable(session, &relative, text.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
fn store(root: &Path) -> Store {
let data = WorkspaceFile::open(root, Path::new("habitat.json"), true).unwrap();
let lock = WorkspaceFile::open(root, Path::new("habitat.lock"), true).unwrap();
let original_lock = lock.open_update(true, false).unwrap();
Store {
data,
lock,
original_lock,
expected: None,
}
}
#[test]
fn stale_writer_cannot_replace_a_newer_recording_or_external_edit() {
let root = tempfile::tempdir().unwrap();
let mut first = store(root.path());
let mut second = store(root.path());
assert!(first.load().unwrap().is_none());
assert!(second.load().unwrap().is_none());
first.save("first recording").unwrap();
assert!(second.save("stale recording").is_err());
assert_eq!(
std::fs::read_to_string(root.path().join("habitat.json")).unwrap(),
"first recording"
);
std::fs::write(root.path().join("habitat.json"), "external edit").unwrap();
assert!(first.save("lost edit").is_err());
assert_eq!(first.load().unwrap().as_deref(), Some("external edit"));
}
#[test]
fn invalid_and_oversized_habitats_remain_intact() {
let root = tempfile::tempdir().unwrap();
let mut files = store(root.path());
let path = root.path().join("habitat.json");
std::fs::write(&path, [0xff]).unwrap();
assert!(files.load().is_err());
assert!(files.save("replacement").is_err());
assert_eq!(std::fs::read(&path).unwrap(), [0xff]);
std::fs::write(&path, vec![b' '; MAX_BYTES + 1]).unwrap();
assert!(files.load().is_err());
assert!(files.save("replacement").is_err());
assert_eq!(
std::fs::metadata(&path).unwrap().len(),
MAX_BYTES as u64 + 1
);
}
#[cfg(unix)]
#[test]
fn private_atomic_files_reject_links_and_replaced_locks() {
use std::os::unix::fs::{PermissionsExt, symlink};
let root = tempfile::tempdir().unwrap();
let mut files = store(root.path());
files.save("private").unwrap();
let path = root.path().join("habitat.json");
assert_eq!(
std::fs::metadata(&path).unwrap().permissions().mode() & 0o777,
0o600
);
let outside = root.path().join("outside");
std::fs::rename(&path, &outside).unwrap();
symlink(&outside, &path).unwrap();
assert!(files.load().is_err());
assert!(files.save("replacement").is_err());
assert_eq!(std::fs::read_to_string(&outside).unwrap(), "private");
std::fs::remove_file(&path).unwrap();
std::fs::hard_link(&outside, &path).unwrap();
assert!(files.load().is_err());
std::fs::remove_file(root.path().join("habitat.lock")).unwrap();
let _new_owner = store(root.path());
assert!(files.save("split lock").is_err());
}
}
File diff suppressed because it is too large. Load diff
+338
View File
@@ -0,0 +1,338 @@
//! Sandboxed, read-only execution of the generated Whalesong world. This reuses
//! the workspace's existing QuickJS dependency; no JS filesystem/network APIs,
//! second Engine, async runtime, Node installation or external process is needed.
use std::sync::{Arc, Mutex, mpsc};
use std::time::{Duration, Instant};
use rquickjs::{Context, Runtime};
use serde::Deserialize;
use super::persistence::{self, Store};
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Raster {
pub width: u16,
pub height: u16,
pub cells: Vec<u8>,
#[serde(rename = "timeMs")]
pub time_ms: f64,
/// Host-relative receipt clock, separate from the restored creature clock.
#[serde(skip)]
pub host_time_ms: f64,
pub channel: String,
pub arch: String,
pub hollow: bool,
pub dozing: bool,
pub lit: f64,
}
impl Raster {
/// A hidden clock advance is not a new picture in reduced motion.
pub fn same_picture(&self, other: &Self) -> bool {
self.width == other.width
&& self.height == other.height
&& self.cells == other.cells
&& self.channel == other.channel
&& self.arch == other.arch
&& self.hollow == other.hollow
&& self.dozing == other.dozing
&& self.lit == other.lit
}
}
pub enum Command {
Export,
Observe {
json: String,
time_ms: f64,
},
Advance {
time_ms: f64,
motion: bool,
waiting: bool,
width: u16,
height: u16,
},
}
pub enum Notice {
Restored,
StorageUnavailable,
Exported(std::path::PathBuf),
ExportFailed,
}
pub struct Worker {
pub tx: mpsc::SyncSender<Command>,
pub latest: Arc<Mutex<Option<Result<Raster, ()>>>>,
pub notices: mpsc::Receiver<Notice>,
}
impl Worker {
pub fn start(session: Option<String>) -> std::io::Result<Self> {
let (tx, rx) = mpsc::sync_channel(128);
let (notices_tx, notices) = mpsc::sync_channel(16);
let latest = Arc::new(Mutex::new(None));
let output = Arc::clone(&latest);
std::thread::Builder::new()
.name("pet-world".into())
.spawn(move || {
if run(rx, &output, &notices_tx, session.as_deref()).is_err()
&& let Ok(mut slot) = output.lock()
{
*slot = Some(Err(()));
}
})?;
Ok(Self {
tx,
latest,
notices,
})
}
}
fn run(
rx: mpsc::Receiver<Command>,
output: &Mutex<Option<Result<Raster, ()>>>,
notices: &mpsc::SyncSender<Notice>,
session: Option<&str>,
) -> Result<(), ()> {
let runtime = Runtime::new().map_err(|_| ())?;
runtime.set_memory_limit(64 * 1024 * 1024);
runtime.set_max_stack_size(2 * 1024 * 1024);
let deadline = Arc::new(Mutex::new(Instant::now() + Duration::from_secs(2)));
let check = Arc::clone(&deadline);
runtime.set_interrupt_handler(Some(Box::new(move || {
check.lock().map_or(true, |d| Instant::now() > *d)
})));
let context = Context::full(&runtime).map_err(|_| ())?;
context
.with(|ctx| -> rquickjs::Result<()> {
let points: Vec<Vec<f64>> = include_str!("../ambient_life/whale-points.tsv")
.lines()
.map(|line| {
line.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect()
})
.collect();
ctx.globals().set(
"points",
serde_json::to_string(&points).expect("finite points"),
)?;
ctx.eval::<(), _>(include_bytes!("pet-native.js").as_slice())?;
ctx.eval::<(), _>("globalThis.pet = new PetNative(points, '', '[]', true)")?;
Ok(())
})
.map_err(|_| ())?;
let mut store = None;
let mut offset_ms = 0.0;
if let Some(session) = session {
// Hydrating a bounded recording validates its whole accepted history.
// It runs off the UI thread and can take longer than a frame command.
*deadline.lock().map_err(|_| ())? = Instant::now() + Duration::from_secs(10);
let loaded = (|| -> Result<Store, ()> {
let mut files = Store::open(session).map_err(|_| ())?;
if let Some(saved) = files.load().map_err(|_| ())? {
offset_ms = context
.with(|ctx| -> rquickjs::Result<f64> {
ctx.globals().set("savedHabitat", saved)?;
ctx.eval("pet.restoreRecording(savedHabitat); delete globalThis.savedHabitat; pet.resumeEngine()")
})
.map_err(|_| ())?;
let _ = notices.try_send(Notice::Restored);
}
Ok(files)
})();
match loaded {
Ok(files) => store = Some(files),
Err(()) => {
// A damaged/concurrent file stays untouched; this run can still
// show unknown/live telemetry and export its own accepted tape.
context
.with(|ctx| {
ctx.eval::<(), _>("delete globalThis.savedHabitat; globalThis.pet = new PetNative(points, '', '[]', true)")
})
.map_err(|_| ())?;
offset_ms = 0.0;
let _ = notices.try_send(Notice::StorageUnavailable);
}
}
}
let mut saved_at = None;
for command in rx {
// A delayed host can ask for up to 300 fixed ticks (ten seconds).
// The worker remains interruptible without treating legitimate catch-up
// as a telemetry failure on a busy machine. The UI never waits here.
*deadline.lock().map_err(|_| ())? = Instant::now() + Duration::from_secs(5);
let save_at = match &command {
Command::Advance { time_ms, .. }
if saved_at.is_none_or(|last| time_ms - last >= 5_000.0) =>
{
Some(*time_ms)
}
_ => None,
};
context
.with(|ctx| -> rquickjs::Result<()> {
match command {
Command::Export => {
let json: String = ctx.eval("pet.recording()")?;
let result = session
.ok_or_else(|| std::io::Error::other("No saved session"))
.and_then(|id| persistence::export(id, &json));
let notice = match result {
Ok(path) => Notice::Exported(path),
Err(_) => Notice::ExportFailed,
};
let _ = notices.try_send(notice);
}
Command::Observe { json, time_ms } => {
ctx.globals().set("metadata", json)?;
ctx.globals().set("timeMs", time_ms + offset_ms)?;
ctx.eval::<(), _>("pet.observeEngine(metadata, timeMs)")?;
}
Command::Advance {
time_ms,
motion,
waiting,
width,
height,
} => {
ctx.globals().set("timeMs", time_ms + offset_ms)?;
ctx.globals().set("motion", motion)?;
ctx.globals().set("waiting", waiting)?;
ctx.globals().set("width", width)?;
ctx.globals().set("height", height)?;
let json: String = ctx.eval(
"pet.advanceEngine(timeMs,motion,waiting); pet.terminal(width,height)",
)?;
let mut frame: Raster =
serde_json::from_str(&json).map_err(|_| rquickjs::Error::Unknown)?;
if !frame.time_ms.is_finite() || frame.time_ms < offset_ms {
return Err(rquickjs::Error::Unknown);
}
frame.host_time_ms = time_ms;
if let Ok(mut slot) = output.lock() {
*slot = Some(Ok(frame));
}
}
}
Ok(())
})
.map_err(|_| ())?;
if let Some(at) = save_at {
save(&context, &mut store, notices)?;
saved_at = Some(at);
}
}
*deadline.lock().map_err(|_| ())? = Instant::now() + Duration::from_secs(5);
save(&context, &mut store, notices)?;
Ok(())
}
fn save(
context: &Context,
store: &mut Option<Store>,
notices: &mpsc::SyncSender<Notice>,
) -> Result<(), ()> {
if let Some(files) = store {
let text: String = context
.with(|ctx| ctx.eval("pet.recording(true)"))
.map_err(|_| ())?;
if files.save(&text).is_err() {
*store = None;
let _ = notices.try_send(Notice::StorageUnavailable);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
struct ArtifactRoot(Option<std::path::PathBuf>);
impl Drop for ArtifactRoot {
fn drop(&mut self) {
crate::artifacts::set_test_artifact_sessions_root(self.0.take());
}
}
fn frame(worker: &Worker, at: f64) -> Raster {
worker
.tx
.send(Command::Advance {
time_ms: at,
motion: false,
waiting: true,
width: 78,
height: 22,
})
.unwrap();
let until = Instant::now() + Duration::from_secs(10);
loop {
if let Some(value) = worker.latest.lock().unwrap().take() {
return value.expect("world worker failed");
}
assert!(Instant::now() < until, "worker timed out");
std::thread::sleep(Duration::from_millis(5));
}
}
fn finish(worker: Worker) {
let output = Arc::clone(&worker.latest);
drop(worker);
let until = Instant::now() + Duration::from_secs(10);
while Arc::strong_count(&output) > 1 {
assert!(Instant::now() < until, "worker did not finish saving");
std::thread::sleep(Duration::from_millis(5));
}
assert!(!matches!(*output.lock().unwrap(), Some(Err(()))));
}
#[test]
fn session_checkpoint_reopens_in_the_actual_worker_without_reviving_an_approval() {
let _guard = crate::artifacts::TEST_ARTIFACT_SESSIONS_GUARD
.lock()
.unwrap();
let root = tempfile::tempdir().unwrap();
let _restore = ArtifactRoot(crate::artifacts::set_test_artifact_sessions_root(Some(
root.path().to_owned(),
)));
let worker = Worker::start(Some("pet-worker-test".into())).unwrap();
worker
.tx
.send(Command::Observe {
json: r#"{"event":"approval_required","id":"old-approval"}"#.into(),
time_ms: 0.0,
})
.unwrap();
let before = frame(&worker, 5_000.0);
assert_eq!(before.channel, "human");
finish(worker);
let path = root
.path()
.join("pet-worker-test/artifacts/pet/habitat.json");
let saved: serde_json::Value =
serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
let worker = Worker::start(Some("pet-worker-test".into())).unwrap();
let after = frame(&worker, 800.0);
assert!(after.hollow);
assert!(after.time_ms > before.time_ms);
assert_eq!(after.host_time_ms, 800.0);
assert!(
worker
.notices
.try_iter()
.any(|n| matches!(n, Notice::Restored))
);
finish(worker);
let resumed: serde_json::Value =
serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
assert_eq!(
&resumed["tape"].as_array().unwrap()[..saved["tape"].as_array().unwrap().len()],
saved["tape"].as_array().unwrap()
);
}
}
+1
View File
@@ -180,6 +180,7 @@ impl InteractionTargetId {
pub const DOCK_TAB_CONTEXT: Self = Self("dock.tab.context");
pub const DOCK_TAB_GIT: Self = Self("dock.tab.git");
pub const DOCK_TAB_PRICE: Self = Self("dock.tab.price");
pub const DOCK_TAB_WATCH: Self = Self("dock.tab.watch");
pub const DOCK_CLOSE: Self = Self("dock.close");
}
+41
View File
@@ -1894,6 +1894,46 @@ pub(crate) async fn run_event_loop(
.upload_resync_snapshot(&resync_run, &app.api_messages);
}
}
let pet_event_applies = match &event {
EngineEvent::AgentSpawned {
owner_session_id, ..
}
| EngineEvent::AgentProgress {
owner_session_id, ..
}
| EngineEvent::AgentComplete {
owner_session_id, ..
} => event_owner_is_active(app.current_session_id.as_deref(), owner_session_id),
EngineEvent::UserInputRequired { .. } => {
!should_suppress_user_input_prompt(app)
}
EngineEvent::ApprovalRequired {
tool_name,
approval_grouping_key,
approval_key,
approval_force_prompt,
..
} => {
matches!(
resolve_ui_approval_disposition(
app,
tool_name,
approval_grouping_key,
approval_key,
*approval_force_prompt
),
crate::core::authority::ApprovalRequestDisposition::Prompt
)
}
_ => true,
};
if pet_event_applies {
app.pet_watch.observe(
&event,
app.current_session_id.as_deref(),
Instant::now(),
);
}
record_turn_activity(app, &event, Instant::now());
match event {
EngineEvent::MessageStarted { .. } => {
@@ -4174,6 +4214,7 @@ pub(crate) async fn run_event_loop(
}
maybe_throttled_recovery_snapshot(app, Instant::now(), &mut last_recovery_snapshot_at);
let history_has_live_motion = history_has_live_motion(&app.history);
crate::tui::pet_watch::tick(app, Instant::now(), event_broker.is_paused());
let active_cell_has_live_motion = active_cell_has_live_motion(app);
let translation_placeholder_has_live_motion = app.translation_enabled
&& (pending_thinking_translations > 0 || app.streaming_thinking_active_entry.is_some());
+3 -1
View File
@@ -112,7 +112,9 @@ pub(crate) fn handle_bracketed_paste(app: &mut App, text: &str) {
} else if !app.view_stack.is_empty() {
// A non-consumed modal is open — don't leak paste into composer.
} else {
// Paste into main input.
// Main-input paste takes the same keyboard ownership as typed text.
// Otherwise the visible composer command's Enter stays with the dock.
crate::tui::work_surface::release_focus(app);
app.insert_paste_text(text);
}
}
+14
View File
@@ -721,6 +721,20 @@ fn focus_test_app() -> App {
app
}
#[test]
fn bracketed_paste_returns_watch_focus_to_the_visible_composer() {
let mut app = focus_test_app();
crate::tui::work_surface::select_dock_panel(
&mut app,
crate::tui::work_surface::RailPanel::Watch,
);
assert!(app.work_surface.focused);
handle_bracketed_paste(&mut app, "/workbar watch export");
assert_eq!(app.input, "/workbar watch export");
assert!(!app.work_surface.focused);
assert!(app.composer_enter_would_submit());
}
/// One representative terminal encoding per shell binding.
fn shell_binding_probe(id: ShellBindingId) -> KeyEvent {
match id {
+29 -2
View File
@@ -201,14 +201,14 @@ impl ChatWidget {
// The completion breath is authored decorative motion, so it rides the
// same motion gate as everything else in the water. Both the column's
// settle flourish and ambient life's presence read this one clock:
// presence needs the settle tail past the breath, the column does not.
// the pet needs the settle tail too; the column clips only its light pulse.
let completion_life_clock = (underwater_atmosphere
&& app.motion_policy().allows_decorative())
.then_some(())
.and(app.ocean_completion_started_at)
.map(|started| started.elapsed().as_millis());
let completion_elapsed_ms = completion_life_clock
.filter(|elapsed| *elapsed < crate::tui::ocean::COMPLETION_BREATH_MS);
.filter(|elapsed| *elapsed < crate::tui::ocean::COMPLETION_SETTLE_MS);
let completion_life_active = completion_life_clock
.is_some_and(|elapsed| elapsed < crate::tui::ocean::COMPLETION_SETTLE_MS);
let render_empty_state = should_render_empty_state(app);
@@ -7280,6 +7280,33 @@ mod tests {
);
}
#[test]
fn dot_whale_gets_the_motion_gated_completion_settle_clock() {
let mut app = create_test_app();
app.low_motion = false;
app.fancy_animations = true;
app.ocean_completion_started_at = Some(Instant::now() - std::time::Duration::from_millis(900));
let widget = ChatWidget::new(&mut app, Rect::new(0, 0, 100, 24));
let age = widget
.ocean_column
.and_then(|column| column.completion_elapsed_ms());
assert!(
age.is_some_and(|age| (800..1_400).contains(&age)),
"{age:?}"
);
assert!(widget.life_presence_fixed > 0 && widget.life_presence_fixed < 1_000);
app.low_motion = true;
let still = ChatWidget::new(&mut app, Rect::new(0, 0, 100, 24));
assert_eq!(
still
.ocean_column
.and_then(|column| column.completion_elapsed_ms()),
None
);
assert_eq!(still.life_presence_fixed, 0);
}
#[test]
fn reduced_and_still_modes_clear_the_one_shot_send_flash() {
for (low_motion, fancy_animations) in [(true, true), (false, false)] {
@@ -9,6 +9,9 @@ use crate::tui::views::ModalKind;
use super::model::{RailPanel, WorkRowId};
pub(crate) fn select_dock_panel(app: &mut App, panel: RailPanel) {
if panel == RailPanel::Watch {
app.pet_watch.retry();
}
app.work_surface.panel = panel;
app.work_surface.explicit_view = true;
app.work_surface.dismissed = false;
+10 -2
View File
@@ -73,11 +73,13 @@ pub enum RailPanel {
Git,
/// Session cost, per-agent cost, cache hit %, model rate.
Price,
/// The dot whale, observing the foreground Engine without starting work.
Watch,
}
impl RailPanel {
/// Cycle order — also the tab order in the dock strip.
pub const ORDER: [RailPanel; 8] = [
pub const ORDER: [RailPanel; 9] = [
Self::Tasks,
Self::Agents,
Self::Background,
@@ -86,6 +88,7 @@ impl RailPanel {
Self::Context,
Self::Git,
Self::Price,
Self::Watch,
];
/// Views the dock opens on its own when they have content and the user
@@ -131,6 +134,7 @@ impl RailPanel {
"context" | "session" => Self::Context,
"git" | "branch" => Self::Git,
"price" | "cost" => Self::Price,
"watch" => Self::Watch,
_ => Self::Tasks,
}
}
@@ -146,6 +150,7 @@ impl RailPanel {
Self::Context => "context",
Self::Git => "git",
Self::Price => "price",
Self::Watch => "watch",
}
}
@@ -166,6 +171,7 @@ impl RailPanel {
Self::Context => "CONTEXT",
Self::Git => "GIT",
Self::Price => "PRICE",
Self::Watch => "WATCH",
}
}
}
@@ -870,7 +876,8 @@ fn view_has_work(app: &mut App, panel: RailPanel) -> bool {
| RailPanel::Notepad
| RailPanel::Context
| RailPanel::Git
| RailPanel::Price => false,
| RailPanel::Price
| RailPanel::Watch => false,
}
}
@@ -993,6 +1000,7 @@ pub(super) fn visible_rows_for(app: &mut App, panel: RailPanel) -> Vec<WorkRow>
RailPanel::Context => super::views::context_rows(app),
RailPanel::Git => super::views::git_rows(app),
RailPanel::Price => super::views::price_rows(app),
RailPanel::Watch => Vec::new(),
}
}
@@ -83,6 +83,15 @@ pub fn height(app: &mut App, width: u16, terminal_height: u16, rail_budget: u16)
let strip = app.work_surface.effective_placement.is_strip();
let goal_rows = u16::from(strip && top_goal_title(app).is_some());
let explicit = app.work_surface.explicit_view;
if explicit && app.work_surface.panel == RailPanel::Watch && strip {
// The selected tank uses the same resize/terminal/transcript budget.
let cap = top_cap(app, terminal_height, rail_budget);
if cap < 3 {
collapse_strip(app);
return 0;
}
return cap;
}
if rows.is_empty() && !explicit {
// A live goal alone still deserves a strip: title + divider.
if goal_rows == 0 {
+26 -5
View File
@@ -94,6 +94,21 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) {
}
super::model::resolve_view(app);
if app.work_surface.panel == RailPanel::Watch {
Block::default()
.style(Style::default().bg(app.ui_theme.surface_bg))
.render(area, frame.buffer_mut());
render_dock_tabs(frame, area, app);
register_dock_targets(app);
crate::tui::pet_watch::render(frame, body_area, app);
render_divider(frame, area, placement, app);
app.work_surface.last_area = Some(area);
app.work_surface.hitboxes.clear();
app.work_surface.latest_rows.clear();
app.work_surface.visible_rows = 0;
app.work_surface.total_rows = 0;
return;
}
let rows = visible_rows_for_panel(app);
let todo_ordinals = if placement.is_strip() {
todo_ordinals(&rows)
@@ -471,6 +486,7 @@ fn empty_view_hint(panel: RailPanel) -> &'static str {
RailPanel::Context => "context budget unknown",
RailPanel::Git => "not a git repository",
RailPanel::Price => "no priced turns yet",
RailPanel::Watch => "",
}
}
@@ -590,10 +606,10 @@ fn render_divider(frame: &mut Frame, area: Rect, placement: WorkSurfacePlacement
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
struct DockTab {
target: DockTabTarget,
label: &'static str,
label: std::borrow::Cow<'static, str>,
count: usize,
}
@@ -607,7 +623,11 @@ fn render_dock_tabs(frame: &mut Frame, area: Rect, app: &mut App) {
if useful || panel == app.work_surface.panel {
entries.push(DockTab {
target: DockTabTarget::Panel(panel),
label: panel.title(),
label: if panel == RailPanel::Watch {
codewhale_localization::tr(app.ui_locale, MessageId::PetWatchTitle)
} else {
panel.title().into()
},
count: count.unwrap_or(0),
});
}
@@ -622,7 +642,7 @@ fn render_dock_tabs(frame: &mut Frame, area: Rect, app: &mut App) {
let fits = |tabs: &[DockTab], counts: bool| {
tabs.iter()
.map(|tab| {
UnicodeWidthStr::width(tab.label)
UnicodeWidthStr::width(tab.label.as_ref())
+ if counts && tab.count > 0 {
1 + tab.count.to_string().len()
} else {
@@ -746,7 +766,7 @@ fn dock_tab_count(app: &mut App, panel: RailPanel) -> Option<usize> {
),
RailPanel::Files => Some(super::views::files_touched_count(app)),
RailPanel::Notepad => Some(usize::from(super::views::notepad_has_text(app))),
RailPanel::Context | RailPanel::Git | RailPanel::Price => None,
RailPanel::Context | RailPanel::Git | RailPanel::Price | RailPanel::Watch => None,
}
}
@@ -765,6 +785,7 @@ fn register_dock_targets(app: &mut App) {
RailPanel::Context => Id::DOCK_TAB_CONTEXT,
RailPanel::Git => Id::DOCK_TAB_GIT,
RailPanel::Price => Id::DOCK_TAB_PRICE,
RailPanel::Watch => Id::DOCK_TAB_WATCH,
};
(
id,
+1 -1
View File
@@ -14,7 +14,7 @@ use super::model::{RailPanel, WorkRow, WorkRowId, WorkTone};
pub(super) const fn view_always_has_content(panel: RailPanel) -> bool {
matches!(
panel,
RailPanel::Context | RailPanel::Git | RailPanel::Price
RailPanel::Context | RailPanel::Git | RailPanel::Price | RailPanel::Watch
)
}
+146
View File
@@ -0,0 +1,146 @@
# Dot-whale habitat and Watch
The underwater transcript uses the portable 980-point whale for its completion
visitor. The whale is the home form of a persistent field of dots. The intended
product lets those dots reorganize to express actual work, beyond this first
set of authored gaits. This slice
replaces the old `≈≈>` / spray / fluke glyph sequence. The explicit Watch panel
adds foreground telemetry through the same world and score used by native and
web hosts. Rust audio output remains unfinished.
## Ownership
- `crates/tui/src/tui/ambient_life/pet_sim.rs` owns the dependency-free Rust port
of the TypeScript reference core, with typed `ChannelId` and `Archetype`.
- `whale-points.tsv` is the embedded authored body. `PetSim::whale()` constructs
it using the existing `mulberry32(0xC0FFEE)` particle stream.
- `pet_widget.rs` owns particle-to-braille rendering. The portable Rust runner
and ratatui demo use these same product files, rather than maintaining copies.
- `pet_cameo.rs` is the completion presentation inside the existing habitat.
It consumes `WhaleCameo` and `AmbientActivity`; it never reads a wall clock,
schedules a frame, or classifies model/tool text.
## Completion is a bounded observation
A completion does not identify which Whalesong category was last active. The
single visitor therefore uses `other · drift`, with observed coverage. Existing
`AmbientActivity::Subagents` presents three fixed, staggered peer slots, each
labelled `agent · pod`. At rest the three peers remain three animals; the core's
six-satellite working gait is not multiplied into eighteen animals.
The complete creature and its caption are preflighted against `is_open_water`.
A text collision with either withholds the entire creature. Off-screen peers
are withheld instead of clamping them into an overlapping stack. On a 40-column
surface the middle peer remains visible. Content always owns its cells.
The core is stepped at fixed 30 Hz into two bounded raster caches. A frame is
selected solely from the supplied completion age, so skipped draws, rewind,
resizes, and rendering another session do not alter its pose. There are no new
random choices. The existing theme supplies the habitat ink; the cameo adds no
status-bar colour meaning and never spends Failure red.
The shell already owns an 800 ms completion light pulse and a 600 ms life fade.
It now passes the motion-gated clock through that full 1.4-second interval. The
ocean still clips its light pulse at 800 ms. The low-level cameo retains the
2.4-second safety limit from its predecessor; the shell's life fade ends first.
Reduced motion exits before constructing marks or initializing the caches.
The generic widget retains the core's unknown rendering and the literal
`· unobserved` cue. If the whole cue cannot fit, it withholds the whole widget.
Sleep dimming does not set unknown coverage.
## Evidence boundaries
### Foreground Watch
`/workbar watch` selects `RailPanel::Watch`. Existing dock cycling, tab hitboxes,
focus, dismissal, placement and resizing apply. The tank uses the work surface's
available body; it never overlays transcript cells. Narrow views keep the text
cue. Selecting Watch starts a read-only observer; it does not start an Engine
turn or silently substitute demonstration data. It begins with unknown coverage
and does not backfill events from before attachment.
`ui/event_loop.rs` passes accepted foreground Engine events through the existing
`core::protocol_parity::event_to_protocol` projection. The pet allowlist keeps
only lifecycle kind, stable ids, tool names, channel and outcome. Prompt text,
reasoning, arguments, results, paths and routing credentials never enter the
worker queue or pet tape. No session context or KV-cache prefix is changed.
The generated `pet_watch/pet-native.js` comes from the canonical source in `pet/src/core`:
```sh
npm --prefix pet ci --ignore-scripts
npm --prefix pet run sync
```
The Engine adapter emits normalized event-v1 observations, then uses the existing
`compilePetTelemetry` function. A sealed 400 ms observation bucket appears at
the next world boundary; the world's accepted tape remains the replay authority.
Liveness pulses do not count as repeated tool calls. Silence does not stretch
occupancy across gaps. Error receipts create a current onset, rather than moving
a failure back to the operation's start. Typed `ShellPhase::Waiting/Approval`
can extend an already witnessed human request, preserving the needs ladder.
The workspace's existing QuickJS dependency runs the actual world and score on
one lazy worker thread: 64 MiB memory, 128 queued commands, at most ten seconds
of fixed-tick catch-up and a five-second execution ceiling per command (ten seconds for initial restore). It has
no host filesystem, network, provider or execution APIs. Queue loss or runtime
failure withholds the old frame and emits a localized warning toast; reopening
Watch retries. The UI event loop remains the only draw authority. Session changes
drop the previous observer. Reduced motion and attention holds suppress gait
animation; settled shell phases remain still. New facts can still change the
caption and pose. Chrome and body ink use existing palette roles; Failure ink
requires an actual error channel.
The portable Rust particle core is exported as `codewhale_tui::pet`, including
its deterministic conformance helpers. This makes its existing external
renderer contract explicit instead of hiding unused public fields behind a
dead-code exemption.
Watch saves its accepted tape and a versioned world checkpoint together under
the existing session artifact directory, `artifacts/pet/habitat.json`. It saves
every five seconds and attempts a final save when its command channel closes.
A process crash can lose work since the last successful checkpoint. A fresh
session without an ID remains in memory until it is saved by the session owner.
Reopening Watch in the same saved session restores the exact particle state,
seeded behavior stream, pod identities, interaction cursor and score cursor.
It then advances to the first unrecorded interval, which is explicitly unknown.
Historical approvals and tool spans cannot establish present activity. The
restored creature clock and the host's frame-freshness clock remain separate.
The sidecar reuses confined session artifact I/O, private atomic writes and an
advisory writer lock. A content revision rejects stale writers and external
edits. Invalid files, changed locks, unavailable storage and the 8 MiB native
habitat limit stop persistence with a localized warning; existing files remain
intact and the current in-memory recording can still be exported within that
limit. Full-tape retention is still bounded by the 64 MiB worker and the core's
24-hour recording limit; history rotation remains future work.
`/workbar watch export` writes a new immutable `artifacts/pet/replay-<id>.json`
and reports its path through the existing toast system and transcript. Import it in the web
habitat to replay the accepted telemetry and interactions. Export is available
after opening Watch in a saved session. TUI audio output remains unfinished.
The live Runtime SSE adapter and this foreground protocol adapter consume
different existing input contracts; they share event-v1 projection semantics,
the bucketer, world and score. The Runtime importer separately pairs request
lifecycles and preserves failure receipt times. Its local SSE/CLI and browser
fixtures pass; authenticated real-session attachment remains unverified. The
source and liveness contracts are documented in `pet/README.md`.
The full TUI binary builds offline, its foreground metadata projection test
passes, and the real PTY walkthrough covers Watch selection, keyboard, mouse
and resizing down to 40x12. Receipts and source hashes are in
`CW/artifacts/pet-review-20260912/`. The full product unit suite was not run.
Local render fixtures exercise the actual ocean, glyph and ambient-life modules
with literal shell enums extracted from `underwater.rs`; they do not run an
Engine session. The portable conformance runner compares TypeScript, Rust and
Swift across the original tape and an additional all-channel/error-burst/sleep
tape, each with motion enabled and disabled. A missing tool or failed widget
build now fails that runner instead of printing a false success.
Full product builds, real session acceptance, packaged applications, audio,
mobile builds and hosted CI remain separate proof requirements. The local
fixture and conformance checks do not establish those outcomes.
+13
View File
@@ -67,3 +67,16 @@ caller-assigned `worker_specs` because worker controls address IDs globally.
Older runtimes that do not expose one of these endpoints produce a
`RuntimeCapabilityError` with a stable capability string instead of a generic
fetch failure.
## Read a thread journal
`threadEvents(threadId, { sinceSeq, replayLimit, signal })` reads the existing
`GET /v1/threads/{id}/events` SSE endpoint. It never creates a thread or starts
a turn. Pass an `AbortSignal` to close the subscription. Redirects are refused,
and incomplete or oversized frames fail instead of producing partial records.
The returned `seq` and `previous_seq` belong to Runtime. Keep the last accepted
`seq` for reconnects; sequence numbers need not be consecutive. Consumers should
validate the selected thread and predecessor cursor before advancing their own
read position. Authentication uses the client constructor's existing `token`
option and stays in the Authorization header.
+19
View File
@@ -299,6 +299,24 @@ export interface RuntimeClientOptions {
fetch?: typeof fetch;
}
export interface ThreadRuntimeEvent {
schema_version?: number;
seq: number;
previous_seq?: number;
event: string;
thread_id: string;
turn_id?: string | null;
item_id?: string | null;
timestamp: string;
payload: Record<string, unknown>;
}
export interface ThreadEventOptions {
sinceSeq?: number;
replayLimit?: number;
signal?: AbortSignal;
}
export class RuntimeApiError extends Error {
status?: number;
method?: string;
@@ -327,6 +345,7 @@ export class CodeWhaleRuntimeClient {
runId: FleetRunId,
options?: FleetEventOptions & { path?: string },
): AsyncIterable<FleetStreamEvent>;
threadEvents(threadId: string, options?: ThreadEventOptions): AsyncIterable<ThreadRuntimeEvent>;
}
export function createRuntimeClient(options?: RuntimeClientOptions): CodeWhaleRuntimeClient;
+27 -2
View File
@@ -124,6 +124,26 @@ export class CodeWhaleRuntimeClient {
}
}
/** Read the existing durable thread journal. This never starts a turn. */
async *threadEvents(threadId, options = {}) {
const query = new URLSearchParams();
for (const [key, value] of [["since_seq", options.sinceSeq], ["replay_limit", options.replayLimit]]) {
if (value === undefined) continue;
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${key} must be a nonnegative safe integer`);
query.set(key, String(value));
}
const path = `/v1/threads/${segment(threadId)}/events?${query}`;
const response = await this.#rawRequest(path, {
method: "GET", capability: "thread_event_stream", accept: "text/event-stream",
signal: options.signal, redirect: "error",
});
if (!response.body || !/^text\/event-stream(?:;|$)/i.test(response.headers.get("content-type") ?? "")) {
await response.body?.cancel();
throw new RuntimeApiError("Runtime thread response is not an event stream", { method: "GET", path });
}
yield* parseEventStream(response.body, { maxFrameChars: 2 * 1024 * 1024, requireBoundary: true });
}
async #jsonRequest(path, options = {}) {
const response = await this.#rawRequest(path, options);
if (response.status === 204) {
@@ -140,6 +160,8 @@ export class CodeWhaleRuntimeClient {
headers.set("authorization", `Bearer ${this.token}`);
}
const init = { method, headers };
if (options.signal) init.signal = options.signal;
if (options.redirect) init.redirect = options.redirect;
if (options.body !== undefined) {
headers.set("content-type", "application/json");
init.body = JSON.stringify(options.body);
@@ -205,13 +227,14 @@ async function readErrorBody(response) {
}
}
async function* parseEventStream(body) {
const decoder = new TextDecoder();
async function* parseEventStream(body, { maxFrameChars = Infinity, requireBoundary = false } = {}) {
const decoder = new TextDecoder("utf-8", { fatal: requireBoundary });
let buffer = "";
for await (const chunk of body) {
buffer += decoder.decode(chunk, { stream: true });
let boundary;
while ((boundary = eventStreamBoundary(buffer)) !== null) {
if (boundary.index > maxFrameChars) throw new Error("Runtime event frame exceeds the size limit");
const frame = buffer.slice(0, boundary.index);
buffer = buffer.slice(boundary.index + boundary.length);
const event = parseSseFrame(frame);
@@ -219,8 +242,10 @@ async function* parseEventStream(body) {
yield event;
}
}
if (buffer.length > maxFrameChars) throw new Error("Runtime event frame exceeds the size limit");
}
buffer += decoder.decode();
if (requireBoundary && buffer.trim()) throw new Error("Runtime event stream ended inside a frame");
const event = parseSseFrame(buffer);
if (event !== undefined) {
yield event;
@@ -0,0 +1,38 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { CodeWhaleRuntimeClient } from '../index.js';
function clientFor(chunks, inspect = () => {}) {
return new CodeWhaleRuntimeClient({ token: 'fixture-token', fetch: async (url, init) => {
inspect(url, init);
return new Response(new ReadableStream({ start(controller) {
for (const chunk of chunks) controller.enqueue(new TextEncoder().encode(chunk));
controller.close();
} }), { headers: { 'content-type': 'text/event-stream; charset=utf-8' } });
} });
}
const collect = async stream => { const out = []; for await (const value of stream) out.push(value); return out; };
test('thread journal reads preserve the Runtime cursor, GET, cancellation and redirect boundary', async () => {
const signal = new AbortController().signal;
const record = { seq: 17, previous_seq: 9, event: 'item.completed', thread_id: 't/a', timestamp: '2026-09-12T00:00:00Z', payload: {} };
const raw = `: keepalive\r\n\r\ndata: ${JSON.stringify(record)}\r\n\r\n`;
const client = clientFor([...raw], (url, init) => {
assert.equal(url.pathname, '/v1/threads/t%2Fa/events');
assert.equal(url.searchParams.get('since_seq'), '9'); assert.equal(url.searchParams.get('replay_limit'), '100');
assert.equal(init.method, 'GET'); assert.equal(init.redirect, 'error'); assert.equal(init.signal, signal);
assert.equal(init.headers.get('authorization'), 'Bearer fixture-token');
});
assert.deepEqual(await collect(client.threadEvents('t/a', { sinceSeq: 9, replayLimit: 100, signal })), [record]);
});
test('thread stream refuses incomplete, oversized and invalid frames without emitting a partial event', async () => {
await assert.rejects(collect(clientFor(['data: {"seq":1}']).threadEvents('t')), /inside a frame/);
await assert.rejects(collect(clientFor(['data: ' + 'x'.repeat(2 * 1024 * 1024)]).threadEvents('t')), /size limit/);
await assert.rejects(collect(clientFor(['data: not-json\n\n']).threadEvents('t')), SyntaxError);
});
test('thread stream rejects invalid cursor arguments and a JSON response', async () => {
for (const sinceSeq of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1])
await assert.rejects(collect(clientFor([]).threadEvents('t', { sinceSeq })), /safe integer/);
const client = new CodeWhaleRuntimeClient({ fetch: async () => new Response('{}', { headers: { 'content-type': 'application/json' } }) });
await assert.rejects(collect(client.threadEvents('t')), /not an event stream/);
});
+21
View File
@@ -0,0 +1,21 @@
node_modules/
dist/
world/
conformance-results/
shots/
digests-*.txt
**/*.app/
**/build/
**/target/
**/*.jar
rs/petsim
swift/petsim
swift/world-conformance
macos/CodewhalePet
macos/Info.plist
*.log
*.jsonl
!ios/Resources/demo.jsonl
!**/fixtures/*.jsonl
ios/CodewhalePet.xcodeproj/
.DS_Store
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+6
View File
@@ -0,0 +1,6 @@
Whalesong
Copyright 2026 Whalesong contributors
Agent observability as multivariate temporal signal processing.
All bundled example traces, models, providers, workloads, usage, and cost values
are synthetic. Whalesong is independent and vendor-neutral.
+3
View File
@@ -0,0 +1,3 @@
# Portable pet
The source authority, build commands, replay contract and current limits are in [README.md](README.md). The reproducible review packet is [QA.md](QA.md).
+2
View File
@@ -0,0 +1,2 @@
// Compatibility entry point for the portable conformance viewer.
export * from './dist/core/pet-sim.js';
+84
View File
@@ -0,0 +1,84 @@
# Pet review packet
Review the exact commit checked out (`git rev-parse HEAD`). Report the source SHA,
OS/toolchain, commands, actual nonzero pass/fail counts, and reproducible findings.
Synthetic fixtures prove local contracts, not provider or customer acceptance.
## Fast Linux or macOS review
```sh
npm --prefix pet ci --ignore-scripts
npm --prefix pet run check
npm --prefix pet run sync
git diff --exit-code -- crates/tui/src/tui/pet_watch/pet-native.js pet/ios/Resources
cargo fetch --locked --manifest-path pet/tui/Cargo.toml
./pet/verify.sh --no-swift
npm test
npm run check:web
```
The pet suite currently contains 33 tests: event occupancy/unknown coverage,
late failures, human request pairing, read-only local SSE reconnect/cursor
recovery, deterministic world/score/PCM, checkpoint integrity and continuation.
The standalone verifier compares 190 checkpoints across baseline and edge tapes,
each animated and still. Omission of Swift is explicit in its output.
`npm test` requires the root and web dependencies described by the product.
`check:web` fetches public GitHub release metadata and requires network access.
For the full terminal, start with one build job on memory-limited machines:
```sh
cargo build --locked -j 1 -p codewhale-tui --bin codewhale-tui
cargo test --locked -j 1 -p codewhale-tui --lib tui::pet_watch::
```
Do not rerun a full library test link that is already exhausting the machine.
Report build/resource blockers and continue independent source/browser QA.
Manually inspect `/workbar watch`, narrow layouts, paste/Enter focus, session
save/reopen, immutable `/workbar watch export`, and import that exported JSON in
the browser. Unknown must remain legible; stale approvals must not resurrect.
Use a disposable session and synthetic/loopback telemetry for this assignment.
## Apple and Kotlin
```sh
./pet/verify.sh
./pet/scripts/check-apple.sh
./pet/ios/build.sh
PET_KOTLIN_LIB=/path/to/gradle/lib ./pet/android/verify.sh
```
Apple requires macOS, Xcode command-line tools, and XcodeGen for iOS. The iOS
script generates and builds the Simulator project directly from the shared
source, without first building macOS. Open `pet/ios/CodewhalePet.xcodeproj` to run.
`check-apple.sh` compiles the actual host and runs eight checkpoint/storage/recovery
workflows, including a generated two-hour synthetic unknown recording. It writes
only disposable fixtures under ignored conformance results and temporary storage.
Kotlin 2.3.0 can instead be on PATH; its verifier builds only the particle core
and conformance runner. The Compose view and Android application remain unverified.
## Review direction
Codewhale is a whale living in code. The dots can reorganize to show the work;
the whale is its home form, not a permanent silhouette restriction. Avoid the
old framing of a needy creature turning toward its owner. Evaluate readable,
causal transformations, continuity of particle identity, accessibility and
honest uncertainty. The current seven gaits are a baseline, not the design ceiling.
## Local evidence before publication (2026-09-12)
- Packaged pet: 33 tests passed; existing Whalesong consumers: 306 passed after
canonical source relocation. These are overlapping suites, not additive coverage.
- Product Node gate: 66 package, 12 SDK and 446 web tests passed; production web
check subsequently passed with the GitHub release fetch available.
- TypeScript/Rust/Swift: 190 identical checkpoints before and after source relocation.
- Actual TUI: bounded worker, persistence/export/reopen/corruption and two-hour
continuation exercised in a PTY. Five persistence, eleven paste and nine artifact
guard tests passed in the product; full Rust unit suite was not run.
- Apple: eight checkpoint workflows passed; iOS Simulator exercised two-hour
restoration and visible corruption recovery without overwriting the damaged file.
- Previous deterministic Kotlin parity, browser workflows and synthetic audio
receipts exist locally; they do not establish Android app or listening quality.
Hosted CI and Grokbot findings are separate evidence. Their actual results belong
on the pull request. Open implementation gaps are listed in [README.md](README.md).
+122
View File
@@ -0,0 +1,122 @@
# Codewhale: a whale living in code
The dots are Codewhale's material. The whale is its recognizable home form;
real activity can reorganize that material into a living expression of the work.
This is an evolving audiovisual instrument, not a mascot that asks for attention.
The current implementation has a persistent 980-particle world, thirteen event
categories, seven authored gaits, deterministic audio, replay and checkpoints.
More expressive forms driven by work are the next design step; arbitrary-form
transformations are not implemented yet. Missing telemetry stays visibly unknown.
## Build from this repository
Requires Node 22.13 or later and Python 3 for the local static server.
No sibling repository, investor folder, credentials or provider call is needed.
```sh
npm --prefix pet ci --ignore-scripts
npm --prefix pet run check
npm --prefix pet start
```
Open <http://127.0.0.1:4632/pet.html>. Wild is a simulated creature; Event demo
is synthetic telemetry. Import trace / tape accepts event-v1, OTLP, existing
Codewhale session/Runtime journals and saved pet recordings. Sound requires an
explicit gesture. Still preserves a static semantic pose. Follow local tape
uses the browser's File System Access API; other browsers support import/export.
```sh
# Regenerate embedded QuickJS and Apple bundles from the same source.
npm --prefix pet run sync
# Full product terminal; use /workbar watch and /workbar watch export.
cargo build --locked -p codewhale-tui --bin codewhale-tui
# Small standalone braille renderer, without the full product link.
cargo fetch --locked --manifest-path pet/tui/Cargo.toml
cd pet
./verify.sh --no-swift
```
`./verify.sh` without the flag additionally requires Swift and compares all
three ports. The script fails for a missing tool or failed build; it does not
silently call an omitted platform verified. See [QA.md](QA.md) for native builds.
## One source of meaning
`src/core/pet-telemetry.ts` derives 400 ms buckets from normalized Whalesong
event-v1. Measured span occupancy selects the channel; onset counts, repeat
density, error receipts, human requests and agent identities carry other facts.
Container spans are excluded. Open-ended spans without observation coverage,
disconnects and missing intervals are unknown, not successful idle time.
`pet-world.ts` advances a 30 Hz creature clock and named seeded random streams.
It journals accepted telemetry, interactions, behaviors and persistent pod slots.
`pet-audio.ts` schedules voices from that same clock. Noise uses absolute sample
positions, so buffer partitioning cannot change the score. Audio mixing is a
host concern. `pet-native.ts` exposes this exact core to QuickJS/JavaScriptCore.
The TUI and Apple hosts do not carry their own event bucketers or score schedulers.
The Rust particle implementation lives in the product's
`crates/tui/src/tui/ambient_life/pet_sim.rs`; this package's runner imports it.
Swift and Kotlin particle ports must match its conformance digests. Generated
native bundles are committed so the product Rust build needs no Node compiler.
Run `npm run sync` after changing core source; the generated-byte check in CI
rejects a stale bundle. Local Whalesong consumers use aliases to these same
canonical files, not separately maintained source copies.
The original Whalesong importer, signal model, schema and browser storage code
are included because they are actual dependencies of the pet. Their original
Apache-2.0 [license](LICENSE) and [notice](NOTICE) are retained. Rust files imported
from the product retain that repository's license.
## Live recording
The adapter only reads the existing Runtime journal endpoint
`GET /v1/threads/{id}/events`. It never starts a turn.
```sh
cd pet
node scripts/pet.mjs --runtime=http://127.0.0.1:7878 --thread=THREAD_ID --output=pet.jsonl
node scripts/pet.mjs --input=trace.jsonl --output=other.pet.jsonl --watch
node scripts/pet.mjs --demo --output=demo.pet.jsonl
```
Choose an existing thread and an unused output path. Optional authentication
comes from `CODEWHALE_RUNTIME_TOKEN`; tokens are rejected in URLs. Only plain
HTTP loopback IP origins are accepted. Redirects, invalid envelopes and cursor
holes are rejected; reconnects resume from the last accepted Runtime cursor.
The recorder seals the preceding observation interval against a fixed clock.
It retains request lifetimes and counts a delayed error once at receipt time.
Raw prompts, arguments, results and tokens do not enter the pet recording.
The thin wire is JSONL, one flat version-1 `PetBucket` per line: PetState plus
`sequence`, `simTimeMs`, `durationMs`, thirteen-element `onsets` and `activeMs`,
`errors`, `agentIds` and `waiting`. Sequence starts at zero in 400 ms steps.
Saved replay JSON contains this accepted tape and the interaction journal;
a versioned checkpoint also contains particle, random-stream and score cursors.
The older TSV is a particle conformance tape and cannot preserve audio onsets.
macOS watches `~/.codewhale/pet-state`. iOS watches `pet-state` in Documents.
File replacement and source restart are handled by the existing host watchers.
Authenticated attachment to a real running session still needs acceptance QA.
## Persistence and current limits
The browser commits checkpoint and recording together with an optimistic
IndexedDB revision. A saved live source reopens as Replay until explicitly
reattached. TUI Watch saves under the owning session's `artifacts/pet/habitat.json`;
Apple hosts keep source-specific files in Application Support/CodewhalePet.
Native writes are private and atomic, use a writer lock and content revision,
and preserve corrupt files or external edits. Recovery is visible in the UI.
Modern checkpoints hydrate without replaying historical simulation. Live resume
starts unknown and drops old human requests. Apple legacy wild preferences
migrate only after a successful checkpoint save. Native autosave is every five
seconds, so a crash may lose work since the last successful save.
Native habitats are limited to 8 MiB; the QuickJS worker has a 64 MiB memory
limit. Two-hour restoration has been exercised, but serializing full long tapes
still costs seconds in QuickJS. History rotation and long-session performance
remain open. TUI audio output, Android Compose/app/audio, macOS popover inspection,
authenticated Runtime attachment, richer forms and final listening/power quality
are unfinished. This branch is a reviewable prototype, not a release candidate.
+70
View File
@@ -0,0 +1,70 @@
// CodewhalePet.kt — the Jetpack Compose renderer for the pet.
//
// The composable owns no simulation: the caller steps `sim` (e.g. from a
// `withFrameNanos` loop in a LaunchedEffect) and passes the `PetState` that
// produced the frame — the same contract as the ratatui widget and the
// SwiftUI view. One Canvas pass:
//
// * filled circle per particle, colour and alpha from sim.frame
// * hollow frames stroke the circles instead of filling them
// * the caption row carries the non-colour cue ("tool · strike · unobserved")
//
// Reduced motion: honour the system's animator-duration-scale / accessibility
// setting by stepping the sim with motion = false — same contract as every
// other port. TalkBack gets petContentDescription(), not "a whale animation".
//
// NOT COMPILE-VERIFIED: the Kotlin core passes offline conformance, but the
// required Compose dependencies are not available in the inspected local cache.
package codewhale.pet
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
@Composable
fun CodewhalePet(sim: PetSim, state: PetState, modifier: Modifier = Modifier) {
val f = sim.frame
val color = Color(f.r.toInt() and 0xff, f.g.toInt() and 0xff, f.b.toInt() and 0xff)
Column(modifier = modifier.semantics { contentDescription = petContentDescription(sim, state) }) {
Canvas(modifier = Modifier.weight(1f).fillMaxSize()) {
val lay = petLayout(size.width.toDouble(), size.height.toDouble(), state)
val d = lay.dot.toFloat()
for (q in sim.p) {
val px = (lay.ox + q.x * lay.scale * lay.flipX).toFloat()
val py = (lay.oy + q.y * lay.scale).toFloat()
if (f.hollow) {
drawCircle(color, radius = d / 2, center = Offset(px, py),
alpha = f.alpha.toFloat(), style = Stroke(width = 1f))
} else {
drawCircle(color, radius = d / 2, center = Offset(px, py),
alpha = f.alpha.toFloat())
}
}
}
Text(
"${f.channel} · ${f.arch}${if (f.hollow) " · unobserved" else ""}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
fun petContentDescription(sim: PetSim, state: PetState): String {
val f = sim.frame
return buildList {
add("Codewhale pet"); add(f.channel); add(f.arch)
if (f.hollow) add("unobserved")
if (state.lit < 0.5) add("dozing")
if (state.attention > 0.5) add("needs you")
}.joinToString(", ")
}
+40
View File
@@ -0,0 +1,40 @@
package codewhale.pet
import java.io.File
import kotlin.math.floor
import kotlin.math.min
import kotlin.math.roundToInt
fun petDigest(sim: PetSim): String {
val grid = IntArray(64 * 32)
for (p in sim.p) {
val x = floor((p.x + 0.66) / 1.32 * 64).toInt()
val y = floor((p.y + 0.66) / 1.32 * 32).toInt()
if (x in 0..63 && y in 0..31) grid[y * 64 + x] = min(255, grid[y * 64 + x] + 1)
}
var hash = 0xcbf29ce484222325UL.toLong()
fun mix(n: Int) { hash = (hash xor (n and 255).toLong()) * 0x100000001b3L }
for (n in grid) mix(n)
mix(sim.frame.r.roundToInt()); mix(sim.frame.g.roundToInt()); mix(sim.frame.b.roundToInt())
mix((sim.frame.alpha * 255).roundToInt()); mix(if (sim.frame.hollow) 1 else 0)
return hash.toULong().toString(16).padStart(16, '0')
}
fun main(args: Array<String>) {
require(args.isNotEmpty()) { "Usage: points.tsv [--reduced-motion]; tape on stdin" }
val points = File(args[0]).readLines().filter { it.isNotBlank() }.map { row ->
val c = row.split('\t').map { it.toDouble() }; require(c.size == 2); Pair(c[0], c[1])
}
val sim = PetSim(points); var frame = 0
generateSequence(::readlnOrNull).forEach { row ->
val c = row.split('\t')
if (c.size >= 10 && c[0] != "dt") {
val state = PetState(c[1].toDouble(), c[2].toDouble(), c[3].toDouble(), c[4], c[5].toDouble(),
c[6].toDouble(), c[7].toDouble(), c[8].toDouble(), c[9].toDouble())
sim.step(c[0].toDouble(), state, !args.contains("--reduced-motion"))
if (frame % 30 == 0) println("f${frame.toString().padStart(4, '0')} ${petDigest(sim)} ${state.channel}")
frame++
}
}
println("final ${petDigest(sim)}")
}
+298
View File
@@ -0,0 +1,298 @@
// PetSim.kt — the Codewhale pet core, Kotlin/Android port.
//
// A faithful port of PetSim.ts. Same 980-point body from whale-points.tsv,
// same mulberry32(0xC0FFEE) jitter, same gait field and spring integration,
// same colour/hollow/brightness encoding. Pure Kotlin + java.lang.Math —
// no Android APIs, so it runs on the JVM for tests and in the app for rendering.
//
// Compile-verified offline with the cached Kotlin 2.3.0 JVM compiler.
// android/verify.sh compares all four tapes/modes against TypeScript.
package codewhale.pet
import kotlin.math.*
data class PetState(
var activity: Double = 0.35,
var coherence: Double = 0.8,
var attention: Double = 0.0,
var channel: String = "reasoning",
var observed: Double = 1.0,
var roamX: Double = 0.0,
var roamY: Double = 0.0,
var flip: Double = 1.0,
var lit: Double = 1.0,
)
data class Channel(val key: String, val label: String, val r: Int, val g: Int, val b: Int,
val arch: String, val form: String)
val CHANNELS = listOf(
Channel("reasoning", "Model / reasoning", 0x73, 0xc9, 0xb5, "gyre", "gyre · rolling"),
Channel("tool", "Tool calls", 0x74, 0xaa, 0xdd, "strike", "strike · reaching"),
Channel("memory", "Memory / RAG", 0xb6, 0xa7, 0x7f, "gyre", "gyre · scanning"),
Channel("code", "Code execution", 0x9b, 0x9e, 0xd7, "strike", "strike · along the body"),
Channel("filesystem", "Filesystem", 0x92, 0xb9, 0xc9, "strike", "strike · fanning"),
Channel("network", "Network / API", 0xd3, 0xac, 0x74, "cross", "crossing · one way"),
Channel("browser", "Browser / computer", 0x9e, 0xa9, 0xdf, "cross", "crossing · a sweep"),
Channel("communication", "Agent messages", 0x83, 0xc5, 0xc9, "cross", "crossing · two ways"),
Channel("agent", "Subagent activity", 0xb0, 0x9a, 0xcb, "pod", "pod · peers"),
Channel("orchestration", "Orchestration", 0x6c, 0x87, 0x98, "pod", "pod · hub"),
Channel("error", "Errors / exceptions", 0xe7, 0x91, 0x86, "tear", "torn · irregular"),
Channel("human", "Human interaction", 0xc2, 0xb7, 0x87, "address", "it turns to face you"),
Channel("other", "Unclassified", 0x73, 0x84, 0x92, "drift", "drifting · unformed"),
)
val CHANNEL_INDEX = CHANNELS.mapIndexed { i, c -> c.key to i }.toMap()
fun archOf(key: String) = when (key) {
"reasoning", "memory" -> "gyre"
"tool", "code", "filesystem" -> "strike"
"network", "communication", "browser" -> "cross"
"agent", "orchestration" -> "pod"
"error" -> "tear"
"human" -> "address"
else -> "drift"
}
private val UNKNOWN_RGB = doubleArrayOf(115.0, 132.0, 146.0)
private val REST_RGB = doubleArrayOf(122.0, 214.0, 240.0)
private fun lerp(a: Double, b: Double, t: Double) = a + (b - a) * t
private fun clamp(v: Double, lo: Double = 0.0, hi: Double = 1.0) = min(hi, max(lo, v))
/** mulberry32 — the same 32-bit sequence as every other port. */
class Mulberry32(seed: Int) {
private var a = seed
fun next(): Double {
a += 0x6D2B79F5.toInt()
var t = a
t = (t xor t.ushr(15)) * (t or 1)
t = t xor (t + (t xor t.ushr(7)) * (t or 61))
return (t xor t.ushr(14)).toUInt().toDouble() / 4294967296.0
}
}
class Particle {
var x = 0.0; var y = 0.0; var vx = 0.0; var vy = 0.0
var s = 0.0; var jx = 0.0; var jy = 0.0
var pod = 0
var hx = 0.0; var hy = 0.0; var ang = 0.0; var rad = 0.0; var tail = 0.0
var tx = 0.0; var ty = 0.0
}
data class Frame(
var r: Double = 122.0, var g: Double = 214.0, var b: Double = 240.0,
var alpha: Double = 0.3, var hollow: Boolean = false,
var channel: String = "reasoning", var arch: String = "gyre", var work: Double = 0.0,
)
/** The gait field — a velocity field on the body, not a silhouette. */
private fun gaitTarget(q: Particle, t: Double, act: Double, coh: Double,
att: Double, key: String, work: Double): Pair<Double, Double> {
val omega = lerp(4.6, 5.2 + act * 2.8, work)
val breath = 1 + sin(t * 1.85) * lerp(0.048, 0.018, work)
val flex = sin(q.ang * 2.05 + t * omega) * lerp(0.042, 0.016 + act * 0.028, work) * (0.18 + 0.82 * q.tail)
var px = cos(q.ang + flex) * q.rad * breath
var py = sin(q.ang + flex) * q.rad * breath
px += sin(t * 0.33) * lerp(0.030, 0.014, work)
py += cos(t * 0.21) * lerp(0.018, 0.010, work)
if (work < 0.02) return px to py
var gx = px; var gy = py
when (archOf(key)) {
"gyre" -> if (key == "memory") {
val pulse = 1 + sin(t * (2.4 + act * 1.6) - q.rad * 11) * (0.15 + act * 0.10)
gx *= pulse; gy *= pulse
} else {
val roll = sin(t * (1.05 + act * 0.35)) * (0.48 + act * 0.32)
val c = cos(roll); val sn = sin(roll)
gx = px * c - py * sn * 0.88
gy = px * sn * 0.88 + py * c
}
"strike" -> if (key == "tool") {
val rate = 2.7 + act * 2.1
val lunge = max(0.0, sin(t * rate)).pow(2)
gx += lunge * 0.11
if (q.s > 0.60) {
val reach = max(0.0, sin(t * rate + q.pod * 0.92)).pow(4) * (0.30 + act * 0.24)
gx += cos(q.ang) * reach
gy += sin(q.ang) * reach
}
} else if (key == "code") {
val rate = 3.2 + act * 1.8
val wave = sin(t * rate - q.tail * 7.5)
val bump = 0.11 + act * 0.08
gx += cos(q.ang) * wave * bump
gy += sin(q.ang) * wave * bump * 1.2
gx += max(0.0, wave) * 0.07
} else {
val rate = 2.15 + act * 1.5
val side = (q.pod % 2) * 2 - 1
val w = max(0.0, sin(t * rate + q.pod * 0.72)).pow(2)
gx += w * 0.055
gy += side * w * (0.17 + act * 0.13)
}
"cross" -> if (key == "browser") {
val band = (t * (0.55 + act * 0.35)) % 1 * 1.28 - 0.64
val inBand = max(0.0, 1 - abs(q.hy - band) / 0.08)
gx += inBand * (0.24 + act * 0.10)
gy += inBand * 0.02
} else {
val two = key == "communication"
if (q.s < if (two) 0.44 else 0.32) {
val dir = if (two) (if (q.s < 0.22) 1.0 else -1.0) else 1.0
val u = (t * (0.38 + act * 0.36) + q.s * 5.2) % 1
val going = if (u < 0.5) u * 2 else 2 - u * 2
val e = going * going * (3 - 2 * going)
gx = lerp(q.hx, dir * 0.80, e)
gy = q.hy * (1 - e * 0.38) + sin(going * PI) * 0.11 * dir
}
}
"pod" -> {
val n = 6
val k = q.pod % n
val hub = key == "orchestration" && k == 0
val spread = 0.30 + act * 0.11
val orbit = t * (0.55 + act * 0.28)
if (hub) { gx = px * 0.70; gy = py * 0.70 }
else {
val slots = if (key == "orchestration") n - 1 else n
val a = (if (key == "orchestration") k - 1 else k) * (PI * 2 / slots) + orbit
val sc = 0.34
gx = q.hx * sc + cos(a) * spread * 1.28
gy = q.hy * sc + sin(a) * spread * 0.80
}
}
"tear" -> {
val side = if (q.hx + q.hy < 0) -1.0 else 1.0
gx += side * (0.24 + (1 - coh) * 0.16)
gy += side * 0.15
gx += sin(t * 11.4 + q.s * 40) * (0.045 + act * 0.05)
gy += cos(t * 9.2 + q.s * 31) * (0.040 + act * 0.045)
}
"address" -> {
val face = 0.90 + att * 0.08
val th = 0.70
val z = (q.s - 0.5) * 0.42
var ax = q.hx * cos(th) + z * sin(th)
var ay = q.hy
val disc = 0.48 * face
ax = lerp(ax, cos(q.ang) * min(0.36, q.rad + 0.06) * 0.95, disc)
ay = lerp(ay, sin(q.ang) * min(0.36, q.rad + 0.06) * 1.08, disc)
val grow = 1.20 + sin(t * 1.65) * 0.055
gx = ax * grow; gy = ay * grow
}
else -> {
val mill = 0.13 + (1 - coh) * 0.10
gx = q.hx * 0.52 + sin(t * 0.72 + q.jx) * mill
gy = q.hy * 0.52 + cos(t * 0.54 + q.jy) * mill
}
}
return lerp(px, gx, work) to lerp(py, gy, work)
}
private fun stillT(key: String) = when (key) {
"reasoning" -> 1.15; "memory" -> 0.42; "tool" -> 0.30; "code" -> 0.18
"filesystem" -> 0.48; "network" -> 0.72; "browser" -> 0.95
"communication" -> 0.58; "agent" -> 1.25; "orchestration" -> 0.85
"error" -> 0.35; "human" -> 0.05; "other" -> 0.90; else -> 0.4
}
class PetSim(points: List<Pair<Double, Double>>, seed: Int = 0xC0FFEE.toInt()) {
val p: List<Particle>
private var phase = 0.0
private var clock = 0.0
private var tear = 0.0
private var prev: Int
private val col = REST_RGB.copyOf()
private var cur: Int
var frame = Frame(); private set
init {
val rng = Mulberry32(seed)
p = points.mapIndexed { i, (hx, hy) ->
Particle().apply {
this.hx = hx; this.hy = hy
x = hx; y = hy; tx = hx; ty = hy
s = rng.next(); jx = rng.next() * 6.283; jy = rng.next() * 6.283
pod = i % 6
ang = atan2(hy, hx)
rad = hypot(hx, hy)
tail = clamp(((-hx - hy) * 0.5 + 0.22) / 0.62)
}
}
cur = CHANNEL_INDEX["reasoning"]!!
prev = cur
}
/** Advance the sim by dt seconds under `state`. Identical math to PetSim.ts. */
fun step(dt: Double, state: PetState, motion: Boolean = true, sensitivity: Double = 1.0) {
fun s(v: Double) = lerp(0.5, v, sensitivity)
val act = s(state.activity); val coh = s(state.coherence); val att = s(state.attention)
val seen = s(state.observed)
phase += dt * (0.18 + act * 0.55) * (if (motion) 1.0 else 0.0)
if (motion) clock += dt
CHANNEL_INDEX[state.channel]?.let { cur = it }
val shown = cur
val ch = CHANNELS[shown]
val work = clamp((act - 0.16) / 0.18)
val wander = lerp(0.32, 1.0, (1 - coh).pow(1.15))
if (shown != prev) {
if (shown == CHANNEL_INDEX["error"]) tear = 1.0
prev = shown
}
tear = if (motion) max(0.0, tear - dt * 1.6) else 0.0
val split = (1 - coh).pow(1.6) * 0.16 + tear * 0.10
val blur = (1 - coh).pow(1.45) * 0.22 + tear * 0.18
val pull = if (motion) 2.2 + coh * 5.2 else 18.0
val tGait = if (motion) clock else stillT(ch.key)
for (q in p) {
if (motion) {
q.jx += dt * (0.40 + act * 1.1)
q.jy += dt * (0.34 + act * 0.9)
}
val (gx, gy) = gaitTarget(q, tGait, act, coh, att, ch.key, work)
val podAng = q.pod * 1.047 + phase * 0.22
val tx = gx + sin(q.jx + q.s * 9) * blur * wander + cos(podAng) * split
val ty = gy + cos(q.jy + q.s * 7) * blur * wander + sin(podAng) * split * 0.55
q.tx = tx; q.ty = ty
if (!motion) { q.x = tx; q.y = ty; q.vx = 0.0; q.vy = 0.0; continue }
q.vx += (tx - q.x) * pull * dt
q.vy += (ty - q.y) * pull * dt
q.vx *= 0.90; q.vy *= 0.90
val speed = if (motion) 2.6 else 8.0
q.x += q.vx * dt * speed
q.y += q.vy * dt * speed
}
val want = if (work > 0.35)
doubleArrayOf(CHANNELS[shown].r.toDouble(), CHANNELS[shown].g.toDouble(), CHANNELS[shown].b.toDouble())
else REST_RGB
val k = if (motion) min(1.0, dt * 2.6) else 1.0
for (c in 0..2) col[c] += (lerp(UNKNOWN_RGB[c], want[c], seen) - col[c]) * k
val lit = clamp(state.lit)
val alpha = (0.22 + act * 0.10) * lerp(0.50, 1.0, coh) * lerp(0.55, 1.0, seen) * lerp(0.35, 1.0, lit)
frame = Frame(col[0], col[1], col[2],
alpha = min(0.92, alpha * 1.85),
hollow = seen < 0.92,
channel = ch.key, arch = ch.arch, work = work)
}
}
/** Body-space → renderer-space, same as PetSim.ts layout(). */
data class PetLayout(val scale: Double, val flipX: Double, val ox: Double, val oy: Double, val dot: Double)
fun petLayout(w: Double, h: Double, state: PetState): PetLayout {
val att = state.attention
return PetLayout(
scale = min(w * 0.52, h * 0.92) * (1 + att * 0.07),
flipX = state.flip,
ox = w / 2 + state.roamX * w * 0.30,
oy = h / 2 + state.roamY * h * 0.30 + h * att * 0.05,
dot = max(1.6, min(w, h) * 0.0092) * (1 + att * 0.18))
}
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
# Use a pinned Kotlin installation, or an explicitly selected cached Gradle library.
if command -v kotlinc >/dev/null 2>&1; then
kotlinc android/PetSim.kt android/Conformance.kt -include-runtime -d android/conformance.jar
kotlin_classpath=android/conformance.jar
elif [ -n "${PET_KOTLIN_LIB:-}" ]; then
java -cp "$PET_KOTLIN_LIB/*" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler -no-stdlib -no-reflect -classpath "$PET_KOTLIN_LIB/kotlin-stdlib-2.3.0.jar" -d android/conformance.jar android/PetSim.kt android/Conformance.kt
kotlin_classpath="android/conformance.jar:$PET_KOTLIN_LIB/kotlin-stdlib-2.3.0.jar"
else
printf '%s\n' 'Kotlin compiler missing: install Kotlin 2.3.0 or set PET_KOTLIN_LIB to its cached Gradle lib directory.' >&2
exit 1
fi
for tape in tape.tsv tapes/edge-cases.tsv; do
case "$tape" in tape.tsv) name=baseline ;; *) name=edge-cases ;; esac
for mode in animated reduced-motion; do
set --
if [ "$mode" = reduced-motion ]; then set -- --reduced-motion; fi
java -cp "$kotlin_classpath" codewhale.pet.ConformanceKt whale-points.tsv "$@" < "$tape" > "conformance-results/$name-$mode-kotlin.txt"
diff -u "conformance-results/$name-$mode-ts.txt" "conformance-results/$name-$mode-kotlin.txt"
printf 'PASS Kotlin %s %s\n' "$name" "$mode"
done
done
+27
View File
@@ -0,0 +1,27 @@
import SwiftUI
import AVFoundation
@main struct CodewhalePetMobileApp: App {
@StateObject private var host = PetHost(points: WHALE_POINTS)
@Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
VStack(alignment: .leading, spacing: 22) {
Text("CODEWHALE / WHALESONG").font(.caption.monospaced()).foregroundStyle(.secondary)
Text("A living field.").font(.largeTitle.weight(.medium))
Text("Activity sets the pace. Coherence holds the figure together. When it needs you, it turns toward you.").font(.subheadline).foregroundStyle(.secondary)
PetHabitatView(host: host).frame(minHeight: 300, maxHeight: .infinity)
Picker("World", selection: $host.source) { ForEach(PetSource.allCases) { Text($0.label).tag($0) } }.pickerStyle(.segmented)
Toggle("Still", isOn: $host.still)
Text(host.source == .live ? "Live input: pet-state in this app’s Documents folder." : "Hollow dots mean missing telemetry. Sleep is a separate dimmer.")
.font(.caption).foregroundStyle(.secondary)
}.padding(24).preferredColorScheme(.dark)
.onAppear {
host.setLiveFile(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("pet-state"))
do { try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default); try AVAudioSession.sharedInstance().setActive(true) }
catch { /* Visual world remains available; audio output reports start errors. */ }
}
.onChange(of: phase) { _, value in host.suspend(value != .active) }
}
}
}
+200
View File
@@ -0,0 +1,200 @@
{"version":1,"sequence":0,"simTimeMs":0,"durationMs":400,"activity":0.74,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[1,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":1,"simTimeMs":400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":2,"simTimeMs":800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":3,"simTimeMs":1200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":4,"simTimeMs":1600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":5,"simTimeMs":2000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":6,"simTimeMs":2400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":7,"simTimeMs":2800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":8,"simTimeMs":3200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":9,"simTimeMs":3600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":10,"simTimeMs":4000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":11,"simTimeMs":4400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":12,"simTimeMs":4800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":13,"simTimeMs":5200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":14,"simTimeMs":5600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":15,"simTimeMs":6000,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":16,"simTimeMs":6400,"durationMs":400,"activity":0.74,"coherence":0.92,"attention":0,"channel":"filesystem","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,1,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,400,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":17,"simTimeMs":6800,"durationMs":400,"activity":0.375,"coherence":0.92,"attention":0,"channel":"filesystem","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,100,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":18,"simTimeMs":7200,"durationMs":400,"activity":0.74,"coherence":0.92,"attention":0,"channel":"filesystem","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,1,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,400,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":19,"simTimeMs":7600,"durationMs":400,"activity":0.375,"coherence":0.92,"attention":0,"channel":"filesystem","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,100,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":20,"simTimeMs":8000,"durationMs":400,"activity":0.74,"coherence":0.92,"attention":0,"channel":"filesystem","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,1,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,400,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":21,"simTimeMs":8400,"durationMs":400,"activity":0.375,"coherence":0.92,"attention":0,"channel":"filesystem","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,100,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":22,"simTimeMs":8800,"durationMs":400,"activity":0.74,"coherence":0.92,"attention":0,"channel":"filesystem","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,1,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,400,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":23,"simTimeMs":9200,"durationMs":400,"activity":0.375,"coherence":0.92,"attention":0,"channel":"filesystem","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,100,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":24,"simTimeMs":9600,"durationMs":400,"activity":0.74,"coherence":0.92,"attention":0,"channel":"filesystem","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,1,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,400,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":25,"simTimeMs":10000,"durationMs":400,"activity":0.375,"coherence":0.92,"attention":0,"channel":"filesystem","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,100,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":26,"simTimeMs":10400,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":27,"simTimeMs":10800,"durationMs":400,"activity":0.55,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,1,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,200,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":28,"simTimeMs":11200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":29,"simTimeMs":11600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":30,"simTimeMs":12000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":31,"simTimeMs":12400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":32,"simTimeMs":12800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":33,"simTimeMs":13200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":34,"simTimeMs":13600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":35,"simTimeMs":14000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":36,"simTimeMs":14400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":37,"simTimeMs":14800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":38,"simTimeMs":15200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":39,"simTimeMs":15600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"code","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,400,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":40,"simTimeMs":16000,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":41,"simTimeMs":16400,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":42,"simTimeMs":16800,"durationMs":400,"activity":0.55,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[1,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":43,"simTimeMs":17200,"durationMs":400,"activity":0.9299999999999999,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[1,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[600,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer"],"waiting":false}
{"version":1,"sequence":44,"simTimeMs":17600,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[1,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1000,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":45,"simTimeMs":18000,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":46,"simTimeMs":18400,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":47,"simTimeMs":18800,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":48,"simTimeMs":19200,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":49,"simTimeMs":19600,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":50,"simTimeMs":20000,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":51,"simTimeMs":20400,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":52,"simTimeMs":20800,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":53,"simTimeMs":21200,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":54,"simTimeMs":21600,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":55,"simTimeMs":22000,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":56,"simTimeMs":22400,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":57,"simTimeMs":22800,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":58,"simTimeMs":23200,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":59,"simTimeMs":23600,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":60,"simTimeMs":24000,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":61,"simTimeMs":24400,"durationMs":400,"activity":1,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[1200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":62,"simTimeMs":24800,"durationMs":400,"activity":0.85,"coherence":0.92,"attention":0,"channel":"agent","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[600,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent","reviewer","tester"],"waiting":false}
{"version":1,"sequence":63,"simTimeMs":25200,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":64,"simTimeMs":25600,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":65,"simTimeMs":26000,"durationMs":400,"activity":0.74,"coherence":0.92,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,1,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,400,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":66,"simTimeMs":26400,"durationMs":400,"activity":0.5025,"coherence":0.92,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,1,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,150,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":67,"simTimeMs":26800,"durationMs":400,"activity":0.6125,"coherence":0.92,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,350,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":68,"simTimeMs":27200,"durationMs":400,"activity":0.55,"coherence":0.92,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,1,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,200,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":69,"simTimeMs":27600,"durationMs":400,"activity":0.5175000000000001,"coherence":0.92,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,250,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":70,"simTimeMs":28000,"durationMs":400,"activity":0.6449999999999999,"coherence":0.5885714285714286,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,1,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,300,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":71,"simTimeMs":28400,"durationMs":400,"activity":0.4225,"coherence":0.5885714285714286,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,150,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":72,"simTimeMs":28800,"durationMs":400,"activity":0.74,"coherence":0.5057142857142858,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,1,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,400,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":73,"simTimeMs":29200,"durationMs":400,"activity":0.5025,"coherence":0.42285714285714293,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,1,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,150,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":74,"simTimeMs":29600,"durationMs":400,"activity":0.6125,"coherence":0.3400000000000001,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,350,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":75,"simTimeMs":30000,"durationMs":400,"activity":0.55,"coherence":0.3400000000000001,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,1,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,200,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":76,"simTimeMs":30400,"durationMs":400,"activity":0.5175000000000001,"coherence":0.3400000000000001,"attention":0,"channel":"tool","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,250,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":77,"simTimeMs":30800,"durationMs":400,"activity":0.36000000000000004,"coherence":0.2325000000000001,"attention":0.45,"channel":"error","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,1,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":1,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":78,"simTimeMs":31200,"durationMs":400,"activity":0.36000000000000004,"coherence":0.288888888888889,"attention":0.45,"channel":"error","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,1,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":1,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":79,"simTimeMs":31600,"durationMs":400,"activity":0.36000000000000004,"coherence":0.334,"attention":0.45,"channel":"error","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,1,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":1,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":80,"simTimeMs":32000,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":81,"simTimeMs":32400,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":82,"simTimeMs":32800,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":83,"simTimeMs":33200,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":84,"simTimeMs":33600,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":85,"simTimeMs":34000,"durationMs":400,"activity":0.74,"coherence":0.550909090909091,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,1,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":86,"simTimeMs":34400,"durationMs":400,"activity":0.66,"coherence":0.550909090909091,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":87,"simTimeMs":34800,"durationMs":400,"activity":0.66,"coherence":0.550909090909091,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":88,"simTimeMs":35200,"durationMs":400,"activity":0.66,"coherence":0.550909090909091,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":89,"simTimeMs":35600,"durationMs":400,"activity":0.66,"coherence":0.550909090909091,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":90,"simTimeMs":36000,"durationMs":400,"activity":0.66,"coherence":0.550909090909091,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":91,"simTimeMs":36400,"durationMs":400,"activity":0.66,"coherence":0.550909090909091,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":92,"simTimeMs":36800,"durationMs":400,"activity":0.66,"coherence":0.550909090909091,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":93,"simTimeMs":37200,"durationMs":400,"activity":0.66,"coherence":0.550909090909091,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":94,"simTimeMs":37600,"durationMs":400,"activity":0.66,"coherence":0.550909090909091,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":95,"simTimeMs":38000,"durationMs":400,"activity":0.66,"coherence":0.5720000000000001,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":96,"simTimeMs":38400,"durationMs":400,"activity":0.66,"coherence":0.5977777777777779,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":97,"simTimeMs":38800,"durationMs":400,"activity":0.66,"coherence":0.5977777777777779,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":98,"simTimeMs":39200,"durationMs":400,"activity":0.66,"coherence":0.6300000000000001,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":99,"simTimeMs":39600,"durationMs":400,"activity":0.66,"coherence":0.6300000000000001,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":100,"simTimeMs":40000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":101,"simTimeMs":40400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":102,"simTimeMs":40800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":103,"simTimeMs":41200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":104,"simTimeMs":41600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":105,"simTimeMs":42000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":106,"simTimeMs":42400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":107,"simTimeMs":42800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":108,"simTimeMs":43200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":109,"simTimeMs":43600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":110,"simTimeMs":44000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":111,"simTimeMs":44400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":112,"simTimeMs":44800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":113,"simTimeMs":45200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":114,"simTimeMs":45600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":115,"simTimeMs":46000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":116,"simTimeMs":46400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":117,"simTimeMs":46800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":118,"simTimeMs":47200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":119,"simTimeMs":47600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":120,"simTimeMs":48000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":121,"simTimeMs":48400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":122,"simTimeMs":48800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":123,"simTimeMs":49200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":124,"simTimeMs":49600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":125,"simTimeMs":50000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":126,"simTimeMs":50400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":127,"simTimeMs":50800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":128,"simTimeMs":51200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":129,"simTimeMs":51600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":130,"simTimeMs":52000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":131,"simTimeMs":52400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":132,"simTimeMs":52800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":133,"simTimeMs":53200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":134,"simTimeMs":53600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":135,"simTimeMs":54000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":136,"simTimeMs":54400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":137,"simTimeMs":54800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":138,"simTimeMs":55200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":139,"simTimeMs":55600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":140,"simTimeMs":56000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":141,"simTimeMs":56400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":142,"simTimeMs":56800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":143,"simTimeMs":57200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":144,"simTimeMs":57600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":145,"simTimeMs":58000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":146,"simTimeMs":58400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":147,"simTimeMs":58800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":148,"simTimeMs":59200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":149,"simTimeMs":59600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":150,"simTimeMs":60000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":151,"simTimeMs":60400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":152,"simTimeMs":60800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":153,"simTimeMs":61200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":154,"simTimeMs":61600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":155,"simTimeMs":62000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":156,"simTimeMs":62400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,400,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":157,"simTimeMs":62800,"durationMs":400,"activity":0.55,"coherence":0.92,"attention":0.8,"channel":"human","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,1,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,200,0],"errors":0,"agentIds":["parent"],"waiting":true}
{"version":1,"sequence":158,"simTimeMs":63200,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":159,"simTimeMs":63600,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":160,"simTimeMs":64000,"durationMs":400,"activity":0.74,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,1,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":161,"simTimeMs":64400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":162,"simTimeMs":64800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":163,"simTimeMs":65200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":164,"simTimeMs":65600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":165,"simTimeMs":66000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":166,"simTimeMs":66400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":167,"simTimeMs":66800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":168,"simTimeMs":67200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":169,"simTimeMs":67600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":170,"simTimeMs":68000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":171,"simTimeMs":68400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,400,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":172,"simTimeMs":68800,"durationMs":400,"activity":0.47000000000000003,"coherence":0.92,"attention":0,"channel":"memory","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,200,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":173,"simTimeMs":69200,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":174,"simTimeMs":69600,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":175,"simTimeMs":70000,"durationMs":400,"activity":0.74,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[1,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":176,"simTimeMs":70400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":177,"simTimeMs":70800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":178,"simTimeMs":71200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":179,"simTimeMs":71600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":180,"simTimeMs":72000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":181,"simTimeMs":72400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":182,"simTimeMs":72800,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":183,"simTimeMs":73200,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":184,"simTimeMs":73600,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":185,"simTimeMs":74000,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":186,"simTimeMs":74400,"durationMs":400,"activity":0.66,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[400,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":187,"simTimeMs":74800,"durationMs":400,"activity":0.47000000000000003,"coherence":0.92,"attention":0,"channel":"reasoning","observed":1,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[200,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":["parent"],"waiting":false}
{"version":1,"sequence":188,"simTimeMs":75200,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":189,"simTimeMs":75600,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":190,"simTimeMs":76000,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":191,"simTimeMs":76400,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":192,"simTimeMs":76800,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":193,"simTimeMs":77200,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":194,"simTimeMs":77600,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":195,"simTimeMs":78000,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":196,"simTimeMs":78400,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":197,"simTimeMs":78800,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":198,"simTimeMs":79200,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
{"version":1,"sequence":199,"simTimeMs":79600,"durationMs":400,"activity":0.12,"coherence":0.25,"attention":0,"channel":"other","observed":0,"roamX":0,"roamY":0,"flip":1,"lit":1,"onsets":[0,0,0,0,0,0,0,0,0,0,0,0,0],"activeMs":[0,0,0,0,0,0,0,0,0,0,0,0,0],"errors":0,"agentIds":[],"waiting":false}
File diff suppressed because it is too large. Load diff
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")"
node ../scripts/build-pet.mjs --apple
xcodegen generate --spec project.yml
xcodebuild -project CodewhalePet.xcodeproj -scheme CodewhalePet -configuration Debug -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath build -disableAutomaticPackageResolution CODE_SIGNING_ALLOWED=NO build
+35
View File
@@ -0,0 +1,35 @@
name: CodewhalePet
options:
deploymentTarget:
iOS: "17.0"
settings:
base:
SWIFT_VERSION: "5.0"
CODE_SIGNING_ALLOWED: NO
ENABLE_USER_SCRIPT_SANDBOXING: YES
targets:
CodewhalePet:
type: application
platform: iOS
sources:
- path: CodewhalePetApp.swift
- path: ../swift/PetSim.swift
- path: ../swift/PetNativeCore.swift
- path: ../swift/PetHabitatStore.swift
- path: ../swift/PetAudioOutput.swift
- path: ../swift/PetHost.swift
- path: ../swift/CodewhalePetView.swift
- path: ../swift/PetHabitatView.swift
- path: ../macos/WhalePoints.swift
- path: Resources
buildPhase: resources
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: dev.shannonlabs.CodewhalePet.mobile
GENERATE_INFOPLIST_FILE: YES
INFOPLIST_KEY_CFBundleDisplayName: Codewhale
INFOPLIST_KEY_UILaunchScreen_Generation: YES
INFOPLIST_KEY_UIFileSharingEnabled: YES
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace: YES
INFOPLIST_KEY_UISupportedInterfaceOrientations: UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
TARGETED_DEVICE_FAMILY: "1,2"
+56
View File
@@ -0,0 +1,56 @@
import SwiftUI
import AppKit
import ServiceManagement
@main
struct CodewhalePetApp: App {
@StateObject private var host = PetHost(points: WHALE_POINTS)
@State private var login = SMAppService.mainApp.status == .enabled
@State private var notice = ""
var body: some Scene {
MenuBarExtra {
VStack(spacing: 12) {
HStack { Text("Codewhale").font(.headline); Spacer(); Text("A living field").font(.caption).foregroundStyle(.secondary) }
PetHabitatView(host: host).frame(height: 250)
Picker("World", selection: $host.source) { ForEach(PetSource.allCases) { Text($0.label).tag($0) } }.pickerStyle(.segmented)
HStack {
Toggle("Still", isOn: $host.still)
Toggle("Launch at login", isOn: $login).onChange(of: login) { _, value in
do { if value { try SMAppService.mainApp.register() } else { try SMAppService.mainApp.unregister() }; notice = "" }
catch { notice = error.localizedDescription; login = SMAppService.mainApp.status == .enabled }
}
}.font(.caption)
if host.source == .live { Text("Local source: ~/.codewhale/pet-state").font(.caption2).textSelection(.enabled) }
if !notice.isEmpty { Text(notice).font(.caption).foregroundStyle(.secondary) }
HStack {
Button("Save replay…") {
let panel = NSSavePanel(); panel.nameFieldStringValue = "codewhale-pet-replay.json"
if panel.runModal() == .OK, let url = panel.url {
do { try host.exportRecording(to: url); notice = "Replay saved" } catch { notice = error.localizedDescription }
}
}
Spacer(); Button("Quit") { host.suspend(true); NSApplication.shared.terminate(nil) }
}
}.padding(16).frame(width: 390)
} label: {
PetMenuIcon(host: host)
.onAppear { host.setLiveFile(URL(fileURLWithPath: NSHomeDirectory() + "/.codewhale/pet-state")) }
}.menuBarExtraStyle(.window)
}
}
private struct PetMenuIcon: View {
@ObservedObject var host: PetHost
var body: some View {
Canvas { ctx, size in
guard let core = host.core else { return }
let lay = petLayout(w: size.width, h: size.height, state: core.frame.state), frame = core.sim.frame
for q in core.sim.p {
let dot = Path(ellipseIn: CGRect(x: lay.ox + q.x * lay.scale * lay.flipX, y: lay.oy + q.y * lay.scale, width: 0.7, height: 0.7))
if frame.hollow { ctx.stroke(dot, with: .color(.primary.opacity(0.55)), lineWidth: 0.3) }
else { ctx.fill(dot, with: .color(.primary.opacity(0.8))) }
}
}.frame(width: 24, height: 24)
.accessibilityLabel(host.core.map { petA11yLabel(sim: $0.sim, state: $0.frame.state) } ?? "Codewhale habitat unavailable")
}
}
+200
View File
@@ -0,0 +1,200 @@
// Generated from whale-points.tsv — the same 980-point body every
// platform loads. Do not regenerate per-platform; this IS the whale.
let WHALE_POINTS: [(Double, Double)] = [
(-0.100000, -0.390000), (-0.040000, -0.390000), (-0.090000, -0.385000), (-0.030000, -0.385000), (-0.125000, -0.380000),
(-0.065000, -0.380000), (0.000000, -0.380000), (-0.135000, -0.375000), (-0.075000, -0.375000), (-0.010000, -0.375000),
(-0.185000, -0.370000), (-0.120000, -0.370000), (-0.055000, -0.370000), (0.005000, -0.370000), (-0.185000, -0.365000),
(-0.125000, -0.365000), (-0.060000, -0.365000), (0.000000, -0.365000), (-0.210000, -0.360000), (-0.145000, -0.360000),
(-0.085000, -0.360000), (-0.020000, -0.360000), (0.040000, -0.360000), (-0.175000, -0.355000), (-0.110000, -0.355000),
(-0.050000, -0.355000), (0.015000, -0.355000), (-0.215000, -0.350000), (-0.150000, -0.350000), (-0.085000, -0.350000),
(-0.025000, -0.350000), (0.040000, -0.350000), (-0.195000, -0.345000), (-0.130000, -0.345000), (-0.070000, -0.345000),
(-0.005000, -0.345000), (-0.235000, -0.340000), (-0.175000, -0.340000), (-0.110000, -0.340000), (-0.050000, -0.340000),
(0.015000, -0.340000), (-0.220000, -0.335000), (-0.160000, -0.335000), (-0.095000, -0.335000), (-0.035000, -0.335000),
(0.030000, -0.335000), (-0.220000, -0.330000), (-0.155000, -0.330000), (-0.090000, -0.330000), (-0.030000, -0.330000),
(0.035000, -0.330000), (-0.215000, -0.325000), (-0.150000, -0.325000), (-0.085000, -0.325000), (-0.025000, -0.325000),
(-0.270000, -0.320000), (-0.210000, -0.320000), (-0.145000, -0.320000), (-0.080000, -0.320000), (-0.020000, -0.320000),
(-0.265000, -0.315000), (-0.205000, -0.315000), (-0.140000, -0.315000), (-0.080000, -0.315000), (-0.015000, -0.315000),
(-0.260000, -0.310000), (-0.200000, -0.310000), (-0.135000, -0.310000), (-0.075000, -0.310000), (-0.010000, -0.310000),
(-0.250000, -0.305000), (-0.190000, -0.305000), (-0.125000, -0.305000), (-0.065000, -0.305000), (-0.305000, -0.300000),
(-0.240000, -0.300000), (-0.180000, -0.300000), (-0.115000, -0.300000), (-0.055000, -0.300000), (-0.285000, -0.295000),
(-0.225000, -0.295000), (-0.160000, -0.295000), (-0.095000, -0.295000), (-0.035000, -0.295000), (0.035000, -0.295000),
(-0.295000, -0.290000), (-0.230000, -0.290000), (-0.165000, -0.290000), (-0.105000, -0.290000), (-0.040000, -0.290000),
(0.020000, -0.290000), (0.085000, -0.290000), (-0.270000, -0.285000), (-0.205000, -0.285000), (-0.140000, -0.285000),
(-0.080000, -0.285000), (-0.015000, -0.285000), (0.045000, -0.285000), (-0.320000, -0.280000), (-0.255000, -0.280000),
(-0.195000, -0.280000), (-0.130000, -0.280000), (-0.070000, -0.280000), (-0.005000, -0.280000), (0.060000, -0.280000),
(-0.330000, -0.275000), (-0.265000, -0.275000), (-0.205000, -0.275000), (-0.140000, -0.275000), (-0.080000, -0.275000),
(-0.015000, -0.275000), (0.050000, -0.275000), (0.110000, -0.275000), (-0.295000, -0.270000), (-0.235000, -0.270000),
(-0.170000, -0.270000), (-0.105000, -0.270000), (-0.045000, -0.270000), (0.020000, -0.270000), (0.080000, -0.270000),
(0.145000, -0.270000), (-0.290000, -0.265000), (-0.225000, -0.265000), (-0.160000, -0.265000), (-0.100000, -0.265000),
(-0.035000, -0.265000), (0.025000, -0.265000), (0.090000, -0.265000), (0.155000, -0.265000), (-0.305000, -0.260000),
(-0.240000, -0.260000), (-0.180000, -0.260000), (-0.055000, -0.260000), (0.010000, -0.260000), (0.070000, -0.260000),
(0.135000, -0.260000), (-0.340000, -0.255000), (-0.275000, -0.255000), (-0.215000, -0.255000), (-0.150000, -0.255000),
(0.010000, -0.255000), (0.070000, -0.255000), (0.135000, -0.255000), (-0.350000, -0.250000), (-0.285000, -0.250000),
(-0.220000, -0.250000), (-0.160000, -0.250000), (0.030000, -0.250000), (0.090000, -0.250000), (0.155000, -0.250000),
(-0.325000, -0.245000), (-0.265000, -0.245000), (-0.200000, -0.245000), (0.005000, -0.245000), (0.070000, -0.245000),
(0.130000, -0.245000), (-0.355000, -0.240000), (-0.290000, -0.240000), (-0.230000, -0.240000), (0.000000, -0.240000),
(0.060000, -0.240000), (0.125000, -0.240000), (-0.360000, -0.235000), (-0.300000, -0.235000), (-0.235000, -0.235000),
(0.005000, -0.235000), (0.070000, -0.235000), (0.130000, -0.235000), (-0.350000, -0.230000), (-0.285000, -0.230000),
(-0.225000, -0.230000), (0.035000, -0.230000), (0.095000, -0.230000), (0.160000, -0.230000), (-0.320000, -0.225000),
(-0.260000, -0.225000), (0.010000, -0.225000), (0.070000, -0.225000), (0.135000, -0.225000), (-0.345000, -0.220000),
(-0.285000, -0.220000), (-0.220000, -0.220000), (0.060000, -0.220000), (0.125000, -0.220000), (-0.355000, -0.215000),
(-0.290000, -0.215000), (-0.225000, -0.215000), (0.065000, -0.215000), (0.130000, -0.215000), (-0.350000, -0.210000),
(-0.285000, -0.210000), (0.025000, -0.210000), (0.085000, -0.210000), (0.150000, -0.210000), (-0.330000, -0.205000),
(-0.265000, -0.205000), (0.060000, -0.205000), (0.120000, -0.205000), (-0.345000, -0.200000), (-0.285000, -0.200000),
(0.055000, -0.200000), (0.115000, -0.200000), (-0.345000, -0.195000), (-0.280000, -0.195000), (0.070000, -0.195000),
(-0.375000, -0.190000), (-0.315000, -0.190000), (-0.395000, -0.185000), (-0.330000, -0.185000), (-0.270000, -0.185000),
(-0.345000, -0.180000), (-0.285000, -0.180000), (-0.360000, -0.175000), (-0.300000, -0.175000), (-0.375000, -0.170000),
(-0.310000, -0.170000), (-0.390000, -0.165000), (-0.325000, -0.165000), (-0.400000, -0.160000), (-0.335000, -0.160000),
(-0.405000, -0.155000), (-0.345000, -0.155000), (-0.280000, -0.155000), (-0.355000, -0.150000), (-0.290000, -0.150000),
(-0.360000, -0.145000), (-0.300000, -0.145000), (-0.365000, -0.140000), (-0.305000, -0.140000), (-0.370000, -0.135000),
(-0.310000, -0.135000), (-0.375000, -0.130000), (-0.310000, -0.130000), (-0.380000, -0.125000), (-0.315000, -0.125000),
(-0.380000, -0.120000), (-0.315000, -0.120000), (-0.380000, -0.115000), (-0.320000, -0.115000), (-0.385000, -0.110000),
(-0.325000, -0.110000), (-0.385000, -0.105000), (-0.325000, -0.105000), (-0.385000, -0.100000), (-0.320000, -0.100000),
(-0.390000, -0.095000), (-0.325000, -0.095000), (-0.390000, -0.090000), (-0.325000, -0.090000), (-0.385000, -0.085000),
(-0.325000, -0.085000), (-0.385000, -0.080000), (-0.325000, -0.080000), (-0.385000, -0.075000), (-0.320000, -0.075000),
(-0.385000, -0.070000), (-0.320000, -0.070000), (-0.435000, -0.065000), (-0.370000, -0.065000), (0.255000, -0.065000),
(0.320000, -0.065000), (0.385000, -0.065000), (-0.390000, -0.060000), (-0.325000, -0.060000), (0.270000, -0.060000),
(0.335000, -0.060000), (0.400000, -0.060000), (-0.385000, -0.055000), (-0.320000, -0.055000), (0.250000, -0.055000),
(0.315000, -0.055000), (0.380000, -0.055000), (-0.415000, -0.050000), (-0.350000, -0.050000), (0.195000, -0.050000),
(0.260000, -0.050000), (0.320000, -0.050000), (0.385000, -0.050000), (-0.410000, -0.045000), (-0.350000, -0.045000),
(0.180000, -0.045000), (0.240000, -0.045000), (0.305000, -0.045000), (0.370000, -0.045000), (-0.440000, -0.040000),
(-0.375000, -0.040000), (-0.315000, -0.040000), (0.200000, -0.040000), (0.260000, -0.040000), (0.325000, -0.040000),
(0.390000, -0.040000), (-0.420000, -0.035000), (-0.355000, -0.035000), (0.135000, -0.035000), (0.200000, -0.035000),
(0.265000, -0.035000), (0.325000, -0.035000), (0.390000, -0.035000), (-0.425000, -0.030000), (-0.360000, -0.030000),
(0.120000, -0.030000), (0.180000, -0.030000), (0.245000, -0.030000), (0.305000, -0.030000), (0.370000, -0.030000),
(0.430000, -0.030000), (-0.380000, -0.025000), (-0.315000, -0.025000), (0.145000, -0.025000), (0.210000, -0.025000),
(0.270000, -0.025000), (0.335000, -0.025000), (0.400000, -0.025000), (-0.415000, -0.020000), (-0.350000, -0.020000),
(0.095000, -0.020000), (0.160000, -0.020000), (0.220000, -0.020000), (0.285000, -0.020000), (0.350000, -0.020000),
(0.410000, -0.020000), (-0.400000, -0.015000), (-0.340000, -0.015000), (0.095000, -0.015000), (0.160000, -0.015000),
(0.220000, -0.015000), (0.285000, -0.015000), (0.345000, -0.015000), (0.410000, -0.015000), (-0.405000, -0.010000),
(-0.345000, -0.010000), (0.075000, -0.010000), (0.135000, -0.010000), (0.200000, -0.010000), (0.260000, -0.010000),
(0.325000, -0.010000), (0.390000, -0.010000), (-0.430000, -0.005000), (-0.365000, -0.005000), (0.030000, -0.005000),
(0.095000, -0.005000), (0.160000, -0.005000), (0.220000, -0.005000), (0.285000, -0.005000), (0.345000, -0.005000),
(0.410000, -0.005000), (-0.400000, 0.000000), (-0.340000, 0.000000), (0.045000, 0.000000), (0.105000, 0.000000),
(0.170000, 0.000000), (0.230000, 0.000000), (0.295000, 0.000000), (0.360000, 0.000000), (0.420000, 0.000000),
(-0.390000, 0.005000), (-0.330000, 0.005000), (0.045000, 0.005000), (0.110000, 0.005000), (0.170000, 0.005000),
(0.235000, 0.005000), (0.295000, 0.005000), (0.360000, 0.005000), (0.420000, 0.005000), (-0.390000, 0.010000),
(-0.325000, 0.010000), (0.025000, 0.010000), (0.090000, 0.010000), (0.150000, 0.010000), (0.215000, 0.010000),
(0.280000, 0.010000), (0.340000, 0.010000), (0.405000, 0.010000), (-0.410000, 0.015000), (-0.345000, 0.015000),
(-0.005000, 0.015000), (0.055000, 0.015000), (0.120000, 0.015000), (0.180000, 0.015000), (0.245000, 0.015000),
(0.305000, 0.015000), (0.370000, 0.015000), (-0.440000, 0.020000), (-0.380000, 0.020000), (-0.315000, 0.020000),
(0.005000, 0.020000), (0.070000, 0.020000), (0.135000, 0.020000), (0.195000, 0.020000), (0.260000, 0.020000),
(0.320000, 0.020000), (0.385000, 0.020000), (-0.425000, 0.025000), (-0.365000, 0.025000), (-0.300000, 0.025000),
(0.005000, 0.025000), (0.070000, 0.025000), (0.130000, 0.025000), (0.195000, 0.025000), (0.260000, 0.025000),
(0.320000, 0.025000), (0.415000, 0.025000), (-0.400000, 0.030000), (-0.335000, 0.030000), (-0.040000, 0.030000),
(0.020000, 0.030000), (0.085000, 0.030000), (0.145000, 0.030000), (0.210000, 0.030000), (0.270000, 0.030000),
(0.335000, 0.030000), (-0.420000, 0.035000), (-0.360000, 0.035000), (-0.295000, 0.035000), (-0.025000, 0.035000),
(0.040000, 0.035000), (0.105000, 0.035000), (0.165000, 0.035000), (0.230000, 0.035000), (0.290000, 0.035000),
(0.430000, 0.035000), (-0.375000, 0.040000), (-0.315000, 0.040000), (-0.060000, 0.040000), (0.000000, 0.040000),
(0.065000, 0.040000), (0.125000, 0.040000), (0.190000, 0.040000), (0.255000, 0.040000), (0.405000, 0.040000),
(-0.400000, 0.045000), (-0.340000, 0.045000), (-0.100000, 0.045000), (-0.035000, 0.045000), (0.025000, 0.045000),
(0.090000, 0.045000), (0.150000, 0.045000), (0.215000, 0.045000), (0.275000, 0.045000), (-0.430000, 0.050000),
(-0.365000, 0.050000), (-0.305000, 0.050000), (-0.090000, 0.050000), (-0.030000, 0.050000), (0.035000, 0.050000),
(0.100000, 0.050000), (0.160000, 0.050000), (0.225000, 0.050000), (0.285000, 0.050000), (-0.405000, 0.055000),
(-0.340000, 0.055000), (-0.280000, 0.055000), (-0.085000, 0.055000), (-0.025000, 0.055000), (0.040000, 0.055000),
(0.100000, 0.055000), (0.165000, 0.055000), (0.230000, 0.055000), (0.405000, 0.055000), (-0.395000, 0.060000),
(-0.335000, 0.060000), (-0.270000, 0.060000), (-0.110000, 0.060000), (-0.050000, 0.060000), (0.015000, 0.060000),
(0.075000, 0.060000), (0.140000, 0.060000), (0.205000, 0.060000), (0.265000, 0.060000), (-0.410000, 0.065000),
(-0.350000, 0.065000), (-0.285000, 0.065000), (-0.180000, 0.065000), (-0.115000, 0.065000), (-0.050000, 0.065000),
(0.010000, 0.065000), (0.075000, 0.065000), (0.135000, 0.065000), (0.200000, 0.065000), (0.265000, 0.065000),
(-0.410000, 0.070000), (-0.345000, 0.070000), (-0.285000, 0.070000), (-0.220000, 0.070000), (-0.160000, 0.070000),
(-0.095000, 0.070000), (-0.030000, 0.070000), (0.030000, 0.070000), (0.095000, 0.070000), (0.155000, 0.070000),
(0.220000, 0.070000), (0.420000, 0.070000), (-0.375000, 0.075000), (-0.310000, 0.075000), (-0.250000, 0.075000),
(-0.185000, 0.075000), (-0.120000, 0.075000), (-0.060000, 0.075000), (0.005000, 0.075000), (0.065000, 0.075000),
(0.130000, 0.075000), (0.190000, 0.075000), (0.395000, 0.075000), (-0.395000, 0.080000), (-0.335000, 0.080000),
(-0.270000, 0.080000), (-0.210000, 0.080000), (-0.145000, 0.080000), (-0.080000, 0.080000), (-0.020000, 0.080000),
(0.045000, 0.080000), (0.105000, 0.080000), (0.170000, 0.080000), (0.230000, 0.080000), (-0.415000, 0.085000),
(-0.350000, 0.085000), (-0.290000, 0.085000), (-0.225000, 0.085000), (-0.165000, 0.085000), (-0.100000, 0.085000),
(-0.035000, 0.085000), (0.025000, 0.085000), (0.090000, 0.085000), (0.150000, 0.085000), (0.215000, 0.085000),
(-0.425000, 0.090000), (-0.365000, 0.090000), (-0.300000, 0.090000), (-0.240000, 0.090000), (-0.175000, 0.090000),
(-0.115000, 0.090000), (-0.050000, 0.090000), (0.015000, 0.090000), (0.075000, 0.090000), (0.140000, 0.090000),
(0.200000, 0.090000), (0.410000, 0.090000), (-0.375000, 0.095000), (-0.315000, 0.095000), (-0.250000, 0.095000),
(-0.190000, 0.095000), (-0.125000, 0.095000), (-0.060000, 0.095000), (0.000000, 0.095000), (0.065000, 0.095000),
(0.125000, 0.095000), (0.190000, 0.095000), (0.410000, 0.095000), (-0.365000, 0.100000), (-0.300000, 0.100000),
(-0.240000, 0.100000), (-0.175000, 0.100000), (-0.115000, 0.100000), (-0.050000, 0.100000), (0.015000, 0.100000),
(0.075000, 0.100000), (0.140000, 0.100000), (0.200000, 0.100000), (-0.415000, 0.105000), (-0.355000, 0.105000),
(-0.290000, 0.105000), (-0.225000, 0.105000), (-0.165000, 0.105000), (-0.100000, 0.105000), (-0.040000, 0.105000),
(0.025000, 0.105000), (0.090000, 0.105000), (0.150000, 0.105000), (0.335000, 0.105000), (-0.400000, 0.110000),
(-0.335000, 0.110000), (-0.270000, 0.110000), (-0.210000, 0.110000), (-0.145000, 0.110000), (-0.085000, 0.110000),
(-0.020000, 0.110000), (0.040000, 0.110000), (0.105000, 0.110000), (0.170000, 0.110000), (0.395000, 0.110000),
(-0.370000, 0.115000), (-0.310000, 0.115000), (-0.245000, 0.115000), (-0.180000, 0.115000), (-0.120000, 0.115000),
(-0.055000, 0.115000), (0.005000, 0.115000), (0.070000, 0.115000), (0.135000, 0.115000), (0.195000, 0.115000),
(-0.405000, 0.120000), (-0.345000, 0.120000), (-0.280000, 0.120000), (-0.220000, 0.120000), (-0.155000, 0.120000),
(-0.090000, 0.120000), (-0.030000, 0.120000), (0.035000, 0.120000), (0.095000, 0.120000), (0.160000, 0.120000),
(0.390000, 0.120000), (-0.375000, 0.125000), (-0.310000, 0.125000), (-0.250000, 0.125000), (-0.185000, 0.125000),
(-0.125000, 0.125000), (-0.060000, 0.125000), (0.005000, 0.125000), (0.065000, 0.125000), (0.130000, 0.125000),
(0.270000, 0.125000), (-0.395000, 0.130000), (-0.330000, 0.130000), (-0.270000, 0.130000), (-0.205000, 0.130000),
(-0.145000, 0.130000), (-0.080000, 0.130000), (-0.015000, 0.130000), (0.045000, 0.130000), (0.120000, 0.130000),
(0.180000, 0.130000), (-0.400000, 0.135000), (-0.340000, 0.135000), (-0.275000, 0.135000), (-0.210000, 0.135000),
(-0.150000, 0.135000), (-0.085000, 0.135000), (-0.025000, 0.135000), (0.040000, 0.135000), (0.140000, 0.135000),
(0.370000, 0.135000), (-0.380000, 0.140000), (-0.320000, 0.140000), (-0.255000, 0.140000), (-0.195000, 0.140000),
(-0.130000, 0.140000), (-0.065000, 0.140000), (-0.005000, 0.140000), (0.100000, 0.140000), (0.160000, 0.140000),
(-0.405000, 0.145000), (-0.340000, 0.145000), (-0.280000, 0.145000), (-0.215000, 0.145000), (-0.155000, 0.145000),
(-0.090000, 0.145000), (-0.025000, 0.145000), (0.035000, 0.145000), (0.145000, 0.145000), (0.385000, 0.145000),
(-0.355000, 0.150000), (-0.295000, 0.150000), (-0.230000, 0.150000), (-0.165000, 0.150000), (-0.105000, 0.150000),
(-0.040000, 0.150000), (0.020000, 0.150000), (0.130000, 0.150000), (0.370000, 0.150000), (-0.365000, 0.155000),
(-0.300000, 0.155000), (-0.240000, 0.155000), (-0.175000, 0.155000), (-0.110000, 0.155000), (-0.050000, 0.155000),
(0.015000, 0.155000), (0.115000, 0.155000), (0.355000, 0.155000), (-0.380000, 0.160000), (-0.315000, 0.160000),
(-0.250000, 0.160000), (-0.190000, 0.160000), (-0.125000, 0.160000), (-0.065000, 0.160000), (0.000000, 0.160000),
(0.095000, 0.160000), (0.155000, 0.160000), (-0.390000, 0.165000), (-0.330000, 0.165000), (-0.265000, 0.165000),
(-0.205000, 0.165000), (-0.140000, 0.165000), (-0.075000, 0.165000), (-0.015000, 0.165000), (0.050000, 0.165000),
(0.110000, 0.165000), (0.350000, 0.165000), (-0.370000, 0.170000), (-0.310000, 0.170000), (-0.245000, 0.170000),
(-0.185000, 0.170000), (-0.120000, 0.170000), (-0.055000, 0.170000), (0.005000, 0.170000), (0.070000, 0.170000),
(0.130000, 0.170000), (0.370000, 0.170000), (-0.350000, 0.175000), (-0.285000, 0.175000), (-0.220000, 0.175000),
(-0.160000, 0.175000), (-0.095000, 0.175000), (-0.035000, 0.175000), (0.030000, 0.175000), (0.095000, 0.175000),
(0.295000, 0.175000), (-0.370000, 0.180000), (-0.310000, 0.180000), (-0.245000, 0.180000), (-0.185000, 0.180000),
(-0.120000, 0.180000), (-0.055000, 0.180000), (0.005000, 0.180000), (0.070000, 0.180000), (0.130000, 0.180000),
(-0.390000, 0.185000), (-0.325000, 0.185000), (-0.265000, 0.185000), (-0.200000, 0.185000), (-0.140000, 0.185000),
(-0.075000, 0.185000), (-0.010000, 0.185000), (0.050000, 0.185000), (0.115000, 0.185000), (0.345000, 0.185000),
(-0.350000, 0.190000), (-0.290000, 0.190000), (-0.225000, 0.190000), (-0.160000, 0.190000), (-0.100000, 0.190000),
(-0.035000, 0.190000), (0.025000, 0.190000), (0.090000, 0.190000), (0.285000, 0.190000), (-0.365000, 0.195000),
(-0.300000, 0.195000), (-0.240000, 0.195000), (-0.175000, 0.195000), (-0.110000, 0.195000), (-0.050000, 0.195000),
(0.015000, 0.195000), (0.075000, 0.195000), (0.220000, 0.195000), (-0.380000, 0.200000), (-0.315000, 0.200000),
(-0.250000, 0.200000), (-0.190000, 0.200000), (-0.125000, 0.200000), (-0.065000, 0.200000), (0.000000, 0.200000),
(0.065000, 0.200000), (0.205000, 0.200000), (-0.380000, 0.205000), (-0.320000, 0.205000), (-0.255000, 0.205000),
(-0.195000, 0.205000), (-0.130000, 0.205000), (-0.065000, 0.205000), (-0.005000, 0.205000), (0.060000, 0.205000),
(0.200000, 0.205000), (0.350000, 0.205000), (-0.315000, 0.210000), (-0.255000, 0.210000), (-0.190000, 0.210000),
(-0.130000, 0.210000), (-0.065000, 0.210000), (0.000000, 0.210000), (0.060000, 0.210000), (0.200000, 0.210000),
(-0.375000, 0.215000), (-0.310000, 0.215000), (-0.250000, 0.215000), (-0.185000, 0.215000), (-0.120000, 0.215000),
(-0.060000, 0.215000), (0.005000, 0.215000), (0.065000, 0.215000), (0.260000, 0.215000), (-0.360000, 0.220000),
(-0.300000, 0.220000), (-0.235000, 0.220000), (-0.175000, 0.220000), (-0.110000, 0.220000), (-0.045000, 0.220000),
(0.015000, 0.220000), (0.080000, 0.220000), (0.265000, 0.220000), (-0.345000, 0.225000), (-0.285000, 0.225000),
(-0.220000, 0.225000), (-0.155000, 0.225000), (-0.095000, 0.225000), (-0.030000, 0.225000), (0.030000, 0.225000),
(0.095000, 0.225000), (0.320000, 0.225000), (-0.325000, 0.230000), (-0.260000, 0.230000), (-0.200000, 0.230000),
(-0.135000, 0.230000), (-0.075000, 0.230000), (-0.010000, 0.230000), (0.055000, 0.230000), (0.180000, 0.230000),
(-0.355000, 0.235000), (-0.295000, 0.235000), (-0.230000, 0.235000), (-0.165000, 0.235000), (-0.105000, 0.235000),
(-0.040000, 0.235000), (0.020000, 0.235000), (0.085000, 0.235000), (0.310000, 0.235000), (-0.315000, 0.240000),
(-0.250000, 0.240000), (-0.190000, 0.240000), (-0.125000, 0.240000), (-0.065000, 0.240000), (0.000000, 0.240000),
(0.065000, 0.240000), (0.240000, 0.240000), (-0.330000, 0.245000), (-0.270000, 0.245000), (-0.205000, 0.245000),
(-0.140000, 0.245000), (-0.080000, 0.245000), (-0.015000, 0.245000), (0.045000, 0.245000), (0.230000, 0.245000),
(-0.340000, 0.250000), (-0.275000, 0.250000), (-0.210000, 0.250000), (-0.150000, 0.250000), (-0.085000, 0.250000),
(-0.020000, 0.250000), (0.045000, 0.250000), (0.225000, 0.250000), (-0.340000, 0.255000), (-0.275000, 0.255000),
(-0.215000, 0.255000), (-0.150000, 0.255000), (-0.085000, 0.255000), (-0.020000, 0.255000), (0.045000, 0.255000),
(0.215000, 0.255000), (-0.335000, 0.260000), (-0.275000, 0.260000), (-0.210000, 0.260000), (-0.145000, 0.260000),
(-0.085000, 0.260000), (-0.015000, 0.260000), (0.045000, 0.260000), (0.220000, 0.260000), (-0.315000, 0.265000),
(-0.255000, 0.265000), (-0.190000, 0.265000), (-0.130000, 0.265000), (-0.055000, 0.265000), (0.010000, 0.265000),
(0.070000, 0.265000), (0.280000, 0.265000), (-0.285000, 0.270000), (-0.220000, 0.270000), (-0.160000, 0.270000),
(-0.095000, 0.270000), (-0.020000, 0.270000), (0.040000, 0.270000), (0.255000, 0.270000), (-0.300000, 0.275000),
(-0.235000, 0.275000), (-0.170000, 0.275000), (-0.110000, 0.275000), (-0.030000, 0.275000), (0.030000, 0.275000),
(0.245000, 0.275000), (-0.295000, 0.280000), (-0.230000, 0.280000), (-0.165000, 0.280000), (-0.105000, 0.280000),
(-0.025000, 0.280000), (0.035000, 0.280000), (0.250000, 0.280000), (-0.270000, 0.285000), (-0.210000, 0.285000),
(-0.145000, 0.285000), (-0.065000, 0.285000), (0.000000, 0.285000), (0.170000, 0.285000), (0.270000, 0.285000),
(-0.235000, 0.290000), (-0.175000, 0.290000), (-0.085000, 0.290000), (-0.025000, 0.290000), (0.040000, 0.290000),
(0.245000, 0.290000), (-0.250000, 0.295000), (-0.185000, 0.295000), (-0.090000, 0.295000), (-0.025000, 0.295000),
(0.040000, 0.295000), (0.230000, 0.295000), (-0.235000, 0.300000), (-0.175000, 0.300000), (-0.065000, 0.300000),
(-0.005000, 0.300000), (0.145000, 0.300000), (0.255000, 0.300000), (-0.190000, 0.305000), (-0.070000, 0.305000),
(-0.010000, 0.305000), (0.135000, 0.305000), (0.245000, 0.305000), (-0.070000, 0.310000), (-0.005000, 0.310000),
(0.130000, 0.310000), (-0.130000, 0.315000), (-0.065000, 0.315000), (-0.005000, 0.315000), (0.180000, 0.315000),
(-0.140000, 0.320000), (-0.075000, 0.320000), (-0.015000, 0.320000), (0.105000, 0.320000), (-0.150000, 0.325000),
(-0.090000, 0.325000), (-0.025000, 0.325000), (0.080000, 0.325000), (0.210000, 0.325000), (-0.110000, 0.330000),
(-0.050000, 0.330000), (0.145000, 0.330000), (0.205000, 0.330000), (-0.125000, 0.335000), (-0.060000, 0.335000),
(0.000000, 0.335000), (0.190000, 0.335000), (-0.155000, 0.340000), (-0.090000, 0.340000), (-0.030000, 0.340000),
(0.150000, 0.340000), (-0.190000, 0.345000), (-0.130000, 0.345000), (-0.065000, 0.345000), (-0.005000, 0.345000),
(0.160000, 0.345000), (-0.170000, 0.350000), (-0.110000, 0.350000), (-0.045000, 0.350000), (0.110000, 0.350000),
(-0.200000, 0.355000), (-0.140000, 0.355000), (-0.075000, 0.355000), (0.070000, 0.355000), (0.130000, 0.355000),
(-0.155000, 0.360000), (-0.095000, 0.360000), (-0.030000, 0.360000), (0.085000, 0.360000), (0.145000, 0.360000),
(-0.115000, 0.365000), (-0.055000, 0.365000), (0.010000, 0.365000), (0.075000, 0.365000), (-0.160000, 0.370000),
(-0.095000, 0.370000), (-0.035000, 0.370000), (0.030000, 0.370000), (0.090000, 0.370000), (-0.110000, 0.375000),
(-0.045000, 0.375000), (0.015000, 0.375000), (0.080000, 0.375000), (-0.095000, 0.380000), (-0.030000, 0.380000),
(0.035000, 0.380000), (-0.095000, 0.385000), (-0.030000, 0.385000), (0.030000, 0.385000), (-0.040000, 0.390000),
]
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
pet_dir=$(pwd)
node "$pet_dir/scripts/build-pet.mjs" --apple
app_dir="macos/CodewhalePet.app"
mkdir -p "$app_dir/Contents/MacOS" "$app_dir/Contents/Resources"
swiftc -O -target "${PET_MACOS_ARCH:-$(uname -m)}-apple-macos14.0" -parse-as-library swift/PetSim.swift swift/PetNativeCore.swift swift/PetHabitatStore.swift swift/PetAudioOutput.swift swift/PetHost.swift swift/CodewhalePetView.swift swift/PetHabitatView.swift macos/WhalePoints.swift macos/CodewhalePetApp.swift -o "$app_dir/Contents/MacOS/CodewhalePet"
cp "$pet_dir/dist/pet-native.js" "$app_dir/Contents/Resources/pet-native.js"
cp ios/Resources/demo.jsonl "$app_dir/Contents/Resources/demo.jsonl"
cp public/whale.png "$app_dir/Contents/Resources/whale.png"
python3 - "$app_dir/Contents/Info.plist" <<'PY'
import plistlib,sys
with open(sys.argv[1],'wb') as f:
plistlib.dump({'CFBundleIdentifier':'dev.shannonlabs.CodewhalePet','CFBundleName':'Codewhale Pet','CFBundleDisplayName':'Codewhale Pet','CFBundleExecutable':'CodewhalePet','CFBundlePackageType':'APPL','CFBundleIconFile':'whale.png','CFBundleShortVersionString':'0.1.0','CFBundleVersion':'1','LSMinimumSystemVersion':'14.0','LSUIElement':True,'NSHighResolutionCapable':True},f)
PY
codesign --force --sign - "$app_dir"
codesign --verify --strict "$app_dir"
printf 'Built and locally signed %s\n' "$app_dir"
+33
View File
@@ -0,0 +1,33 @@
{
"name": "@codewhale/pet",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@codewhale/pet",
"version": "0.1.0",
"license": "Apache-2.0",
"devDependencies": {
"typescript": "5.8.3"
},
"engines": {
"node": ">=22.13"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@codewhale/pet",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "Apache-2.0",
"engines": {
"node": ">=22.13"
},
"scripts": {
"build": "node scripts/build-pet.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "node --test tests/*.test.mjs",
"check": "npm run typecheck && npm run build && npm test",
"start": "python3 -m http.server 4632 --bind 127.0.0.1 --directory dist",
"sync": "node scripts/build-pet.mjs --tui --apple --study"
},
"devDependencies": {
"typescript": "5.8.3"
}
}
+61
View File
@@ -0,0 +1,61 @@
<!doctype html>
<meta charset="utf-8">
<title>pet · cross-platform tape</title>
<style>
body { margin: 0; background: #06080d; color: #9fb4c4; font: 12px/1.4 ui-monospace, monospace }
#cap { position: fixed; left: 10px; bottom: 8px; }
canvas { display: block; }
</style>
<canvas id="c"></canvas>
<div id="cap"></div>
<script type="module">
import { PetSim } from './dist/PetSim.js';
import { drawPet } from './dist/web.js';
const points = (await (await fetch('whale-points.tsv')).text())
.trim().split('\n').map(l => l.split('\t').map(Number));
const rows = (await (await fetch('tape.tsv')).text())
.trim().split('\n').slice(1).map(l => l.split('\t'));
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
const cap = document.getElementById('cap');
const W = 900, H = 420;
cv.width = W; cv.height = H;
const sim = new PetSim(points);
const motion = !matchMedia('(prefers-reduced-motion: reduce)').matches;
let i = 0;
// Step the sim at the tape's recorded cadence (30fps), looped.
// ?frame=N renders a single frame and stops — for screenshots and diffs.
const single = new URLSearchParams(location.search).get('frame');
function rowToState(c) {
return { activity: +c[1], coherence: +c[2], attention: +c[3], channel: c[4],
observed: +c[5], roamX: +c[6], roamY: +c[7], flip: +c[8], lit: +c[9] };
}
if (single !== null) {
const target = +single;
for (i = 0; i <= target && i < rows.length; i++) sim.step(+rows[i][0], rowToState(rows[i]), { motion: true, sensitivity: 1 });
draw();
} else {
(function tick() {
const c = rows[i % rows.length];
sim.step(+c[0], rowToState(c), { motion, sensitivity: 1 });
draw();
i++;
requestAnimationFrame(tick);
})();
}
function draw() {
const st = rowToState(rows[(single !== null ? i - 1 : i) % rows.length]);
ctx.fillStyle = '#06080d';
ctx.fillRect(0, 0, W, H);
drawPet(ctx, sim, st, W, H);
const f = sim.frame;
cap.textContent = `frame ${i - 1} · ${f.channel} · ${f.arch}${f.hollow ? ' · unobserved' : ''}`;
}
window.__pet = { sim, i };
</script>
+3
View File
@@ -0,0 +1,3 @@
:root{color-scheme:dark;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#080f15;color:#c7d5dc}*{box-sizing:border-box}body{margin:0}main{max-width:1140px;margin:0 auto;padding:34px 38px}header,footer{display:flex;justify-content:space-between;gap:20px;font-size:11px;letter-spacing:.07em;color:#859aa7}header a{color:#bdd1db;text-decoration:none}.intro{margin:56px 0 30px}.eyebrow{font-size:10px;letter-spacing:.25em;color:#89b7bf}h1{font-size:clamp(28px,4vw,44px);font-weight:450;letter-spacing:-.04em;margin:14px 0}.intro>p:last-child{color:#8fa5b0;font-size:14px;line-height:1.75}.tank{height:430px;position:relative;border:1px solid #20333f;background:radial-gradient(ellipse at 50% 10%,#112733 0,#0a151d 58%,#080f15 100%);overflow:hidden;border-radius:3px}canvas{display:block;width:100%;height:100%}.readout{position:absolute;left:24px;bottom:26px;display:flex;flex-direction:column;gap:7px;pointer-events:none;font:11px ui-monospace,monospace}.readout strong{font-weight:500;color:#b7d5dd}.readout span{color:#829ba8}.depth{position:absolute;right:24px;bottom:26px;font-size:8px;letter-spacing:.2em;color:#5a7686;pointer-events:none}nav{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin:20px 0}button,select,.file{background:#101f29;border:1px solid #2a424f;border-radius:3px;color:#c2d6df;font:11px inherit;padding:9px 11px;cursor:pointer}button:hover,select:hover,.file:hover{background:#1a303c}button[aria-pressed=true]{border-color:#81b5c3;color:#d2f5fb}label{font-size:11px;color:#a5bbc6}.toggle{display:flex;align-items:center;gap:5px;padding:5px}.file{position:relative;overflow:hidden}.file input{position:absolute;inset:0;opacity:0;width:100%;cursor:pointer}input:focus-visible,button:focus-visible,select:focus-visible,canvas:focus-visible,.file:focus-within{outline:2px solid #9ed9e6;outline-offset:4px}.timeline{display:flex;align-items:center;gap:15px;font:10px ui-monospace,monospace;color:#8ba5b3}.timeline input{flex:1;accent-color:#85bdc9}#message{min-height:34px;font-size:11px;line-height:1.7;color:#8fa8b5}footer{border-top:1px solid #1e303b;padding-top:18px;letter-spacing:0;line-height:1.5;font-size:10px}@media(max-width:650px){main{padding:22px 18px}.intro{margin-top:36px}.tank{height:340px}.depth{display:none}header,footer{flex-direction:column;gap:8px}nav{gap:8px}button,select,.file{min-height:38px}}
#persistence{font-size:11px;line-height:1.7;color:#8fa8b5}button:disabled{opacity:.45;cursor:default}
+16
View File
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Codewhale · A living field</title><link rel="stylesheet" href="./pet.css"></head>
<body><main>
<header><a href="./">CODEWHALE / WHALESONG</a><span id="source">Wild · simulated creature</span></header>
<section class="intro"><p class="eyebrow">A WHALE LIVING IN CODE</p><h1>Watch the work take shape.</h1><p>One field of dots, shaped by what Codewhale is doing.<br>A whale at rest. A living expression of the work.</p></section>
<section class="tank" aria-label="Whale habitat"><canvas id="tank" tabindex="0" role="img" aria-label="A whale made of dots. Use the Attention and Feed buttons to interact."></canvas>
<div class="readout"><strong id="channel">other · drift</strong><span id="behaviour">swim</span></div><span class="depth">CODE, MADE VISIBLE.</span></section>
<nav aria-label="Habitat controls"><label>World <select id="mode"><option value="wild">Wild</option><option value="demo">Event demo</option><option value="replay" disabled>Imported replay</option><option value="live" disabled>Live local tape</option></select></label>
<button id="pause" aria-pressed="false">Pause</button><button id="attention">Attention</button><button id="feed">Feed</button><button id="sound" aria-pressed="false">Sound off</button>
<label class="toggle"><input id="motion" type="checkbox"> Still</label><label class="file">Import trace / tape<input id="file" type="file" accept=".json,.jsonl,.ndjson"></label><button id="follow">Follow local tape</button><button id="save">Save replay</button></nav>
<div class="timeline"><output id="clock">0:00</output><input id="seek" type="range" min="0" max="90" step="0.1" value="0" aria-label="Replay time"><output id="end">1:30</output></div>
<p id="message" role="status">Wild mode is a simulated creature. Import event-v1, OTLP, or Codewhale telemetry to see work.</p>
<p id="persistence" role="status">The habitat saves on this device.</p>
<footer><span>Hollow dots mean missing telemetry. Sleep is a separate dimmer.</span><span>Everything stays on this device.</span></footer>
</main><script type="module" src="./ui/pet.js"></script></body></html>
+980
View File
@@ -0,0 +1,980 @@
-0.100000 -0.390000
-0.040000 -0.390000
-0.090000 -0.385000
-0.030000 -0.385000
-0.125000 -0.380000
-0.065000 -0.380000
0.000000 -0.380000
-0.135000 -0.375000
-0.075000 -0.375000
-0.010000 -0.375000
-0.185000 -0.370000
-0.120000 -0.370000
-0.055000 -0.370000
0.005000 -0.370000
-0.185000 -0.365000
-0.125000 -0.365000
-0.060000 -0.365000
0.000000 -0.365000
-0.210000 -0.360000
-0.145000 -0.360000
-0.085000 -0.360000
-0.020000 -0.360000
0.040000 -0.360000
-0.175000 -0.355000
-0.110000 -0.355000
-0.050000 -0.355000
0.015000 -0.355000
-0.215000 -0.350000
-0.150000 -0.350000
-0.085000 -0.350000
-0.025000 -0.350000
0.040000 -0.350000
-0.195000 -0.345000
-0.130000 -0.345000
-0.070000 -0.345000
-0.005000 -0.345000
-0.235000 -0.340000
-0.175000 -0.340000
-0.110000 -0.340000
-0.050000 -0.340000
0.015000 -0.340000
-0.220000 -0.335000
-0.160000 -0.335000
-0.095000 -0.335000
-0.035000 -0.335000
0.030000 -0.335000
-0.220000 -0.330000
-0.155000 -0.330000
-0.090000 -0.330000
-0.030000 -0.330000
0.035000 -0.330000
-0.215000 -0.325000
-0.150000 -0.325000
-0.085000 -0.325000
-0.025000 -0.325000
-0.270000 -0.320000
-0.210000 -0.320000
-0.145000 -0.320000
-0.080000 -0.320000
-0.020000 -0.320000
-0.265000 -0.315000
-0.205000 -0.315000
-0.140000 -0.315000
-0.080000 -0.315000
-0.015000 -0.315000
-0.260000 -0.310000
-0.200000 -0.310000
-0.135000 -0.310000
-0.075000 -0.310000
-0.010000 -0.310000
-0.250000 -0.305000
-0.190000 -0.305000
-0.125000 -0.305000
-0.065000 -0.305000
-0.305000 -0.300000
-0.240000 -0.300000
-0.180000 -0.300000
-0.115000 -0.300000
-0.055000 -0.300000
-0.285000 -0.295000
-0.225000 -0.295000
-0.160000 -0.295000
-0.095000 -0.295000
-0.035000 -0.295000
0.035000 -0.295000
-0.295000 -0.290000
-0.230000 -0.290000
-0.165000 -0.290000
-0.105000 -0.290000
-0.040000 -0.290000
0.020000 -0.290000
0.085000 -0.290000
-0.270000 -0.285000
-0.205000 -0.285000
-0.140000 -0.285000
-0.080000 -0.285000
-0.015000 -0.285000
0.045000 -0.285000
-0.320000 -0.280000
-0.255000 -0.280000
-0.195000 -0.280000
-0.130000 -0.280000
-0.070000 -0.280000
-0.005000 -0.280000
0.060000 -0.280000
-0.330000 -0.275000
-0.265000 -0.275000
-0.205000 -0.275000
-0.140000 -0.275000
-0.080000 -0.275000
-0.015000 -0.275000
0.050000 -0.275000
0.110000 -0.275000
-0.295000 -0.270000
-0.235000 -0.270000
-0.170000 -0.270000
-0.105000 -0.270000
-0.045000 -0.270000
0.020000 -0.270000
0.080000 -0.270000
0.145000 -0.270000
-0.290000 -0.265000
-0.225000 -0.265000
-0.160000 -0.265000
-0.100000 -0.265000
-0.035000 -0.265000
0.025000 -0.265000
0.090000 -0.265000
0.155000 -0.265000
-0.305000 -0.260000
-0.240000 -0.260000
-0.180000 -0.260000
-0.055000 -0.260000
0.010000 -0.260000
0.070000 -0.260000
0.135000 -0.260000
-0.340000 -0.255000
-0.275000 -0.255000
-0.215000 -0.255000
-0.150000 -0.255000
0.010000 -0.255000
0.070000 -0.255000
0.135000 -0.255000
-0.350000 -0.250000
-0.285000 -0.250000
-0.220000 -0.250000
-0.160000 -0.250000
0.030000 -0.250000
0.090000 -0.250000
0.155000 -0.250000
-0.325000 -0.245000
-0.265000 -0.245000
-0.200000 -0.245000
0.005000 -0.245000
0.070000 -0.245000
0.130000 -0.245000
-0.355000 -0.240000
-0.290000 -0.240000
-0.230000 -0.240000
0.000000 -0.240000
0.060000 -0.240000
0.125000 -0.240000
-0.360000 -0.235000
-0.300000 -0.235000
-0.235000 -0.235000
0.005000 -0.235000
0.070000 -0.235000
0.130000 -0.235000
-0.350000 -0.230000
-0.285000 -0.230000
-0.225000 -0.230000
0.035000 -0.230000
0.095000 -0.230000
0.160000 -0.230000
-0.320000 -0.225000
-0.260000 -0.225000
0.010000 -0.225000
0.070000 -0.225000
0.135000 -0.225000
-0.345000 -0.220000
-0.285000 -0.220000
-0.220000 -0.220000
0.060000 -0.220000
0.125000 -0.220000
-0.355000 -0.215000
-0.290000 -0.215000
-0.225000 -0.215000
0.065000 -0.215000
0.130000 -0.215000
-0.350000 -0.210000
-0.285000 -0.210000
0.025000 -0.210000
0.085000 -0.210000
0.150000 -0.210000
-0.330000 -0.205000
-0.265000 -0.205000
0.060000 -0.205000
0.120000 -0.205000
-0.345000 -0.200000
-0.285000 -0.200000
0.055000 -0.200000
0.115000 -0.200000
-0.345000 -0.195000
-0.280000 -0.195000
0.070000 -0.195000
-0.375000 -0.190000
-0.315000 -0.190000
-0.395000 -0.185000
-0.330000 -0.185000
-0.270000 -0.185000
-0.345000 -0.180000
-0.285000 -0.180000
-0.360000 -0.175000
-0.300000 -0.175000
-0.375000 -0.170000
-0.310000 -0.170000
-0.390000 -0.165000
-0.325000 -0.165000
-0.400000 -0.160000
-0.335000 -0.160000
-0.405000 -0.155000
-0.345000 -0.155000
-0.280000 -0.155000
-0.355000 -0.150000
-0.290000 -0.150000
-0.360000 -0.145000
-0.300000 -0.145000
-0.365000 -0.140000
-0.305000 -0.140000
-0.370000 -0.135000
-0.310000 -0.135000
-0.375000 -0.130000
-0.310000 -0.130000
-0.380000 -0.125000
-0.315000 -0.125000
-0.380000 -0.120000
-0.315000 -0.120000
-0.380000 -0.115000
-0.320000 -0.115000
-0.385000 -0.110000
-0.325000 -0.110000
-0.385000 -0.105000
-0.325000 -0.105000
-0.385000 -0.100000
-0.320000 -0.100000
-0.390000 -0.095000
-0.325000 -0.095000
-0.390000 -0.090000
-0.325000 -0.090000
-0.385000 -0.085000
-0.325000 -0.085000
-0.385000 -0.080000
-0.325000 -0.080000
-0.385000 -0.075000
-0.320000 -0.075000
-0.385000 -0.070000
-0.320000 -0.070000
-0.435000 -0.065000
-0.370000 -0.065000
0.255000 -0.065000
0.320000 -0.065000
0.385000 -0.065000
-0.390000 -0.060000
-0.325000 -0.060000
0.270000 -0.060000
0.335000 -0.060000
0.400000 -0.060000
-0.385000 -0.055000
-0.320000 -0.055000
0.250000 -0.055000
0.315000 -0.055000
0.380000 -0.055000
-0.415000 -0.050000
-0.350000 -0.050000
0.195000 -0.050000
0.260000 -0.050000
0.320000 -0.050000
0.385000 -0.050000
-0.410000 -0.045000
-0.350000 -0.045000
0.180000 -0.045000
0.240000 -0.045000
0.305000 -0.045000
0.370000 -0.045000
-0.440000 -0.040000
-0.375000 -0.040000
-0.315000 -0.040000
0.200000 -0.040000
0.260000 -0.040000
0.325000 -0.040000
0.390000 -0.040000
-0.420000 -0.035000
-0.355000 -0.035000
0.135000 -0.035000
0.200000 -0.035000
0.265000 -0.035000
0.325000 -0.035000
0.390000 -0.035000
-0.425000 -0.030000
-0.360000 -0.030000
0.120000 -0.030000
0.180000 -0.030000
0.245000 -0.030000
0.305000 -0.030000
0.370000 -0.030000
0.430000 -0.030000
-0.380000 -0.025000
-0.315000 -0.025000
0.145000 -0.025000
0.210000 -0.025000
0.270000 -0.025000
0.335000 -0.025000
0.400000 -0.025000
-0.415000 -0.020000
-0.350000 -0.020000
0.095000 -0.020000
0.160000 -0.020000
0.220000 -0.020000
0.285000 -0.020000
0.350000 -0.020000
0.410000 -0.020000
-0.400000 -0.015000
-0.340000 -0.015000
0.095000 -0.015000
0.160000 -0.015000
0.220000 -0.015000
0.285000 -0.015000
0.345000 -0.015000
0.410000 -0.015000
-0.405000 -0.010000
-0.345000 -0.010000
0.075000 -0.010000
0.135000 -0.010000
0.200000 -0.010000
0.260000 -0.010000
0.325000 -0.010000
0.390000 -0.010000
-0.430000 -0.005000
-0.365000 -0.005000
0.030000 -0.005000
0.095000 -0.005000
0.160000 -0.005000
0.220000 -0.005000
0.285000 -0.005000
0.345000 -0.005000
0.410000 -0.005000
-0.400000 0.000000
-0.340000 0.000000
0.045000 0.000000
0.105000 0.000000
0.170000 0.000000
0.230000 0.000000
0.295000 0.000000
0.360000 0.000000
0.420000 0.000000
-0.390000 0.005000
-0.330000 0.005000
0.045000 0.005000
0.110000 0.005000
0.170000 0.005000
0.235000 0.005000
0.295000 0.005000
0.360000 0.005000
0.420000 0.005000
-0.390000 0.010000
-0.325000 0.010000
0.025000 0.010000
0.090000 0.010000
0.150000 0.010000
0.215000 0.010000
0.280000 0.010000
0.340000 0.010000
0.405000 0.010000
-0.410000 0.015000
-0.345000 0.015000
-0.005000 0.015000
0.055000 0.015000
0.120000 0.015000
0.180000 0.015000
0.245000 0.015000
0.305000 0.015000
0.370000 0.015000
-0.440000 0.020000
-0.380000 0.020000
-0.315000 0.020000
0.005000 0.020000
0.070000 0.020000
0.135000 0.020000
0.195000 0.020000
0.260000 0.020000
0.320000 0.020000
0.385000 0.020000
-0.425000 0.025000
-0.365000 0.025000
-0.300000 0.025000
0.005000 0.025000
0.070000 0.025000
0.130000 0.025000
0.195000 0.025000
0.260000 0.025000
0.320000 0.025000
0.415000 0.025000
-0.400000 0.030000
-0.335000 0.030000
-0.040000 0.030000
0.020000 0.030000
0.085000 0.030000
0.145000 0.030000
0.210000 0.030000
0.270000 0.030000
0.335000 0.030000
-0.420000 0.035000
-0.360000 0.035000
-0.295000 0.035000
-0.025000 0.035000
0.040000 0.035000
0.105000 0.035000
0.165000 0.035000
0.230000 0.035000
0.290000 0.035000
0.430000 0.035000
-0.375000 0.040000
-0.315000 0.040000
-0.060000 0.040000
0.000000 0.040000
0.065000 0.040000
0.125000 0.040000
0.190000 0.040000
0.255000 0.040000
0.405000 0.040000
-0.400000 0.045000
-0.340000 0.045000
-0.100000 0.045000
-0.035000 0.045000
0.025000 0.045000
0.090000 0.045000
0.150000 0.045000
0.215000 0.045000
0.275000 0.045000
-0.430000 0.050000
-0.365000 0.050000
-0.305000 0.050000
-0.090000 0.050000
-0.030000 0.050000
0.035000 0.050000
0.100000 0.050000
0.160000 0.050000
0.225000 0.050000
0.285000 0.050000
-0.405000 0.055000
-0.340000 0.055000
-0.280000 0.055000
-0.085000 0.055000
-0.025000 0.055000
0.040000 0.055000
0.100000 0.055000
0.165000 0.055000
0.230000 0.055000
0.405000 0.055000
-0.395000 0.060000
-0.335000 0.060000
-0.270000 0.060000
-0.110000 0.060000
-0.050000 0.060000
0.015000 0.060000
0.075000 0.060000
0.140000 0.060000
0.205000 0.060000
0.265000 0.060000
-0.410000 0.065000
-0.350000 0.065000
-0.285000 0.065000
-0.180000 0.065000
-0.115000 0.065000
-0.050000 0.065000
0.010000 0.065000
0.075000 0.065000
0.135000 0.065000
0.200000 0.065000
0.265000 0.065000
-0.410000 0.070000
-0.345000 0.070000
-0.285000 0.070000
-0.220000 0.070000
-0.160000 0.070000
-0.095000 0.070000
-0.030000 0.070000
0.030000 0.070000
0.095000 0.070000
0.155000 0.070000
0.220000 0.070000
0.420000 0.070000
-0.375000 0.075000
-0.310000 0.075000
-0.250000 0.075000
-0.185000 0.075000
-0.120000 0.075000
-0.060000 0.075000
0.005000 0.075000
0.065000 0.075000
0.130000 0.075000
0.190000 0.075000
0.395000 0.075000
-0.395000 0.080000
-0.335000 0.080000
-0.270000 0.080000
-0.210000 0.080000
-0.145000 0.080000
-0.080000 0.080000
-0.020000 0.080000
0.045000 0.080000
0.105000 0.080000
0.170000 0.080000
0.230000 0.080000
-0.415000 0.085000
-0.350000 0.085000
-0.290000 0.085000
-0.225000 0.085000
-0.165000 0.085000
-0.100000 0.085000
-0.035000 0.085000
0.025000 0.085000
0.090000 0.085000
0.150000 0.085000
0.215000 0.085000
-0.425000 0.090000
-0.365000 0.090000
-0.300000 0.090000
-0.240000 0.090000
-0.175000 0.090000
-0.115000 0.090000
-0.050000 0.090000
0.015000 0.090000
0.075000 0.090000
0.140000 0.090000
0.200000 0.090000
0.410000 0.090000
-0.375000 0.095000
-0.315000 0.095000
-0.250000 0.095000
-0.190000 0.095000
-0.125000 0.095000
-0.060000 0.095000
0.000000 0.095000
0.065000 0.095000
0.125000 0.095000
0.190000 0.095000
0.410000 0.095000
-0.365000 0.100000
-0.300000 0.100000
-0.240000 0.100000
-0.175000 0.100000
-0.115000 0.100000
-0.050000 0.100000
0.015000 0.100000
0.075000 0.100000
0.140000 0.100000
0.200000 0.100000
-0.415000 0.105000
-0.355000 0.105000
-0.290000 0.105000
-0.225000 0.105000
-0.165000 0.105000
-0.100000 0.105000
-0.040000 0.105000
0.025000 0.105000
0.090000 0.105000
0.150000 0.105000
0.335000 0.105000
-0.400000 0.110000
-0.335000 0.110000
-0.270000 0.110000
-0.210000 0.110000
-0.145000 0.110000
-0.085000 0.110000
-0.020000 0.110000
0.040000 0.110000
0.105000 0.110000
0.170000 0.110000
0.395000 0.110000
-0.370000 0.115000
-0.310000 0.115000
-0.245000 0.115000
-0.180000 0.115000
-0.120000 0.115000
-0.055000 0.115000
0.005000 0.115000
0.070000 0.115000
0.135000 0.115000
0.195000 0.115000
-0.405000 0.120000
-0.345000 0.120000
-0.280000 0.120000
-0.220000 0.120000
-0.155000 0.120000
-0.090000 0.120000
-0.030000 0.120000
0.035000 0.120000
0.095000 0.120000
0.160000 0.120000
0.390000 0.120000
-0.375000 0.125000
-0.310000 0.125000
-0.250000 0.125000
-0.185000 0.125000
-0.125000 0.125000
-0.060000 0.125000
0.005000 0.125000
0.065000 0.125000
0.130000 0.125000
0.270000 0.125000
-0.395000 0.130000
-0.330000 0.130000
-0.270000 0.130000
-0.205000 0.130000
-0.145000 0.130000
-0.080000 0.130000
-0.015000 0.130000
0.045000 0.130000
0.120000 0.130000
0.180000 0.130000
-0.400000 0.135000
-0.340000 0.135000
-0.275000 0.135000
-0.210000 0.135000
-0.150000 0.135000
-0.085000 0.135000
-0.025000 0.135000
0.040000 0.135000
0.140000 0.135000
0.370000 0.135000
-0.380000 0.140000
-0.320000 0.140000
-0.255000 0.140000
-0.195000 0.140000
-0.130000 0.140000
-0.065000 0.140000
-0.005000 0.140000
0.100000 0.140000
0.160000 0.140000
-0.405000 0.145000
-0.340000 0.145000
-0.280000 0.145000
-0.215000 0.145000
-0.155000 0.145000
-0.090000 0.145000
-0.025000 0.145000
0.035000 0.145000
0.145000 0.145000
0.385000 0.145000
-0.355000 0.150000
-0.295000 0.150000
-0.230000 0.150000
-0.165000 0.150000
-0.105000 0.150000
-0.040000 0.150000
0.020000 0.150000
0.130000 0.150000
0.370000 0.150000
-0.365000 0.155000
-0.300000 0.155000
-0.240000 0.155000
-0.175000 0.155000
-0.110000 0.155000
-0.050000 0.155000
0.015000 0.155000
0.115000 0.155000
0.355000 0.155000
-0.380000 0.160000
-0.315000 0.160000
-0.250000 0.160000
-0.190000 0.160000
-0.125000 0.160000
-0.065000 0.160000
0.000000 0.160000
0.095000 0.160000
0.155000 0.160000
-0.390000 0.165000
-0.330000 0.165000
-0.265000 0.165000
-0.205000 0.165000
-0.140000 0.165000
-0.075000 0.165000
-0.015000 0.165000
0.050000 0.165000
0.110000 0.165000
0.350000 0.165000
-0.370000 0.170000
-0.310000 0.170000
-0.245000 0.170000
-0.185000 0.170000
-0.120000 0.170000
-0.055000 0.170000
0.005000 0.170000
0.070000 0.170000
0.130000 0.170000
0.370000 0.170000
-0.350000 0.175000
-0.285000 0.175000
-0.220000 0.175000
-0.160000 0.175000
-0.095000 0.175000
-0.035000 0.175000
0.030000 0.175000
0.095000 0.175000
0.295000 0.175000
-0.370000 0.180000
-0.310000 0.180000
-0.245000 0.180000
-0.185000 0.180000
-0.120000 0.180000
-0.055000 0.180000
0.005000 0.180000
0.070000 0.180000
0.130000 0.180000
-0.390000 0.185000
-0.325000 0.185000
-0.265000 0.185000
-0.200000 0.185000
-0.140000 0.185000
-0.075000 0.185000
-0.010000 0.185000
0.050000 0.185000
0.115000 0.185000
0.345000 0.185000
-0.350000 0.190000
-0.290000 0.190000
-0.225000 0.190000
-0.160000 0.190000
-0.100000 0.190000
-0.035000 0.190000
0.025000 0.190000
0.090000 0.190000
0.285000 0.190000
-0.365000 0.195000
-0.300000 0.195000
-0.240000 0.195000
-0.175000 0.195000
-0.110000 0.195000
-0.050000 0.195000
0.015000 0.195000
0.075000 0.195000
0.220000 0.195000
-0.380000 0.200000
-0.315000 0.200000
-0.250000 0.200000
-0.190000 0.200000
-0.125000 0.200000
-0.065000 0.200000
0.000000 0.200000
0.065000 0.200000
0.205000 0.200000
-0.380000 0.205000
-0.320000 0.205000
-0.255000 0.205000
-0.195000 0.205000
-0.130000 0.205000
-0.065000 0.205000
-0.005000 0.205000
0.060000 0.205000
0.200000 0.205000
0.350000 0.205000
-0.315000 0.210000
-0.255000 0.210000
-0.190000 0.210000
-0.130000 0.210000
-0.065000 0.210000
0.000000 0.210000
0.060000 0.210000
0.200000 0.210000
-0.375000 0.215000
-0.310000 0.215000
-0.250000 0.215000
-0.185000 0.215000
-0.120000 0.215000
-0.060000 0.215000
0.005000 0.215000
0.065000 0.215000
0.260000 0.215000
-0.360000 0.220000
-0.300000 0.220000
-0.235000 0.220000
-0.175000 0.220000
-0.110000 0.220000
-0.045000 0.220000
0.015000 0.220000
0.080000 0.220000
0.265000 0.220000
-0.345000 0.225000
-0.285000 0.225000
-0.220000 0.225000
-0.155000 0.225000
-0.095000 0.225000
-0.030000 0.225000
0.030000 0.225000
0.095000 0.225000
0.320000 0.225000
-0.325000 0.230000
-0.260000 0.230000
-0.200000 0.230000
-0.135000 0.230000
-0.075000 0.230000
-0.010000 0.230000
0.055000 0.230000
0.180000 0.230000
-0.355000 0.235000
-0.295000 0.235000
-0.230000 0.235000
-0.165000 0.235000
-0.105000 0.235000
-0.040000 0.235000
0.020000 0.235000
0.085000 0.235000
0.310000 0.235000
-0.315000 0.240000
-0.250000 0.240000
-0.190000 0.240000
-0.125000 0.240000
-0.065000 0.240000
0.000000 0.240000
0.065000 0.240000
0.240000 0.240000
-0.330000 0.245000
-0.270000 0.245000
-0.205000 0.245000
-0.140000 0.245000
-0.080000 0.245000
-0.015000 0.245000
0.045000 0.245000
0.230000 0.245000
-0.340000 0.250000
-0.275000 0.250000
-0.210000 0.250000
-0.150000 0.250000
-0.085000 0.250000
-0.020000 0.250000
0.045000 0.250000
0.225000 0.250000
-0.340000 0.255000
-0.275000 0.255000
-0.215000 0.255000
-0.150000 0.255000
-0.085000 0.255000
-0.020000 0.255000
0.045000 0.255000
0.215000 0.255000
-0.335000 0.260000
-0.275000 0.260000
-0.210000 0.260000
-0.145000 0.260000
-0.085000 0.260000
-0.015000 0.260000
0.045000 0.260000
0.220000 0.260000
-0.315000 0.265000
-0.255000 0.265000
-0.190000 0.265000
-0.130000 0.265000
-0.055000 0.265000
0.010000 0.265000
0.070000 0.265000
0.280000 0.265000
-0.285000 0.270000
-0.220000 0.270000
-0.160000 0.270000
-0.095000 0.270000
-0.020000 0.270000
0.040000 0.270000
0.255000 0.270000
-0.300000 0.275000
-0.235000 0.275000
-0.170000 0.275000
-0.110000 0.275000
-0.030000 0.275000
0.030000 0.275000
0.245000 0.275000
-0.295000 0.280000
-0.230000 0.280000
-0.165000 0.280000
-0.105000 0.280000
-0.025000 0.280000
0.035000 0.280000
0.250000 0.280000
-0.270000 0.285000
-0.210000 0.285000
-0.145000 0.285000
-0.065000 0.285000
0.000000 0.285000
0.170000 0.285000
0.270000 0.285000
-0.235000 0.290000
-0.175000 0.290000
-0.085000 0.290000
-0.025000 0.290000
0.040000 0.290000
0.245000 0.290000
-0.250000 0.295000
-0.185000 0.295000
-0.090000 0.295000
-0.025000 0.295000
0.040000 0.295000
0.230000 0.295000
-0.235000 0.300000
-0.175000 0.300000
-0.065000 0.300000
-0.005000 0.300000
0.145000 0.300000
0.255000 0.300000
-0.190000 0.305000
-0.070000 0.305000
-0.010000 0.305000
0.135000 0.305000
0.245000 0.305000
-0.070000 0.310000
-0.005000 0.310000
0.130000 0.310000
-0.130000 0.315000
-0.065000 0.315000
-0.005000 0.315000
0.180000 0.315000
-0.140000 0.320000
-0.075000 0.320000
-0.015000 0.320000
0.105000 0.320000
-0.150000 0.325000
-0.090000 0.325000
-0.025000 0.325000
0.080000 0.325000
0.210000 0.325000
-0.110000 0.330000
-0.050000 0.330000
0.145000 0.330000
0.205000 0.330000
-0.125000 0.335000
-0.060000 0.335000
0.000000 0.335000
0.190000 0.335000
-0.155000 0.340000
-0.090000 0.340000
-0.030000 0.340000
0.150000 0.340000
-0.190000 0.345000
-0.130000 0.345000
-0.065000 0.345000
-0.005000 0.345000
0.160000 0.345000
-0.170000 0.350000
-0.110000 0.350000
-0.045000 0.350000
0.110000 0.350000
-0.200000 0.355000
-0.140000 0.355000
-0.075000 0.355000
0.070000 0.355000
0.130000 0.355000
-0.155000 0.360000
-0.095000 0.360000
-0.030000 0.360000
0.085000 0.360000
0.145000 0.360000
-0.115000 0.365000
-0.055000 0.365000
0.010000 0.365000
0.075000 0.365000
-0.160000 0.370000
-0.095000 0.370000
-0.035000 0.370000
0.030000 0.370000
0.090000 0.370000
-0.110000 0.375000
-0.045000 0.375000
0.015000 0.375000
0.080000 0.375000
-0.095000 0.380000
-0.030000 0.380000
0.035000 0.380000
-0.095000 0.385000
-0.030000 0.385000
0.030000 0.385000
-0.040000 0.390000
1 -0.100000 -0.390000
2 -0.040000 -0.390000
3 -0.090000 -0.385000
4 -0.030000 -0.385000
5 -0.125000 -0.380000
6 -0.065000 -0.380000
7 0.000000 -0.380000
8 -0.135000 -0.375000
9 -0.075000 -0.375000
10 -0.010000 -0.375000
11 -0.185000 -0.370000
12 -0.120000 -0.370000
13 -0.055000 -0.370000
14 0.005000 -0.370000
15 -0.185000 -0.365000
16 -0.125000 -0.365000
17 -0.060000 -0.365000
18 0.000000 -0.365000
19 -0.210000 -0.360000
20 -0.145000 -0.360000
21 -0.085000 -0.360000
22 -0.020000 -0.360000
23 0.040000 -0.360000
24 -0.175000 -0.355000
25 -0.110000 -0.355000
26 -0.050000 -0.355000
27 0.015000 -0.355000
28 -0.215000 -0.350000
29 -0.150000 -0.350000
30 -0.085000 -0.350000
31 -0.025000 -0.350000
32 0.040000 -0.350000
33 -0.195000 -0.345000
34 -0.130000 -0.345000
35 -0.070000 -0.345000
36 -0.005000 -0.345000
37 -0.235000 -0.340000
38 -0.175000 -0.340000
39 -0.110000 -0.340000
40 -0.050000 -0.340000
41 0.015000 -0.340000
42 -0.220000 -0.335000
43 -0.160000 -0.335000
44 -0.095000 -0.335000
45 -0.035000 -0.335000
46 0.030000 -0.335000
47 -0.220000 -0.330000
48 -0.155000 -0.330000
49 -0.090000 -0.330000
50 -0.030000 -0.330000
51 0.035000 -0.330000
52 -0.215000 -0.325000
53 -0.150000 -0.325000
54 -0.085000 -0.325000
55 -0.025000 -0.325000
56 -0.270000 -0.320000
57 -0.210000 -0.320000
58 -0.145000 -0.320000
59 -0.080000 -0.320000
60 -0.020000 -0.320000
61 -0.265000 -0.315000
62 -0.205000 -0.315000
63 -0.140000 -0.315000
64 -0.080000 -0.315000
65 -0.015000 -0.315000
66 -0.260000 -0.310000
67 -0.200000 -0.310000
68 -0.135000 -0.310000
69 -0.075000 -0.310000
70 -0.010000 -0.310000
71 -0.250000 -0.305000
72 -0.190000 -0.305000
73 -0.125000 -0.305000
74 -0.065000 -0.305000
75 -0.305000 -0.300000
76 -0.240000 -0.300000
77 -0.180000 -0.300000
78 -0.115000 -0.300000
79 -0.055000 -0.300000
80 -0.285000 -0.295000
81 -0.225000 -0.295000
82 -0.160000 -0.295000
83 -0.095000 -0.295000
84 -0.035000 -0.295000
85 0.035000 -0.295000
86 -0.295000 -0.290000
87 -0.230000 -0.290000
88 -0.165000 -0.290000
89 -0.105000 -0.290000
90 -0.040000 -0.290000
91 0.020000 -0.290000
92 0.085000 -0.290000
93 -0.270000 -0.285000
94 -0.205000 -0.285000
95 -0.140000 -0.285000
96 -0.080000 -0.285000
97 -0.015000 -0.285000
98 0.045000 -0.285000
99 -0.320000 -0.280000
100 -0.255000 -0.280000
101 -0.195000 -0.280000
102 -0.130000 -0.280000
103 -0.070000 -0.280000
104 -0.005000 -0.280000
105 0.060000 -0.280000
106 -0.330000 -0.275000
107 -0.265000 -0.275000
108 -0.205000 -0.275000
109 -0.140000 -0.275000
110 -0.080000 -0.275000
111 -0.015000 -0.275000
112 0.050000 -0.275000
113 0.110000 -0.275000
114 -0.295000 -0.270000
115 -0.235000 -0.270000
116 -0.170000 -0.270000
117 -0.105000 -0.270000
118 -0.045000 -0.270000
119 0.020000 -0.270000
120 0.080000 -0.270000
121 0.145000 -0.270000
122 -0.290000 -0.265000
123 -0.225000 -0.265000
124 -0.160000 -0.265000
125 -0.100000 -0.265000
126 -0.035000 -0.265000
127 0.025000 -0.265000
128 0.090000 -0.265000
129 0.155000 -0.265000
130 -0.305000 -0.260000
131 -0.240000 -0.260000
132 -0.180000 -0.260000
133 -0.055000 -0.260000
134 0.010000 -0.260000
135 0.070000 -0.260000
136 0.135000 -0.260000
137 -0.340000 -0.255000
138 -0.275000 -0.255000
139 -0.215000 -0.255000
140 -0.150000 -0.255000
141 0.010000 -0.255000
142 0.070000 -0.255000
143 0.135000 -0.255000
144 -0.350000 -0.250000
145 -0.285000 -0.250000
146 -0.220000 -0.250000
147 -0.160000 -0.250000
148 0.030000 -0.250000
149 0.090000 -0.250000
150 0.155000 -0.250000
151 -0.325000 -0.245000
152 -0.265000 -0.245000
153 -0.200000 -0.245000
154 0.005000 -0.245000
155 0.070000 -0.245000
156 0.130000 -0.245000
157 -0.355000 -0.240000
158 -0.290000 -0.240000
159 -0.230000 -0.240000
160 0.000000 -0.240000
161 0.060000 -0.240000
162 0.125000 -0.240000
163 -0.360000 -0.235000
164 -0.300000 -0.235000
165 -0.235000 -0.235000
166 0.005000 -0.235000
167 0.070000 -0.235000
168 0.130000 -0.235000
169 -0.350000 -0.230000
170 -0.285000 -0.230000
171 -0.225000 -0.230000
172 0.035000 -0.230000
173 0.095000 -0.230000
174 0.160000 -0.230000
175 -0.320000 -0.225000
176 -0.260000 -0.225000
177 0.010000 -0.225000
178 0.070000 -0.225000
179 0.135000 -0.225000
180 -0.345000 -0.220000
181 -0.285000 -0.220000
182 -0.220000 -0.220000
183 0.060000 -0.220000
184 0.125000 -0.220000
185 -0.355000 -0.215000
186 -0.290000 -0.215000
187 -0.225000 -0.215000
188 0.065000 -0.215000
189 0.130000 -0.215000
190 -0.350000 -0.210000
191 -0.285000 -0.210000
192 0.025000 -0.210000
193 0.085000 -0.210000
194 0.150000 -0.210000
195 -0.330000 -0.205000
196 -0.265000 -0.205000
197 0.060000 -0.205000
198 0.120000 -0.205000
199 -0.345000 -0.200000
200 -0.285000 -0.200000
201 0.055000 -0.200000
202 0.115000 -0.200000
203 -0.345000 -0.195000
204 -0.280000 -0.195000
205 0.070000 -0.195000
206 -0.375000 -0.190000
207 -0.315000 -0.190000
208 -0.395000 -0.185000
209 -0.330000 -0.185000
210 -0.270000 -0.185000
211 -0.345000 -0.180000
212 -0.285000 -0.180000
213 -0.360000 -0.175000
214 -0.300000 -0.175000
215 -0.375000 -0.170000
216 -0.310000 -0.170000
217 -0.390000 -0.165000
218 -0.325000 -0.165000
219 -0.400000 -0.160000
220 -0.335000 -0.160000
221 -0.405000 -0.155000
222 -0.345000 -0.155000
223 -0.280000 -0.155000
224 -0.355000 -0.150000
225 -0.290000 -0.150000
226 -0.360000 -0.145000
227 -0.300000 -0.145000
228 -0.365000 -0.140000
229 -0.305000 -0.140000
230 -0.370000 -0.135000
231 -0.310000 -0.135000
232 -0.375000 -0.130000
233 -0.310000 -0.130000
234 -0.380000 -0.125000
235 -0.315000 -0.125000
236 -0.380000 -0.120000
237 -0.315000 -0.120000
238 -0.380000 -0.115000
239 -0.320000 -0.115000
240 -0.385000 -0.110000
241 -0.325000 -0.110000
242 -0.385000 -0.105000
243 -0.325000 -0.105000
244 -0.385000 -0.100000
245 -0.320000 -0.100000
246 -0.390000 -0.095000
247 -0.325000 -0.095000
248 -0.390000 -0.090000
249 -0.325000 -0.090000
250 -0.385000 -0.085000
251 -0.325000 -0.085000
252 -0.385000 -0.080000
253 -0.325000 -0.080000
254 -0.385000 -0.075000
255 -0.320000 -0.075000
256 -0.385000 -0.070000
257 -0.320000 -0.070000
258 -0.435000 -0.065000
259 -0.370000 -0.065000
260 0.255000 -0.065000
261 0.320000 -0.065000
262 0.385000 -0.065000
263 -0.390000 -0.060000
264 -0.325000 -0.060000
265 0.270000 -0.060000
266 0.335000 -0.060000
267 0.400000 -0.060000
268 -0.385000 -0.055000
269 -0.320000 -0.055000
270 0.250000 -0.055000
271 0.315000 -0.055000
272 0.380000 -0.055000
273 -0.415000 -0.050000
274 -0.350000 -0.050000
275 0.195000 -0.050000
276 0.260000 -0.050000
277 0.320000 -0.050000
278 0.385000 -0.050000
279 -0.410000 -0.045000
280 -0.350000 -0.045000
281 0.180000 -0.045000
282 0.240000 -0.045000
283 0.305000 -0.045000
284 0.370000 -0.045000
285 -0.440000 -0.040000
286 -0.375000 -0.040000
287 -0.315000 -0.040000
288 0.200000 -0.040000
289 0.260000 -0.040000
290 0.325000 -0.040000
291 0.390000 -0.040000
292 -0.420000 -0.035000
293 -0.355000 -0.035000
294 0.135000 -0.035000
295 0.200000 -0.035000
296 0.265000 -0.035000
297 0.325000 -0.035000
298 0.390000 -0.035000
299 -0.425000 -0.030000
300 -0.360000 -0.030000
301 0.120000 -0.030000
302 0.180000 -0.030000
303 0.245000 -0.030000
304 0.305000 -0.030000
305 0.370000 -0.030000
306 0.430000 -0.030000
307 -0.380000 -0.025000
308 -0.315000 -0.025000
309 0.145000 -0.025000
310 0.210000 -0.025000
311 0.270000 -0.025000
312 0.335000 -0.025000
313 0.400000 -0.025000
314 -0.415000 -0.020000
315 -0.350000 -0.020000
316 0.095000 -0.020000
317 0.160000 -0.020000
318 0.220000 -0.020000
319 0.285000 -0.020000
320 0.350000 -0.020000
321 0.410000 -0.020000
322 -0.400000 -0.015000
323 -0.340000 -0.015000
324 0.095000 -0.015000
325 0.160000 -0.015000
326 0.220000 -0.015000
327 0.285000 -0.015000
328 0.345000 -0.015000
329 0.410000 -0.015000
330 -0.405000 -0.010000
331 -0.345000 -0.010000
332 0.075000 -0.010000
333 0.135000 -0.010000
334 0.200000 -0.010000
335 0.260000 -0.010000
336 0.325000 -0.010000
337 0.390000 -0.010000
338 -0.430000 -0.005000
339 -0.365000 -0.005000
340 0.030000 -0.005000
341 0.095000 -0.005000
342 0.160000 -0.005000
343 0.220000 -0.005000
344 0.285000 -0.005000
345 0.345000 -0.005000
346 0.410000 -0.005000
347 -0.400000 0.000000
348 -0.340000 0.000000
349 0.045000 0.000000
350 0.105000 0.000000
351 0.170000 0.000000
352 0.230000 0.000000
353 0.295000 0.000000
354 0.360000 0.000000
355 0.420000 0.000000
356 -0.390000 0.005000
357 -0.330000 0.005000
358 0.045000 0.005000
359 0.110000 0.005000
360 0.170000 0.005000
361 0.235000 0.005000
362 0.295000 0.005000
363 0.360000 0.005000
364 0.420000 0.005000
365 -0.390000 0.010000
366 -0.325000 0.010000
367 0.025000 0.010000
368 0.090000 0.010000
369 0.150000 0.010000
370 0.215000 0.010000
371 0.280000 0.010000
372 0.340000 0.010000
373 0.405000 0.010000
374 -0.410000 0.015000
375 -0.345000 0.015000
376 -0.005000 0.015000
377 0.055000 0.015000
378 0.120000 0.015000
379 0.180000 0.015000
380 0.245000 0.015000
381 0.305000 0.015000
382 0.370000 0.015000
383 -0.440000 0.020000
384 -0.380000 0.020000
385 -0.315000 0.020000
386 0.005000 0.020000
387 0.070000 0.020000
388 0.135000 0.020000
389 0.195000 0.020000
390 0.260000 0.020000
391 0.320000 0.020000
392 0.385000 0.020000
393 -0.425000 0.025000
394 -0.365000 0.025000
395 -0.300000 0.025000
396 0.005000 0.025000
397 0.070000 0.025000
398 0.130000 0.025000
399 0.195000 0.025000
400 0.260000 0.025000
401 0.320000 0.025000
402 0.415000 0.025000
403 -0.400000 0.030000
404 -0.335000 0.030000
405 -0.040000 0.030000
406 0.020000 0.030000
407 0.085000 0.030000
408 0.145000 0.030000
409 0.210000 0.030000
410 0.270000 0.030000
411 0.335000 0.030000
412 -0.420000 0.035000
413 -0.360000 0.035000
414 -0.295000 0.035000
415 -0.025000 0.035000
416 0.040000 0.035000
417 0.105000 0.035000
418 0.165000 0.035000
419 0.230000 0.035000
420 0.290000 0.035000
421 0.430000 0.035000
422 -0.375000 0.040000
423 -0.315000 0.040000
424 -0.060000 0.040000
425 0.000000 0.040000
426 0.065000 0.040000
427 0.125000 0.040000
428 0.190000 0.040000
429 0.255000 0.040000
430 0.405000 0.040000
431 -0.400000 0.045000
432 -0.340000 0.045000
433 -0.100000 0.045000
434 -0.035000 0.045000
435 0.025000 0.045000
436 0.090000 0.045000
437 0.150000 0.045000
438 0.215000 0.045000
439 0.275000 0.045000
440 -0.430000 0.050000
441 -0.365000 0.050000
442 -0.305000 0.050000
443 -0.090000 0.050000
444 -0.030000 0.050000
445 0.035000 0.050000
446 0.100000 0.050000
447 0.160000 0.050000
448 0.225000 0.050000
449 0.285000 0.050000
450 -0.405000 0.055000
451 -0.340000 0.055000
452 -0.280000 0.055000
453 -0.085000 0.055000
454 -0.025000 0.055000
455 0.040000 0.055000
456 0.100000 0.055000
457 0.165000 0.055000
458 0.230000 0.055000
459 0.405000 0.055000
460 -0.395000 0.060000
461 -0.335000 0.060000
462 -0.270000 0.060000
463 -0.110000 0.060000
464 -0.050000 0.060000
465 0.015000 0.060000
466 0.075000 0.060000
467 0.140000 0.060000
468 0.205000 0.060000
469 0.265000 0.060000
470 -0.410000 0.065000
471 -0.350000 0.065000
472 -0.285000 0.065000
473 -0.180000 0.065000
474 -0.115000 0.065000
475 -0.050000 0.065000
476 0.010000 0.065000
477 0.075000 0.065000
478 0.135000 0.065000
479 0.200000 0.065000
480 0.265000 0.065000
481 -0.410000 0.070000
482 -0.345000 0.070000
483 -0.285000 0.070000
484 -0.220000 0.070000
485 -0.160000 0.070000
486 -0.095000 0.070000
487 -0.030000 0.070000
488 0.030000 0.070000
489 0.095000 0.070000
490 0.155000 0.070000
491 0.220000 0.070000
492 0.420000 0.070000
493 -0.375000 0.075000
494 -0.310000 0.075000
495 -0.250000 0.075000
496 -0.185000 0.075000
497 -0.120000 0.075000
498 -0.060000 0.075000
499 0.005000 0.075000
500 0.065000 0.075000
501 0.130000 0.075000
502 0.190000 0.075000
503 0.395000 0.075000
504 -0.395000 0.080000
505 -0.335000 0.080000
506 -0.270000 0.080000
507 -0.210000 0.080000
508 -0.145000 0.080000
509 -0.080000 0.080000
510 -0.020000 0.080000
511 0.045000 0.080000
512 0.105000 0.080000
513 0.170000 0.080000
514 0.230000 0.080000
515 -0.415000 0.085000
516 -0.350000 0.085000
517 -0.290000 0.085000
518 -0.225000 0.085000
519 -0.165000 0.085000
520 -0.100000 0.085000
521 -0.035000 0.085000
522 0.025000 0.085000
523 0.090000 0.085000
524 0.150000 0.085000
525 0.215000 0.085000
526 -0.425000 0.090000
527 -0.365000 0.090000
528 -0.300000 0.090000
529 -0.240000 0.090000
530 -0.175000 0.090000
531 -0.115000 0.090000
532 -0.050000 0.090000
533 0.015000 0.090000
534 0.075000 0.090000
535 0.140000 0.090000
536 0.200000 0.090000
537 0.410000 0.090000
538 -0.375000 0.095000
539 -0.315000 0.095000
540 -0.250000 0.095000
541 -0.190000 0.095000
542 -0.125000 0.095000
543 -0.060000 0.095000
544 0.000000 0.095000
545 0.065000 0.095000
546 0.125000 0.095000
547 0.190000 0.095000
548 0.410000 0.095000
549 -0.365000 0.100000
550 -0.300000 0.100000
551 -0.240000 0.100000
552 -0.175000 0.100000
553 -0.115000 0.100000
554 -0.050000 0.100000
555 0.015000 0.100000
556 0.075000 0.100000
557 0.140000 0.100000
558 0.200000 0.100000
559 -0.415000 0.105000
560 -0.355000 0.105000
561 -0.290000 0.105000
562 -0.225000 0.105000
563 -0.165000 0.105000
564 -0.100000 0.105000
565 -0.040000 0.105000
566 0.025000 0.105000
567 0.090000 0.105000
568 0.150000 0.105000
569 0.335000 0.105000
570 -0.400000 0.110000
571 -0.335000 0.110000
572 -0.270000 0.110000
573 -0.210000 0.110000
574 -0.145000 0.110000
575 -0.085000 0.110000
576 -0.020000 0.110000
577 0.040000 0.110000
578 0.105000 0.110000
579 0.170000 0.110000
580 0.395000 0.110000
581 -0.370000 0.115000
582 -0.310000 0.115000
583 -0.245000 0.115000
584 -0.180000 0.115000
585 -0.120000 0.115000
586 -0.055000 0.115000
587 0.005000 0.115000
588 0.070000 0.115000
589 0.135000 0.115000
590 0.195000 0.115000
591 -0.405000 0.120000
592 -0.345000 0.120000
593 -0.280000 0.120000
594 -0.220000 0.120000
595 -0.155000 0.120000
596 -0.090000 0.120000
597 -0.030000 0.120000
598 0.035000 0.120000
599 0.095000 0.120000
600 0.160000 0.120000
601 0.390000 0.120000
602 -0.375000 0.125000
603 -0.310000 0.125000
604 -0.250000 0.125000
605 -0.185000 0.125000
606 -0.125000 0.125000
607 -0.060000 0.125000
608 0.005000 0.125000
609 0.065000 0.125000
610 0.130000 0.125000
611 0.270000 0.125000
612 -0.395000 0.130000
613 -0.330000 0.130000
614 -0.270000 0.130000
615 -0.205000 0.130000
616 -0.145000 0.130000
617 -0.080000 0.130000
618 -0.015000 0.130000
619 0.045000 0.130000
620 0.120000 0.130000
621 0.180000 0.130000
622 -0.400000 0.135000
623 -0.340000 0.135000
624 -0.275000 0.135000
625 -0.210000 0.135000
626 -0.150000 0.135000
627 -0.085000 0.135000
628 -0.025000 0.135000
629 0.040000 0.135000
630 0.140000 0.135000
631 0.370000 0.135000
632 -0.380000 0.140000
633 -0.320000 0.140000
634 -0.255000 0.140000
635 -0.195000 0.140000
636 -0.130000 0.140000
637 -0.065000 0.140000
638 -0.005000 0.140000
639 0.100000 0.140000
640 0.160000 0.140000
641 -0.405000 0.145000
642 -0.340000 0.145000
643 -0.280000 0.145000
644 -0.215000 0.145000
645 -0.155000 0.145000
646 -0.090000 0.145000
647 -0.025000 0.145000
648 0.035000 0.145000
649 0.145000 0.145000
650 0.385000 0.145000
651 -0.355000 0.150000
652 -0.295000 0.150000
653 -0.230000 0.150000
654 -0.165000 0.150000
655 -0.105000 0.150000
656 -0.040000 0.150000
657 0.020000 0.150000
658 0.130000 0.150000
659 0.370000 0.150000
660 -0.365000 0.155000
661 -0.300000 0.155000
662 -0.240000 0.155000
663 -0.175000 0.155000
664 -0.110000 0.155000
665 -0.050000 0.155000
666 0.015000 0.155000
667 0.115000 0.155000
668 0.355000 0.155000
669 -0.380000 0.160000
670 -0.315000 0.160000
671 -0.250000 0.160000
672 -0.190000 0.160000
673 -0.125000 0.160000
674 -0.065000 0.160000
675 0.000000 0.160000
676 0.095000 0.160000
677 0.155000 0.160000
678 -0.390000 0.165000
679 -0.330000 0.165000
680 -0.265000 0.165000
681 -0.205000 0.165000
682 -0.140000 0.165000
683 -0.075000 0.165000
684 -0.015000 0.165000
685 0.050000 0.165000
686 0.110000 0.165000
687 0.350000 0.165000
688 -0.370000 0.170000
689 -0.310000 0.170000
690 -0.245000 0.170000
691 -0.185000 0.170000
692 -0.120000 0.170000
693 -0.055000 0.170000
694 0.005000 0.170000
695 0.070000 0.170000
696 0.130000 0.170000
697 0.370000 0.170000
698 -0.350000 0.175000
699 -0.285000 0.175000
700 -0.220000 0.175000
701 -0.160000 0.175000
702 -0.095000 0.175000
703 -0.035000 0.175000
704 0.030000 0.175000
705 0.095000 0.175000
706 0.295000 0.175000
707 -0.370000 0.180000
708 -0.310000 0.180000
709 -0.245000 0.180000
710 -0.185000 0.180000
711 -0.120000 0.180000
712 -0.055000 0.180000
713 0.005000 0.180000
714 0.070000 0.180000
715 0.130000 0.180000
716 -0.390000 0.185000
717 -0.325000 0.185000
718 -0.265000 0.185000
719 -0.200000 0.185000
720 -0.140000 0.185000
721 -0.075000 0.185000
722 -0.010000 0.185000
723 0.050000 0.185000
724 0.115000 0.185000
725 0.345000 0.185000
726 -0.350000 0.190000
727 -0.290000 0.190000
728 -0.225000 0.190000
729 -0.160000 0.190000
730 -0.100000 0.190000
731 -0.035000 0.190000
732 0.025000 0.190000
733 0.090000 0.190000
734 0.285000 0.190000
735 -0.365000 0.195000
736 -0.300000 0.195000
737 -0.240000 0.195000
738 -0.175000 0.195000
739 -0.110000 0.195000
740 -0.050000 0.195000
741 0.015000 0.195000
742 0.075000 0.195000
743 0.220000 0.195000
744 -0.380000 0.200000
745 -0.315000 0.200000
746 -0.250000 0.200000
747 -0.190000 0.200000
748 -0.125000 0.200000
749 -0.065000 0.200000
750 0.000000 0.200000
751 0.065000 0.200000
752 0.205000 0.200000
753 -0.380000 0.205000
754 -0.320000 0.205000
755 -0.255000 0.205000
756 -0.195000 0.205000
757 -0.130000 0.205000
758 -0.065000 0.205000
759 -0.005000 0.205000
760 0.060000 0.205000
761 0.200000 0.205000
762 0.350000 0.205000
763 -0.315000 0.210000
764 -0.255000 0.210000
765 -0.190000 0.210000
766 -0.130000 0.210000
767 -0.065000 0.210000
768 0.000000 0.210000
769 0.060000 0.210000
770 0.200000 0.210000
771 -0.375000 0.215000
772 -0.310000 0.215000
773 -0.250000 0.215000
774 -0.185000 0.215000
775 -0.120000 0.215000
776 -0.060000 0.215000
777 0.005000 0.215000
778 0.065000 0.215000
779 0.260000 0.215000
780 -0.360000 0.220000
781 -0.300000 0.220000
782 -0.235000 0.220000
783 -0.175000 0.220000
784 -0.110000 0.220000
785 -0.045000 0.220000
786 0.015000 0.220000
787 0.080000 0.220000
788 0.265000 0.220000
789 -0.345000 0.225000
790 -0.285000 0.225000
791 -0.220000 0.225000
792 -0.155000 0.225000
793 -0.095000 0.225000
794 -0.030000 0.225000
795 0.030000 0.225000
796 0.095000 0.225000
797 0.320000 0.225000
798 -0.325000 0.230000
799 -0.260000 0.230000
800 -0.200000 0.230000
801 -0.135000 0.230000
802 -0.075000 0.230000
803 -0.010000 0.230000
804 0.055000 0.230000
805 0.180000 0.230000
806 -0.355000 0.235000
807 -0.295000 0.235000
808 -0.230000 0.235000
809 -0.165000 0.235000
810 -0.105000 0.235000
811 -0.040000 0.235000
812 0.020000 0.235000
813 0.085000 0.235000
814 0.310000 0.235000
815 -0.315000 0.240000
816 -0.250000 0.240000
817 -0.190000 0.240000
818 -0.125000 0.240000
819 -0.065000 0.240000
820 0.000000 0.240000
821 0.065000 0.240000
822 0.240000 0.240000
823 -0.330000 0.245000
824 -0.270000 0.245000
825 -0.205000 0.245000
826 -0.140000 0.245000
827 -0.080000 0.245000
828 -0.015000 0.245000
829 0.045000 0.245000
830 0.230000 0.245000
831 -0.340000 0.250000
832 -0.275000 0.250000
833 -0.210000 0.250000
834 -0.150000 0.250000
835 -0.085000 0.250000
836 -0.020000 0.250000
837 0.045000 0.250000
838 0.225000 0.250000
839 -0.340000 0.255000
840 -0.275000 0.255000
841 -0.215000 0.255000
842 -0.150000 0.255000
843 -0.085000 0.255000
844 -0.020000 0.255000
845 0.045000 0.255000
846 0.215000 0.255000
847 -0.335000 0.260000
848 -0.275000 0.260000
849 -0.210000 0.260000
850 -0.145000 0.260000
851 -0.085000 0.260000
852 -0.015000 0.260000
853 0.045000 0.260000
854 0.220000 0.260000
855 -0.315000 0.265000
856 -0.255000 0.265000
857 -0.190000 0.265000
858 -0.130000 0.265000
859 -0.055000 0.265000
860 0.010000 0.265000
861 0.070000 0.265000
862 0.280000 0.265000
863 -0.285000 0.270000
864 -0.220000 0.270000
865 -0.160000 0.270000
866 -0.095000 0.270000
867 -0.020000 0.270000
868 0.040000 0.270000
869 0.255000 0.270000
870 -0.300000 0.275000
871 -0.235000 0.275000
872 -0.170000 0.275000
873 -0.110000 0.275000
874 -0.030000 0.275000
875 0.030000 0.275000
876 0.245000 0.275000
877 -0.295000 0.280000
878 -0.230000 0.280000
879 -0.165000 0.280000
880 -0.105000 0.280000
881 -0.025000 0.280000
882 0.035000 0.280000
883 0.250000 0.280000
884 -0.270000 0.285000
885 -0.210000 0.285000
886 -0.145000 0.285000
887 -0.065000 0.285000
888 0.000000 0.285000
889 0.170000 0.285000
890 0.270000 0.285000
891 -0.235000 0.290000
892 -0.175000 0.290000
893 -0.085000 0.290000
894 -0.025000 0.290000
895 0.040000 0.290000
896 0.245000 0.290000
897 -0.250000 0.295000
898 -0.185000 0.295000
899 -0.090000 0.295000
900 -0.025000 0.295000
901 0.040000 0.295000
902 0.230000 0.295000
903 -0.235000 0.300000
904 -0.175000 0.300000
905 -0.065000 0.300000
906 -0.005000 0.300000
907 0.145000 0.300000
908 0.255000 0.300000
909 -0.190000 0.305000
910 -0.070000 0.305000
911 -0.010000 0.305000
912 0.135000 0.305000
913 0.245000 0.305000
914 -0.070000 0.310000
915 -0.005000 0.310000
916 0.130000 0.310000
917 -0.130000 0.315000
918 -0.065000 0.315000
919 -0.005000 0.315000
920 0.180000 0.315000
921 -0.140000 0.320000
922 -0.075000 0.320000
923 -0.015000 0.320000
924 0.105000 0.320000
925 -0.150000 0.325000
926 -0.090000 0.325000
927 -0.025000 0.325000
928 0.080000 0.325000
929 0.210000 0.325000
930 -0.110000 0.330000
931 -0.050000 0.330000
932 0.145000 0.330000
933 0.205000 0.330000
934 -0.125000 0.335000
935 -0.060000 0.335000
936 0.000000 0.335000
937 0.190000 0.335000
938 -0.155000 0.340000
939 -0.090000 0.340000
940 -0.030000 0.340000
941 0.150000 0.340000
942 -0.190000 0.345000
943 -0.130000 0.345000
944 -0.065000 0.345000
945 -0.005000 0.345000
946 0.160000 0.345000
947 -0.170000 0.350000
948 -0.110000 0.350000
949 -0.045000 0.350000
950 0.110000 0.350000
951 -0.200000 0.355000
952 -0.140000 0.355000
953 -0.075000 0.355000
954 0.070000 0.355000
955 0.130000 0.355000
956 -0.155000 0.360000
957 -0.095000 0.360000
958 -0.030000 0.360000
959 0.085000 0.360000
960 0.145000 0.360000
961 -0.115000 0.365000
962 -0.055000 0.365000
963 0.010000 0.365000
964 0.075000 0.365000
965 -0.160000 0.370000
966 -0.095000 0.370000
967 -0.035000 0.370000
968 0.030000 0.370000
969 0.090000 0.370000
970 -0.110000 0.375000
971 -0.045000 0.375000
972 0.015000 0.375000
973 0.080000 0.375000
974 -0.095000 0.380000
975 -0.030000 0.380000
976 0.035000 0.380000
977 -0.095000 0.385000
978 -0.030000 0.385000
979 0.030000 0.385000
980 -0.040000 0.390000
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+57
View File
@@ -0,0 +1,57 @@
// Tape runner for the Rust pet core.
// petsim — run tape.tsv on stdin, print digests
// petsim --frame N WxH — print the braille frame at tape frame N
//
// Build: rustc -O main.rs -o petsim (zero dependencies)
use std::io::Read;
#[path = "pet_sim.rs"]
mod pet_sim;
use pet_sim::*;
fn parse_row(c: Vec<&str>) -> Option<(f64, PetState)> {
if c.len() < 10 || c[0] == "dt" { return None; }
let channel = ChannelId::from_key(c[4]).expect("known tape channel");
Some((c[0].parse().unwrap(), PetState {
activity: c[1].parse().unwrap(), coherence: c[2].parse().unwrap(),
attention: c[3].parse().unwrap(), channel,
observed: c[5].parse().unwrap(), roam_x: c[6].parse().unwrap(),
roam_y: c[7].parse().unwrap(), flip: c[8].parse().unwrap(), lit: c[9].parse().unwrap(),
}))
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let motion = !args.iter().any(|s| s == "--reduced-motion");
let mut tape = String::new();
std::io::stdin().read_to_string(&mut tape).unwrap();
let rows: Vec<(f64, PetState)> =
tape.trim().lines().filter_map(|l| parse_row(l.split('\t').collect())).collect();
let mut sim = PetSim::whale();
if args.get(1).map(|s| s.as_str()) == Some("--frame") {
let target: usize = args[2].parse().unwrap();
let (cw, ch) = if args.len() > 3 {
let mut d = args[3].split('x');
(d.next().unwrap().parse().unwrap(), d.next().unwrap().parse().unwrap())
} else { (78, 26) };
let mut st = PetState::rest();
for (i, (dt, s)) in rows.iter().enumerate() {
sim.step(*dt, s, motion, 1.0);
if i == target { st = *s; break; }
}
print!("{}", braille_text(&braille(&sim, cw, ch, &st), cw, ch));
return;
}
let mut out = String::new();
for (i, (dt, st)) in rows.iter().enumerate() {
sim.step(*dt, st, motion, 1.0);
if i % 30 == 0 {
out.push_str(&format!("f{:04} {} {}\n", i, digest(&sim), st.channel));
}
}
out.push_str(&format!("final {}", digest(&sim)));
println!("{}", out);
}
+4
View File
@@ -0,0 +1,4 @@
//! Compatibility entry point: the product owns the single Rust implementation.
#[path = "../../crates/tui/src/tui/ambient_life/pet_sim.rs"]
mod shared;
pub use self::shared::*;
+980
View File
@@ -0,0 +1,980 @@
-0.100000 -0.390000
-0.040000 -0.390000
-0.090000 -0.385000
-0.030000 -0.385000
-0.125000 -0.380000
-0.065000 -0.380000
0.000000 -0.380000
-0.135000 -0.375000
-0.075000 -0.375000
-0.010000 -0.375000
-0.185000 -0.370000
-0.120000 -0.370000
-0.055000 -0.370000
0.005000 -0.370000
-0.185000 -0.365000
-0.125000 -0.365000
-0.060000 -0.365000
0.000000 -0.365000
-0.210000 -0.360000
-0.145000 -0.360000
-0.085000 -0.360000
-0.020000 -0.360000
0.040000 -0.360000
-0.175000 -0.355000
-0.110000 -0.355000
-0.050000 -0.355000
0.015000 -0.355000
-0.215000 -0.350000
-0.150000 -0.350000
-0.085000 -0.350000
-0.025000 -0.350000
0.040000 -0.350000
-0.195000 -0.345000
-0.130000 -0.345000
-0.070000 -0.345000
-0.005000 -0.345000
-0.235000 -0.340000
-0.175000 -0.340000
-0.110000 -0.340000
-0.050000 -0.340000
0.015000 -0.340000
-0.220000 -0.335000
-0.160000 -0.335000
-0.095000 -0.335000
-0.035000 -0.335000
0.030000 -0.335000
-0.220000 -0.330000
-0.155000 -0.330000
-0.090000 -0.330000
-0.030000 -0.330000
0.035000 -0.330000
-0.215000 -0.325000
-0.150000 -0.325000
-0.085000 -0.325000
-0.025000 -0.325000
-0.270000 -0.320000
-0.210000 -0.320000
-0.145000 -0.320000
-0.080000 -0.320000
-0.020000 -0.320000
-0.265000 -0.315000
-0.205000 -0.315000
-0.140000 -0.315000
-0.080000 -0.315000
-0.015000 -0.315000
-0.260000 -0.310000
-0.200000 -0.310000
-0.135000 -0.310000
-0.075000 -0.310000
-0.010000 -0.310000
-0.250000 -0.305000
-0.190000 -0.305000
-0.125000 -0.305000
-0.065000 -0.305000
-0.305000 -0.300000
-0.240000 -0.300000
-0.180000 -0.300000
-0.115000 -0.300000
-0.055000 -0.300000
-0.285000 -0.295000
-0.225000 -0.295000
-0.160000 -0.295000
-0.095000 -0.295000
-0.035000 -0.295000
0.035000 -0.295000
-0.295000 -0.290000
-0.230000 -0.290000
-0.165000 -0.290000
-0.105000 -0.290000
-0.040000 -0.290000
0.020000 -0.290000
0.085000 -0.290000
-0.270000 -0.285000
-0.205000 -0.285000
-0.140000 -0.285000
-0.080000 -0.285000
-0.015000 -0.285000
0.045000 -0.285000
-0.320000 -0.280000
-0.255000 -0.280000
-0.195000 -0.280000
-0.130000 -0.280000
-0.070000 -0.280000
-0.005000 -0.280000
0.060000 -0.280000
-0.330000 -0.275000
-0.265000 -0.275000
-0.205000 -0.275000
-0.140000 -0.275000
-0.080000 -0.275000
-0.015000 -0.275000
0.050000 -0.275000
0.110000 -0.275000
-0.295000 -0.270000
-0.235000 -0.270000
-0.170000 -0.270000
-0.105000 -0.270000
-0.045000 -0.270000
0.020000 -0.270000
0.080000 -0.270000
0.145000 -0.270000
-0.290000 -0.265000
-0.225000 -0.265000
-0.160000 -0.265000
-0.100000 -0.265000
-0.035000 -0.265000
0.025000 -0.265000
0.090000 -0.265000
0.155000 -0.265000
-0.305000 -0.260000
-0.240000 -0.260000
-0.180000 -0.260000
-0.055000 -0.260000
0.010000 -0.260000
0.070000 -0.260000
0.135000 -0.260000
-0.340000 -0.255000
-0.275000 -0.255000
-0.215000 -0.255000
-0.150000 -0.255000
0.010000 -0.255000
0.070000 -0.255000
0.135000 -0.255000
-0.350000 -0.250000
-0.285000 -0.250000
-0.220000 -0.250000
-0.160000 -0.250000
0.030000 -0.250000
0.090000 -0.250000
0.155000 -0.250000
-0.325000 -0.245000
-0.265000 -0.245000
-0.200000 -0.245000
0.005000 -0.245000
0.070000 -0.245000
0.130000 -0.245000
-0.355000 -0.240000
-0.290000 -0.240000
-0.230000 -0.240000
0.000000 -0.240000
0.060000 -0.240000
0.125000 -0.240000
-0.360000 -0.235000
-0.300000 -0.235000
-0.235000 -0.235000
0.005000 -0.235000
0.070000 -0.235000
0.130000 -0.235000
-0.350000 -0.230000
-0.285000 -0.230000
-0.225000 -0.230000
0.035000 -0.230000
0.095000 -0.230000
0.160000 -0.230000
-0.320000 -0.225000
-0.260000 -0.225000
0.010000 -0.225000
0.070000 -0.225000
0.135000 -0.225000
-0.345000 -0.220000
-0.285000 -0.220000
-0.220000 -0.220000
0.060000 -0.220000
0.125000 -0.220000
-0.355000 -0.215000
-0.290000 -0.215000
-0.225000 -0.215000
0.065000 -0.215000
0.130000 -0.215000
-0.350000 -0.210000
-0.285000 -0.210000
0.025000 -0.210000
0.085000 -0.210000
0.150000 -0.210000
-0.330000 -0.205000
-0.265000 -0.205000
0.060000 -0.205000
0.120000 -0.205000
-0.345000 -0.200000
-0.285000 -0.200000
0.055000 -0.200000
0.115000 -0.200000
-0.345000 -0.195000
-0.280000 -0.195000
0.070000 -0.195000
-0.375000 -0.190000
-0.315000 -0.190000
-0.395000 -0.185000
-0.330000 -0.185000
-0.270000 -0.185000
-0.345000 -0.180000
-0.285000 -0.180000
-0.360000 -0.175000
-0.300000 -0.175000
-0.375000 -0.170000
-0.310000 -0.170000
-0.390000 -0.165000
-0.325000 -0.165000
-0.400000 -0.160000
-0.335000 -0.160000
-0.405000 -0.155000
-0.345000 -0.155000
-0.280000 -0.155000
-0.355000 -0.150000
-0.290000 -0.150000
-0.360000 -0.145000
-0.300000 -0.145000
-0.365000 -0.140000
-0.305000 -0.140000
-0.370000 -0.135000
-0.310000 -0.135000
-0.375000 -0.130000
-0.310000 -0.130000
-0.380000 -0.125000
-0.315000 -0.125000
-0.380000 -0.120000
-0.315000 -0.120000
-0.380000 -0.115000
-0.320000 -0.115000
-0.385000 -0.110000
-0.325000 -0.110000
-0.385000 -0.105000
-0.325000 -0.105000
-0.385000 -0.100000
-0.320000 -0.100000
-0.390000 -0.095000
-0.325000 -0.095000
-0.390000 -0.090000
-0.325000 -0.090000
-0.385000 -0.085000
-0.325000 -0.085000
-0.385000 -0.080000
-0.325000 -0.080000
-0.385000 -0.075000
-0.320000 -0.075000
-0.385000 -0.070000
-0.320000 -0.070000
-0.435000 -0.065000
-0.370000 -0.065000
0.255000 -0.065000
0.320000 -0.065000
0.385000 -0.065000
-0.390000 -0.060000
-0.325000 -0.060000
0.270000 -0.060000
0.335000 -0.060000
0.400000 -0.060000
-0.385000 -0.055000
-0.320000 -0.055000
0.250000 -0.055000
0.315000 -0.055000
0.380000 -0.055000
-0.415000 -0.050000
-0.350000 -0.050000
0.195000 -0.050000
0.260000 -0.050000
0.320000 -0.050000
0.385000 -0.050000
-0.410000 -0.045000
-0.350000 -0.045000
0.180000 -0.045000
0.240000 -0.045000
0.305000 -0.045000
0.370000 -0.045000
-0.440000 -0.040000
-0.375000 -0.040000
-0.315000 -0.040000
0.200000 -0.040000
0.260000 -0.040000
0.325000 -0.040000
0.390000 -0.040000
-0.420000 -0.035000
-0.355000 -0.035000
0.135000 -0.035000
0.200000 -0.035000
0.265000 -0.035000
0.325000 -0.035000
0.390000 -0.035000
-0.425000 -0.030000
-0.360000 -0.030000
0.120000 -0.030000
0.180000 -0.030000
0.245000 -0.030000
0.305000 -0.030000
0.370000 -0.030000
0.430000 -0.030000
-0.380000 -0.025000
-0.315000 -0.025000
0.145000 -0.025000
0.210000 -0.025000
0.270000 -0.025000
0.335000 -0.025000
0.400000 -0.025000
-0.415000 -0.020000
-0.350000 -0.020000
0.095000 -0.020000
0.160000 -0.020000
0.220000 -0.020000
0.285000 -0.020000
0.350000 -0.020000
0.410000 -0.020000
-0.400000 -0.015000
-0.340000 -0.015000
0.095000 -0.015000
0.160000 -0.015000
0.220000 -0.015000
0.285000 -0.015000
0.345000 -0.015000
0.410000 -0.015000
-0.405000 -0.010000
-0.345000 -0.010000
0.075000 -0.010000
0.135000 -0.010000
0.200000 -0.010000
0.260000 -0.010000
0.325000 -0.010000
0.390000 -0.010000
-0.430000 -0.005000
-0.365000 -0.005000
0.030000 -0.005000
0.095000 -0.005000
0.160000 -0.005000
0.220000 -0.005000
0.285000 -0.005000
0.345000 -0.005000
0.410000 -0.005000
-0.400000 0.000000
-0.340000 0.000000
0.045000 0.000000
0.105000 0.000000
0.170000 0.000000
0.230000 0.000000
0.295000 0.000000
0.360000 0.000000
0.420000 0.000000
-0.390000 0.005000
-0.330000 0.005000
0.045000 0.005000
0.110000 0.005000
0.170000 0.005000
0.235000 0.005000
0.295000 0.005000
0.360000 0.005000
0.420000 0.005000
-0.390000 0.010000
-0.325000 0.010000
0.025000 0.010000
0.090000 0.010000
0.150000 0.010000
0.215000 0.010000
0.280000 0.010000
0.340000 0.010000
0.405000 0.010000
-0.410000 0.015000
-0.345000 0.015000
-0.005000 0.015000
0.055000 0.015000
0.120000 0.015000
0.180000 0.015000
0.245000 0.015000
0.305000 0.015000
0.370000 0.015000
-0.440000 0.020000
-0.380000 0.020000
-0.315000 0.020000
0.005000 0.020000
0.070000 0.020000
0.135000 0.020000
0.195000 0.020000
0.260000 0.020000
0.320000 0.020000
0.385000 0.020000
-0.425000 0.025000
-0.365000 0.025000
-0.300000 0.025000
0.005000 0.025000
0.070000 0.025000
0.130000 0.025000
0.195000 0.025000
0.260000 0.025000
0.320000 0.025000
0.415000 0.025000
-0.400000 0.030000
-0.335000 0.030000
-0.040000 0.030000
0.020000 0.030000
0.085000 0.030000
0.145000 0.030000
0.210000 0.030000
0.270000 0.030000
0.335000 0.030000
-0.420000 0.035000
-0.360000 0.035000
-0.295000 0.035000
-0.025000 0.035000
0.040000 0.035000
0.105000 0.035000
0.165000 0.035000
0.230000 0.035000
0.290000 0.035000
0.430000 0.035000
-0.375000 0.040000
-0.315000 0.040000
-0.060000 0.040000
0.000000 0.040000
0.065000 0.040000
0.125000 0.040000
0.190000 0.040000
0.255000 0.040000
0.405000 0.040000
-0.400000 0.045000
-0.340000 0.045000
-0.100000 0.045000
-0.035000 0.045000
0.025000 0.045000
0.090000 0.045000
0.150000 0.045000
0.215000 0.045000
0.275000 0.045000
-0.430000 0.050000
-0.365000 0.050000
-0.305000 0.050000
-0.090000 0.050000
-0.030000 0.050000
0.035000 0.050000
0.100000 0.050000
0.160000 0.050000
0.225000 0.050000
0.285000 0.050000
-0.405000 0.055000
-0.340000 0.055000
-0.280000 0.055000
-0.085000 0.055000
-0.025000 0.055000
0.040000 0.055000
0.100000 0.055000
0.165000 0.055000
0.230000 0.055000
0.405000 0.055000
-0.395000 0.060000
-0.335000 0.060000
-0.270000 0.060000
-0.110000 0.060000
-0.050000 0.060000
0.015000 0.060000
0.075000 0.060000
0.140000 0.060000
0.205000 0.060000
0.265000 0.060000
-0.410000 0.065000
-0.350000 0.065000
-0.285000 0.065000
-0.180000 0.065000
-0.115000 0.065000
-0.050000 0.065000
0.010000 0.065000
0.075000 0.065000
0.135000 0.065000
0.200000 0.065000
0.265000 0.065000
-0.410000 0.070000
-0.345000 0.070000
-0.285000 0.070000
-0.220000 0.070000
-0.160000 0.070000
-0.095000 0.070000
-0.030000 0.070000
0.030000 0.070000
0.095000 0.070000
0.155000 0.070000
0.220000 0.070000
0.420000 0.070000
-0.375000 0.075000
-0.310000 0.075000
-0.250000 0.075000
-0.185000 0.075000
-0.120000 0.075000
-0.060000 0.075000
0.005000 0.075000
0.065000 0.075000
0.130000 0.075000
0.190000 0.075000
0.395000 0.075000
-0.395000 0.080000
-0.335000 0.080000
-0.270000 0.080000
-0.210000 0.080000
-0.145000 0.080000
-0.080000 0.080000
-0.020000 0.080000
0.045000 0.080000
0.105000 0.080000
0.170000 0.080000
0.230000 0.080000
-0.415000 0.085000
-0.350000 0.085000
-0.290000 0.085000
-0.225000 0.085000
-0.165000 0.085000
-0.100000 0.085000
-0.035000 0.085000
0.025000 0.085000
0.090000 0.085000
0.150000 0.085000
0.215000 0.085000
-0.425000 0.090000
-0.365000 0.090000
-0.300000 0.090000
-0.240000 0.090000
-0.175000 0.090000
-0.115000 0.090000
-0.050000 0.090000
0.015000 0.090000
0.075000 0.090000
0.140000 0.090000
0.200000 0.090000
0.410000 0.090000
-0.375000 0.095000
-0.315000 0.095000
-0.250000 0.095000
-0.190000 0.095000
-0.125000 0.095000
-0.060000 0.095000
0.000000 0.095000
0.065000 0.095000
0.125000 0.095000
0.190000 0.095000
0.410000 0.095000
-0.365000 0.100000
-0.300000 0.100000
-0.240000 0.100000
-0.175000 0.100000
-0.115000 0.100000
-0.050000 0.100000
0.015000 0.100000
0.075000 0.100000
0.140000 0.100000
0.200000 0.100000
-0.415000 0.105000
-0.355000 0.105000
-0.290000 0.105000
-0.225000 0.105000
-0.165000 0.105000
-0.100000 0.105000
-0.040000 0.105000
0.025000 0.105000
0.090000 0.105000
0.150000 0.105000
0.335000 0.105000
-0.400000 0.110000
-0.335000 0.110000
-0.270000 0.110000
-0.210000 0.110000
-0.145000 0.110000
-0.085000 0.110000
-0.020000 0.110000
0.040000 0.110000
0.105000 0.110000
0.170000 0.110000
0.395000 0.110000
-0.370000 0.115000
-0.310000 0.115000
-0.245000 0.115000
-0.180000 0.115000
-0.120000 0.115000
-0.055000 0.115000
0.005000 0.115000
0.070000 0.115000
0.135000 0.115000
0.195000 0.115000
-0.405000 0.120000
-0.345000 0.120000
-0.280000 0.120000
-0.220000 0.120000
-0.155000 0.120000
-0.090000 0.120000
-0.030000 0.120000
0.035000 0.120000
0.095000 0.120000
0.160000 0.120000
0.390000 0.120000
-0.375000 0.125000
-0.310000 0.125000
-0.250000 0.125000
-0.185000 0.125000
-0.125000 0.125000
-0.060000 0.125000
0.005000 0.125000
0.065000 0.125000
0.130000 0.125000
0.270000 0.125000
-0.395000 0.130000
-0.330000 0.130000
-0.270000 0.130000
-0.205000 0.130000
-0.145000 0.130000
-0.080000 0.130000
-0.015000 0.130000
0.045000 0.130000
0.120000 0.130000
0.180000 0.130000
-0.400000 0.135000
-0.340000 0.135000
-0.275000 0.135000
-0.210000 0.135000
-0.150000 0.135000
-0.085000 0.135000
-0.025000 0.135000
0.040000 0.135000
0.140000 0.135000
0.370000 0.135000
-0.380000 0.140000
-0.320000 0.140000
-0.255000 0.140000
-0.195000 0.140000
-0.130000 0.140000
-0.065000 0.140000
-0.005000 0.140000
0.100000 0.140000
0.160000 0.140000
-0.405000 0.145000
-0.340000 0.145000
-0.280000 0.145000
-0.215000 0.145000
-0.155000 0.145000
-0.090000 0.145000
-0.025000 0.145000
0.035000 0.145000
0.145000 0.145000
0.385000 0.145000
-0.355000 0.150000
-0.295000 0.150000
-0.230000 0.150000
-0.165000 0.150000
-0.105000 0.150000
-0.040000 0.150000
0.020000 0.150000
0.130000 0.150000
0.370000 0.150000
-0.365000 0.155000
-0.300000 0.155000
-0.240000 0.155000
-0.175000 0.155000
-0.110000 0.155000
-0.050000 0.155000
0.015000 0.155000
0.115000 0.155000
0.355000 0.155000
-0.380000 0.160000
-0.315000 0.160000
-0.250000 0.160000
-0.190000 0.160000
-0.125000 0.160000
-0.065000 0.160000
0.000000 0.160000
0.095000 0.160000
0.155000 0.160000
-0.390000 0.165000
-0.330000 0.165000
-0.265000 0.165000
-0.205000 0.165000
-0.140000 0.165000
-0.075000 0.165000
-0.015000 0.165000
0.050000 0.165000
0.110000 0.165000
0.350000 0.165000
-0.370000 0.170000
-0.310000 0.170000
-0.245000 0.170000
-0.185000 0.170000
-0.120000 0.170000
-0.055000 0.170000
0.005000 0.170000
0.070000 0.170000
0.130000 0.170000
0.370000 0.170000
-0.350000 0.175000
-0.285000 0.175000
-0.220000 0.175000
-0.160000 0.175000
-0.095000 0.175000
-0.035000 0.175000
0.030000 0.175000
0.095000 0.175000
0.295000 0.175000
-0.370000 0.180000
-0.310000 0.180000
-0.245000 0.180000
-0.185000 0.180000
-0.120000 0.180000
-0.055000 0.180000
0.005000 0.180000
0.070000 0.180000
0.130000 0.180000
-0.390000 0.185000
-0.325000 0.185000
-0.265000 0.185000
-0.200000 0.185000
-0.140000 0.185000
-0.075000 0.185000
-0.010000 0.185000
0.050000 0.185000
0.115000 0.185000
0.345000 0.185000
-0.350000 0.190000
-0.290000 0.190000
-0.225000 0.190000
-0.160000 0.190000
-0.100000 0.190000
-0.035000 0.190000
0.025000 0.190000
0.090000 0.190000
0.285000 0.190000
-0.365000 0.195000
-0.300000 0.195000
-0.240000 0.195000
-0.175000 0.195000
-0.110000 0.195000
-0.050000 0.195000
0.015000 0.195000
0.075000 0.195000
0.220000 0.195000
-0.380000 0.200000
-0.315000 0.200000
-0.250000 0.200000
-0.190000 0.200000
-0.125000 0.200000
-0.065000 0.200000
0.000000 0.200000
0.065000 0.200000
0.205000 0.200000
-0.380000 0.205000
-0.320000 0.205000
-0.255000 0.205000
-0.195000 0.205000
-0.130000 0.205000
-0.065000 0.205000
-0.005000 0.205000
0.060000 0.205000
0.200000 0.205000
0.350000 0.205000
-0.315000 0.210000
-0.255000 0.210000
-0.190000 0.210000
-0.130000 0.210000
-0.065000 0.210000
0.000000 0.210000
0.060000 0.210000
0.200000 0.210000
-0.375000 0.215000
-0.310000 0.215000
-0.250000 0.215000
-0.185000 0.215000
-0.120000 0.215000
-0.060000 0.215000
0.005000 0.215000
0.065000 0.215000
0.260000 0.215000
-0.360000 0.220000
-0.300000 0.220000
-0.235000 0.220000
-0.175000 0.220000
-0.110000 0.220000
-0.045000 0.220000
0.015000 0.220000
0.080000 0.220000
0.265000 0.220000
-0.345000 0.225000
-0.285000 0.225000
-0.220000 0.225000
-0.155000 0.225000
-0.095000 0.225000
-0.030000 0.225000
0.030000 0.225000
0.095000 0.225000
0.320000 0.225000
-0.325000 0.230000
-0.260000 0.230000
-0.200000 0.230000
-0.135000 0.230000
-0.075000 0.230000
-0.010000 0.230000
0.055000 0.230000
0.180000 0.230000
-0.355000 0.235000
-0.295000 0.235000
-0.230000 0.235000
-0.165000 0.235000
-0.105000 0.235000
-0.040000 0.235000
0.020000 0.235000
0.085000 0.235000
0.310000 0.235000
-0.315000 0.240000
-0.250000 0.240000
-0.190000 0.240000
-0.125000 0.240000
-0.065000 0.240000
0.000000 0.240000
0.065000 0.240000
0.240000 0.240000
-0.330000 0.245000
-0.270000 0.245000
-0.205000 0.245000
-0.140000 0.245000
-0.080000 0.245000
-0.015000 0.245000
0.045000 0.245000
0.230000 0.245000
-0.340000 0.250000
-0.275000 0.250000
-0.210000 0.250000
-0.150000 0.250000
-0.085000 0.250000
-0.020000 0.250000
0.045000 0.250000
0.225000 0.250000
-0.340000 0.255000
-0.275000 0.255000
-0.215000 0.255000
-0.150000 0.255000
-0.085000 0.255000
-0.020000 0.255000
0.045000 0.255000
0.215000 0.255000
-0.335000 0.260000
-0.275000 0.260000
-0.210000 0.260000
-0.145000 0.260000
-0.085000 0.260000
-0.015000 0.260000
0.045000 0.260000
0.220000 0.260000
-0.315000 0.265000
-0.255000 0.265000
-0.190000 0.265000
-0.130000 0.265000
-0.055000 0.265000
0.010000 0.265000
0.070000 0.265000
0.280000 0.265000
-0.285000 0.270000
-0.220000 0.270000
-0.160000 0.270000
-0.095000 0.270000
-0.020000 0.270000
0.040000 0.270000
0.255000 0.270000
-0.300000 0.275000
-0.235000 0.275000
-0.170000 0.275000
-0.110000 0.275000
-0.030000 0.275000
0.030000 0.275000
0.245000 0.275000
-0.295000 0.280000
-0.230000 0.280000
-0.165000 0.280000
-0.105000 0.280000
-0.025000 0.280000
0.035000 0.280000
0.250000 0.280000
-0.270000 0.285000
-0.210000 0.285000
-0.145000 0.285000
-0.065000 0.285000
0.000000 0.285000
0.170000 0.285000
0.270000 0.285000
-0.235000 0.290000
-0.175000 0.290000
-0.085000 0.290000
-0.025000 0.290000
0.040000 0.290000
0.245000 0.290000
-0.250000 0.295000
-0.185000 0.295000
-0.090000 0.295000
-0.025000 0.295000
0.040000 0.295000
0.230000 0.295000
-0.235000 0.300000
-0.175000 0.300000
-0.065000 0.300000
-0.005000 0.300000
0.145000 0.300000
0.255000 0.300000
-0.190000 0.305000
-0.070000 0.305000
-0.010000 0.305000
0.135000 0.305000
0.245000 0.305000
-0.070000 0.310000
-0.005000 0.310000
0.130000 0.310000
-0.130000 0.315000
-0.065000 0.315000
-0.005000 0.315000
0.180000 0.315000
-0.140000 0.320000
-0.075000 0.320000
-0.015000 0.320000
0.105000 0.320000
-0.150000 0.325000
-0.090000 0.325000
-0.025000 0.325000
0.080000 0.325000
0.210000 0.325000
-0.110000 0.330000
-0.050000 0.330000
0.145000 0.330000
0.205000 0.330000
-0.125000 0.335000
-0.060000 0.335000
0.000000 0.335000
0.190000 0.335000
-0.155000 0.340000
-0.090000 0.340000
-0.030000 0.340000
0.150000 0.340000
-0.190000 0.345000
-0.130000 0.345000
-0.065000 0.345000
-0.005000 0.345000
0.160000 0.345000
-0.170000 0.350000
-0.110000 0.350000
-0.045000 0.350000
0.110000 0.350000
-0.200000 0.355000
-0.140000 0.355000
-0.075000 0.355000
0.070000 0.355000
0.130000 0.355000
-0.155000 0.360000
-0.095000 0.360000
-0.030000 0.360000
0.085000 0.360000
0.145000 0.360000
-0.115000 0.365000
-0.055000 0.365000
0.010000 0.365000
0.075000 0.365000
-0.160000 0.370000
-0.095000 0.370000
-0.035000 0.370000
0.030000 0.370000
0.090000 0.370000
-0.110000 0.375000
-0.045000 0.375000
0.015000 0.375000
0.080000 0.375000
-0.095000 0.380000
-0.030000 0.380000
0.035000 0.380000
-0.095000 0.385000
-0.030000 0.385000
0.030000 0.385000
-0.040000 0.390000
1 -0.100000 -0.390000
2 -0.040000 -0.390000
3 -0.090000 -0.385000
4 -0.030000 -0.385000
5 -0.125000 -0.380000
6 -0.065000 -0.380000
7 0.000000 -0.380000
8 -0.135000 -0.375000
9 -0.075000 -0.375000
10 -0.010000 -0.375000
11 -0.185000 -0.370000
12 -0.120000 -0.370000
13 -0.055000 -0.370000
14 0.005000 -0.370000
15 -0.185000 -0.365000
16 -0.125000 -0.365000
17 -0.060000 -0.365000
18 0.000000 -0.365000
19 -0.210000 -0.360000
20 -0.145000 -0.360000
21 -0.085000 -0.360000
22 -0.020000 -0.360000
23 0.040000 -0.360000
24 -0.175000 -0.355000
25 -0.110000 -0.355000
26 -0.050000 -0.355000
27 0.015000 -0.355000
28 -0.215000 -0.350000
29 -0.150000 -0.350000
30 -0.085000 -0.350000
31 -0.025000 -0.350000
32 0.040000 -0.350000
33 -0.195000 -0.345000
34 -0.130000 -0.345000
35 -0.070000 -0.345000
36 -0.005000 -0.345000
37 -0.235000 -0.340000
38 -0.175000 -0.340000
39 -0.110000 -0.340000
40 -0.050000 -0.340000
41 0.015000 -0.340000
42 -0.220000 -0.335000
43 -0.160000 -0.335000
44 -0.095000 -0.335000
45 -0.035000 -0.335000
46 0.030000 -0.335000
47 -0.220000 -0.330000
48 -0.155000 -0.330000
49 -0.090000 -0.330000
50 -0.030000 -0.330000
51 0.035000 -0.330000
52 -0.215000 -0.325000
53 -0.150000 -0.325000
54 -0.085000 -0.325000
55 -0.025000 -0.325000
56 -0.270000 -0.320000
57 -0.210000 -0.320000
58 -0.145000 -0.320000
59 -0.080000 -0.320000
60 -0.020000 -0.320000
61 -0.265000 -0.315000
62 -0.205000 -0.315000
63 -0.140000 -0.315000
64 -0.080000 -0.315000
65 -0.015000 -0.315000
66 -0.260000 -0.310000
67 -0.200000 -0.310000
68 -0.135000 -0.310000
69 -0.075000 -0.310000
70 -0.010000 -0.310000
71 -0.250000 -0.305000
72 -0.190000 -0.305000
73 -0.125000 -0.305000
74 -0.065000 -0.305000
75 -0.305000 -0.300000
76 -0.240000 -0.300000
77 -0.180000 -0.300000
78 -0.115000 -0.300000
79 -0.055000 -0.300000
80 -0.285000 -0.295000
81 -0.225000 -0.295000
82 -0.160000 -0.295000
83 -0.095000 -0.295000
84 -0.035000 -0.295000
85 0.035000 -0.295000
86 -0.295000 -0.290000
87 -0.230000 -0.290000
88 -0.165000 -0.290000
89 -0.105000 -0.290000
90 -0.040000 -0.290000
91 0.020000 -0.290000
92 0.085000 -0.290000
93 -0.270000 -0.285000
94 -0.205000 -0.285000
95 -0.140000 -0.285000
96 -0.080000 -0.285000
97 -0.015000 -0.285000
98 0.045000 -0.285000
99 -0.320000 -0.280000
100 -0.255000 -0.280000
101 -0.195000 -0.280000
102 -0.130000 -0.280000
103 -0.070000 -0.280000
104 -0.005000 -0.280000
105 0.060000 -0.280000
106 -0.330000 -0.275000
107 -0.265000 -0.275000
108 -0.205000 -0.275000
109 -0.140000 -0.275000
110 -0.080000 -0.275000
111 -0.015000 -0.275000
112 0.050000 -0.275000
113 0.110000 -0.275000
114 -0.295000 -0.270000
115 -0.235000 -0.270000
116 -0.170000 -0.270000
117 -0.105000 -0.270000
118 -0.045000 -0.270000
119 0.020000 -0.270000
120 0.080000 -0.270000
121 0.145000 -0.270000
122 -0.290000 -0.265000
123 -0.225000 -0.265000
124 -0.160000 -0.265000
125 -0.100000 -0.265000
126 -0.035000 -0.265000
127 0.025000 -0.265000
128 0.090000 -0.265000
129 0.155000 -0.265000
130 -0.305000 -0.260000
131 -0.240000 -0.260000
132 -0.180000 -0.260000
133 -0.055000 -0.260000
134 0.010000 -0.260000
135 0.070000 -0.260000
136 0.135000 -0.260000
137 -0.340000 -0.255000
138 -0.275000 -0.255000
139 -0.215000 -0.255000
140 -0.150000 -0.255000
141 0.010000 -0.255000
142 0.070000 -0.255000
143 0.135000 -0.255000
144 -0.350000 -0.250000
145 -0.285000 -0.250000
146 -0.220000 -0.250000
147 -0.160000 -0.250000
148 0.030000 -0.250000
149 0.090000 -0.250000
150 0.155000 -0.250000
151 -0.325000 -0.245000
152 -0.265000 -0.245000
153 -0.200000 -0.245000
154 0.005000 -0.245000
155 0.070000 -0.245000
156 0.130000 -0.245000
157 -0.355000 -0.240000
158 -0.290000 -0.240000
159 -0.230000 -0.240000
160 0.000000 -0.240000
161 0.060000 -0.240000
162 0.125000 -0.240000
163 -0.360000 -0.235000
164 -0.300000 -0.235000
165 -0.235000 -0.235000
166 0.005000 -0.235000
167 0.070000 -0.235000
168 0.130000 -0.235000
169 -0.350000 -0.230000
170 -0.285000 -0.230000
171 -0.225000 -0.230000
172 0.035000 -0.230000
173 0.095000 -0.230000
174 0.160000 -0.230000
175 -0.320000 -0.225000
176 -0.260000 -0.225000
177 0.010000 -0.225000
178 0.070000 -0.225000
179 0.135000 -0.225000
180 -0.345000 -0.220000
181 -0.285000 -0.220000
182 -0.220000 -0.220000
183 0.060000 -0.220000
184 0.125000 -0.220000
185 -0.355000 -0.215000
186 -0.290000 -0.215000
187 -0.225000 -0.215000
188 0.065000 -0.215000
189 0.130000 -0.215000
190 -0.350000 -0.210000
191 -0.285000 -0.210000
192 0.025000 -0.210000
193 0.085000 -0.210000
194 0.150000 -0.210000
195 -0.330000 -0.205000
196 -0.265000 -0.205000
197 0.060000 -0.205000
198 0.120000 -0.205000
199 -0.345000 -0.200000
200 -0.285000 -0.200000
201 0.055000 -0.200000
202 0.115000 -0.200000
203 -0.345000 -0.195000
204 -0.280000 -0.195000
205 0.070000 -0.195000
206 -0.375000 -0.190000
207 -0.315000 -0.190000
208 -0.395000 -0.185000
209 -0.330000 -0.185000
210 -0.270000 -0.185000
211 -0.345000 -0.180000
212 -0.285000 -0.180000
213 -0.360000 -0.175000
214 -0.300000 -0.175000
215 -0.375000 -0.170000
216 -0.310000 -0.170000
217 -0.390000 -0.165000
218 -0.325000 -0.165000
219 -0.400000 -0.160000
220 -0.335000 -0.160000
221 -0.405000 -0.155000
222 -0.345000 -0.155000
223 -0.280000 -0.155000
224 -0.355000 -0.150000
225 -0.290000 -0.150000
226 -0.360000 -0.145000
227 -0.300000 -0.145000
228 -0.365000 -0.140000
229 -0.305000 -0.140000
230 -0.370000 -0.135000
231 -0.310000 -0.135000
232 -0.375000 -0.130000
233 -0.310000 -0.130000
234 -0.380000 -0.125000
235 -0.315000 -0.125000
236 -0.380000 -0.120000
237 -0.315000 -0.120000
238 -0.380000 -0.115000
239 -0.320000 -0.115000
240 -0.385000 -0.110000
241 -0.325000 -0.110000
242 -0.385000 -0.105000
243 -0.325000 -0.105000
244 -0.385000 -0.100000
245 -0.320000 -0.100000
246 -0.390000 -0.095000
247 -0.325000 -0.095000
248 -0.390000 -0.090000
249 -0.325000 -0.090000
250 -0.385000 -0.085000
251 -0.325000 -0.085000
252 -0.385000 -0.080000
253 -0.325000 -0.080000
254 -0.385000 -0.075000
255 -0.320000 -0.075000
256 -0.385000 -0.070000
257 -0.320000 -0.070000
258 -0.435000 -0.065000
259 -0.370000 -0.065000
260 0.255000 -0.065000
261 0.320000 -0.065000
262 0.385000 -0.065000
263 -0.390000 -0.060000
264 -0.325000 -0.060000
265 0.270000 -0.060000
266 0.335000 -0.060000
267 0.400000 -0.060000
268 -0.385000 -0.055000
269 -0.320000 -0.055000
270 0.250000 -0.055000
271 0.315000 -0.055000
272 0.380000 -0.055000
273 -0.415000 -0.050000
274 -0.350000 -0.050000
275 0.195000 -0.050000
276 0.260000 -0.050000
277 0.320000 -0.050000
278 0.385000 -0.050000
279 -0.410000 -0.045000
280 -0.350000 -0.045000
281 0.180000 -0.045000
282 0.240000 -0.045000
283 0.305000 -0.045000
284 0.370000 -0.045000
285 -0.440000 -0.040000
286 -0.375000 -0.040000
287 -0.315000 -0.040000
288 0.200000 -0.040000
289 0.260000 -0.040000
290 0.325000 -0.040000
291 0.390000 -0.040000
292 -0.420000 -0.035000
293 -0.355000 -0.035000
294 0.135000 -0.035000
295 0.200000 -0.035000
296 0.265000 -0.035000
297 0.325000 -0.035000
298 0.390000 -0.035000
299 -0.425000 -0.030000
300 -0.360000 -0.030000
301 0.120000 -0.030000
302 0.180000 -0.030000
303 0.245000 -0.030000
304 0.305000 -0.030000
305 0.370000 -0.030000
306 0.430000 -0.030000
307 -0.380000 -0.025000
308 -0.315000 -0.025000
309 0.145000 -0.025000
310 0.210000 -0.025000
311 0.270000 -0.025000
312 0.335000 -0.025000
313 0.400000 -0.025000
314 -0.415000 -0.020000
315 -0.350000 -0.020000
316 0.095000 -0.020000
317 0.160000 -0.020000
318 0.220000 -0.020000
319 0.285000 -0.020000
320 0.350000 -0.020000
321 0.410000 -0.020000
322 -0.400000 -0.015000
323 -0.340000 -0.015000
324 0.095000 -0.015000
325 0.160000 -0.015000
326 0.220000 -0.015000
327 0.285000 -0.015000
328 0.345000 -0.015000
329 0.410000 -0.015000
330 -0.405000 -0.010000
331 -0.345000 -0.010000
332 0.075000 -0.010000
333 0.135000 -0.010000
334 0.200000 -0.010000
335 0.260000 -0.010000
336 0.325000 -0.010000
337 0.390000 -0.010000
338 -0.430000 -0.005000
339 -0.365000 -0.005000
340 0.030000 -0.005000
341 0.095000 -0.005000
342 0.160000 -0.005000
343 0.220000 -0.005000
344 0.285000 -0.005000
345 0.345000 -0.005000
346 0.410000 -0.005000
347 -0.400000 0.000000
348 -0.340000 0.000000
349 0.045000 0.000000
350 0.105000 0.000000
351 0.170000 0.000000
352 0.230000 0.000000
353 0.295000 0.000000
354 0.360000 0.000000
355 0.420000 0.000000
356 -0.390000 0.005000
357 -0.330000 0.005000
358 0.045000 0.005000
359 0.110000 0.005000
360 0.170000 0.005000
361 0.235000 0.005000
362 0.295000 0.005000
363 0.360000 0.005000
364 0.420000 0.005000
365 -0.390000 0.010000
366 -0.325000 0.010000
367 0.025000 0.010000
368 0.090000 0.010000
369 0.150000 0.010000
370 0.215000 0.010000
371 0.280000 0.010000
372 0.340000 0.010000
373 0.405000 0.010000
374 -0.410000 0.015000
375 -0.345000 0.015000
376 -0.005000 0.015000
377 0.055000 0.015000
378 0.120000 0.015000
379 0.180000 0.015000
380 0.245000 0.015000
381 0.305000 0.015000
382 0.370000 0.015000
383 -0.440000 0.020000
384 -0.380000 0.020000
385 -0.315000 0.020000
386 0.005000 0.020000
387 0.070000 0.020000
388 0.135000 0.020000
389 0.195000 0.020000
390 0.260000 0.020000
391 0.320000 0.020000
392 0.385000 0.020000
393 -0.425000 0.025000
394 -0.365000 0.025000
395 -0.300000 0.025000
396 0.005000 0.025000
397 0.070000 0.025000
398 0.130000 0.025000
399 0.195000 0.025000
400 0.260000 0.025000
401 0.320000 0.025000
402 0.415000 0.025000
403 -0.400000 0.030000
404 -0.335000 0.030000
405 -0.040000 0.030000
406 0.020000 0.030000
407 0.085000 0.030000
408 0.145000 0.030000
409 0.210000 0.030000
410 0.270000 0.030000
411 0.335000 0.030000
412 -0.420000 0.035000
413 -0.360000 0.035000
414 -0.295000 0.035000
415 -0.025000 0.035000
416 0.040000 0.035000
417 0.105000 0.035000
418 0.165000 0.035000
419 0.230000 0.035000
420 0.290000 0.035000
421 0.430000 0.035000
422 -0.375000 0.040000
423 -0.315000 0.040000
424 -0.060000 0.040000
425 0.000000 0.040000
426 0.065000 0.040000
427 0.125000 0.040000
428 0.190000 0.040000
429 0.255000 0.040000
430 0.405000 0.040000
431 -0.400000 0.045000
432 -0.340000 0.045000
433 -0.100000 0.045000
434 -0.035000 0.045000
435 0.025000 0.045000
436 0.090000 0.045000
437 0.150000 0.045000
438 0.215000 0.045000
439 0.275000 0.045000
440 -0.430000 0.050000
441 -0.365000 0.050000
442 -0.305000 0.050000
443 -0.090000 0.050000
444 -0.030000 0.050000
445 0.035000 0.050000
446 0.100000 0.050000
447 0.160000 0.050000
448 0.225000 0.050000
449 0.285000 0.050000
450 -0.405000 0.055000
451 -0.340000 0.055000
452 -0.280000 0.055000
453 -0.085000 0.055000
454 -0.025000 0.055000
455 0.040000 0.055000
456 0.100000 0.055000
457 0.165000 0.055000
458 0.230000 0.055000
459 0.405000 0.055000
460 -0.395000 0.060000
461 -0.335000 0.060000
462 -0.270000 0.060000
463 -0.110000 0.060000
464 -0.050000 0.060000
465 0.015000 0.060000
466 0.075000 0.060000
467 0.140000 0.060000
468 0.205000 0.060000
469 0.265000 0.060000
470 -0.410000 0.065000
471 -0.350000 0.065000
472 -0.285000 0.065000
473 -0.180000 0.065000
474 -0.115000 0.065000
475 -0.050000 0.065000
476 0.010000 0.065000
477 0.075000 0.065000
478 0.135000 0.065000
479 0.200000 0.065000
480 0.265000 0.065000
481 -0.410000 0.070000
482 -0.345000 0.070000
483 -0.285000 0.070000
484 -0.220000 0.070000
485 -0.160000 0.070000
486 -0.095000 0.070000
487 -0.030000 0.070000
488 0.030000 0.070000
489 0.095000 0.070000
490 0.155000 0.070000
491 0.220000 0.070000
492 0.420000 0.070000
493 -0.375000 0.075000
494 -0.310000 0.075000
495 -0.250000 0.075000
496 -0.185000 0.075000
497 -0.120000 0.075000
498 -0.060000 0.075000
499 0.005000 0.075000
500 0.065000 0.075000
501 0.130000 0.075000
502 0.190000 0.075000
503 0.395000 0.075000
504 -0.395000 0.080000
505 -0.335000 0.080000
506 -0.270000 0.080000
507 -0.210000 0.080000
508 -0.145000 0.080000
509 -0.080000 0.080000
510 -0.020000 0.080000
511 0.045000 0.080000
512 0.105000 0.080000
513 0.170000 0.080000
514 0.230000 0.080000
515 -0.415000 0.085000
516 -0.350000 0.085000
517 -0.290000 0.085000
518 -0.225000 0.085000
519 -0.165000 0.085000
520 -0.100000 0.085000
521 -0.035000 0.085000
522 0.025000 0.085000
523 0.090000 0.085000
524 0.150000 0.085000
525 0.215000 0.085000
526 -0.425000 0.090000
527 -0.365000 0.090000
528 -0.300000 0.090000
529 -0.240000 0.090000
530 -0.175000 0.090000
531 -0.115000 0.090000
532 -0.050000 0.090000
533 0.015000 0.090000
534 0.075000 0.090000
535 0.140000 0.090000
536 0.200000 0.090000
537 0.410000 0.090000
538 -0.375000 0.095000
539 -0.315000 0.095000
540 -0.250000 0.095000
541 -0.190000 0.095000
542 -0.125000 0.095000
543 -0.060000 0.095000
544 0.000000 0.095000
545 0.065000 0.095000
546 0.125000 0.095000
547 0.190000 0.095000
548 0.410000 0.095000
549 -0.365000 0.100000
550 -0.300000 0.100000
551 -0.240000 0.100000
552 -0.175000 0.100000
553 -0.115000 0.100000
554 -0.050000 0.100000
555 0.015000 0.100000
556 0.075000 0.100000
557 0.140000 0.100000
558 0.200000 0.100000
559 -0.415000 0.105000
560 -0.355000 0.105000
561 -0.290000 0.105000
562 -0.225000 0.105000
563 -0.165000 0.105000
564 -0.100000 0.105000
565 -0.040000 0.105000
566 0.025000 0.105000
567 0.090000 0.105000
568 0.150000 0.105000
569 0.335000 0.105000
570 -0.400000 0.110000
571 -0.335000 0.110000
572 -0.270000 0.110000
573 -0.210000 0.110000
574 -0.145000 0.110000
575 -0.085000 0.110000
576 -0.020000 0.110000
577 0.040000 0.110000
578 0.105000 0.110000
579 0.170000 0.110000
580 0.395000 0.110000
581 -0.370000 0.115000
582 -0.310000 0.115000
583 -0.245000 0.115000
584 -0.180000 0.115000
585 -0.120000 0.115000
586 -0.055000 0.115000
587 0.005000 0.115000
588 0.070000 0.115000
589 0.135000 0.115000
590 0.195000 0.115000
591 -0.405000 0.120000
592 -0.345000 0.120000
593 -0.280000 0.120000
594 -0.220000 0.120000
595 -0.155000 0.120000
596 -0.090000 0.120000
597 -0.030000 0.120000
598 0.035000 0.120000
599 0.095000 0.120000
600 0.160000 0.120000
601 0.390000 0.120000
602 -0.375000 0.125000
603 -0.310000 0.125000
604 -0.250000 0.125000
605 -0.185000 0.125000
606 -0.125000 0.125000
607 -0.060000 0.125000
608 0.005000 0.125000
609 0.065000 0.125000
610 0.130000 0.125000
611 0.270000 0.125000
612 -0.395000 0.130000
613 -0.330000 0.130000
614 -0.270000 0.130000
615 -0.205000 0.130000
616 -0.145000 0.130000
617 -0.080000 0.130000
618 -0.015000 0.130000
619 0.045000 0.130000
620 0.120000 0.130000
621 0.180000 0.130000
622 -0.400000 0.135000
623 -0.340000 0.135000
624 -0.275000 0.135000
625 -0.210000 0.135000
626 -0.150000 0.135000
627 -0.085000 0.135000
628 -0.025000 0.135000
629 0.040000 0.135000
630 0.140000 0.135000
631 0.370000 0.135000
632 -0.380000 0.140000
633 -0.320000 0.140000
634 -0.255000 0.140000
635 -0.195000 0.140000
636 -0.130000 0.140000
637 -0.065000 0.140000
638 -0.005000 0.140000
639 0.100000 0.140000
640 0.160000 0.140000
641 -0.405000 0.145000
642 -0.340000 0.145000
643 -0.280000 0.145000
644 -0.215000 0.145000
645 -0.155000 0.145000
646 -0.090000 0.145000
647 -0.025000 0.145000
648 0.035000 0.145000
649 0.145000 0.145000
650 0.385000 0.145000
651 -0.355000 0.150000
652 -0.295000 0.150000
653 -0.230000 0.150000
654 -0.165000 0.150000
655 -0.105000 0.150000
656 -0.040000 0.150000
657 0.020000 0.150000
658 0.130000 0.150000
659 0.370000 0.150000
660 -0.365000 0.155000
661 -0.300000 0.155000
662 -0.240000 0.155000
663 -0.175000 0.155000
664 -0.110000 0.155000
665 -0.050000 0.155000
666 0.015000 0.155000
667 0.115000 0.155000
668 0.355000 0.155000
669 -0.380000 0.160000
670 -0.315000 0.160000
671 -0.250000 0.160000
672 -0.190000 0.160000
673 -0.125000 0.160000
674 -0.065000 0.160000
675 0.000000 0.160000
676 0.095000 0.160000
677 0.155000 0.160000
678 -0.390000 0.165000
679 -0.330000 0.165000
680 -0.265000 0.165000
681 -0.205000 0.165000
682 -0.140000 0.165000
683 -0.075000 0.165000
684 -0.015000 0.165000
685 0.050000 0.165000
686 0.110000 0.165000
687 0.350000 0.165000
688 -0.370000 0.170000
689 -0.310000 0.170000
690 -0.245000 0.170000
691 -0.185000 0.170000
692 -0.120000 0.170000
693 -0.055000 0.170000
694 0.005000 0.170000
695 0.070000 0.170000
696 0.130000 0.170000
697 0.370000 0.170000
698 -0.350000 0.175000
699 -0.285000 0.175000
700 -0.220000 0.175000
701 -0.160000 0.175000
702 -0.095000 0.175000
703 -0.035000 0.175000
704 0.030000 0.175000
705 0.095000 0.175000
706 0.295000 0.175000
707 -0.370000 0.180000
708 -0.310000 0.180000
709 -0.245000 0.180000
710 -0.185000 0.180000
711 -0.120000 0.180000
712 -0.055000 0.180000
713 0.005000 0.180000
714 0.070000 0.180000
715 0.130000 0.180000
716 -0.390000 0.185000
717 -0.325000 0.185000
718 -0.265000 0.185000
719 -0.200000 0.185000
720 -0.140000 0.185000
721 -0.075000 0.185000
722 -0.010000 0.185000
723 0.050000 0.185000
724 0.115000 0.185000
725 0.345000 0.185000
726 -0.350000 0.190000
727 -0.290000 0.190000
728 -0.225000 0.190000
729 -0.160000 0.190000
730 -0.100000 0.190000
731 -0.035000 0.190000
732 0.025000 0.190000
733 0.090000 0.190000
734 0.285000 0.190000
735 -0.365000 0.195000
736 -0.300000 0.195000
737 -0.240000 0.195000
738 -0.175000 0.195000
739 -0.110000 0.195000
740 -0.050000 0.195000
741 0.015000 0.195000
742 0.075000 0.195000
743 0.220000 0.195000
744 -0.380000 0.200000
745 -0.315000 0.200000
746 -0.250000 0.200000
747 -0.190000 0.200000
748 -0.125000 0.200000
749 -0.065000 0.200000
750 0.000000 0.200000
751 0.065000 0.200000
752 0.205000 0.200000
753 -0.380000 0.205000
754 -0.320000 0.205000
755 -0.255000 0.205000
756 -0.195000 0.205000
757 -0.130000 0.205000
758 -0.065000 0.205000
759 -0.005000 0.205000
760 0.060000 0.205000
761 0.200000 0.205000
762 0.350000 0.205000
763 -0.315000 0.210000
764 -0.255000 0.210000
765 -0.190000 0.210000
766 -0.130000 0.210000
767 -0.065000 0.210000
768 0.000000 0.210000
769 0.060000 0.210000
770 0.200000 0.210000
771 -0.375000 0.215000
772 -0.310000 0.215000
773 -0.250000 0.215000
774 -0.185000 0.215000
775 -0.120000 0.215000
776 -0.060000 0.215000
777 0.005000 0.215000
778 0.065000 0.215000
779 0.260000 0.215000
780 -0.360000 0.220000
781 -0.300000 0.220000
782 -0.235000 0.220000
783 -0.175000 0.220000
784 -0.110000 0.220000
785 -0.045000 0.220000
786 0.015000 0.220000
787 0.080000 0.220000
788 0.265000 0.220000
789 -0.345000 0.225000
790 -0.285000 0.225000
791 -0.220000 0.225000
792 -0.155000 0.225000
793 -0.095000 0.225000
794 -0.030000 0.225000
795 0.030000 0.225000
796 0.095000 0.225000
797 0.320000 0.225000
798 -0.325000 0.230000
799 -0.260000 0.230000
800 -0.200000 0.230000
801 -0.135000 0.230000
802 -0.075000 0.230000
803 -0.010000 0.230000
804 0.055000 0.230000
805 0.180000 0.230000
806 -0.355000 0.235000
807 -0.295000 0.235000
808 -0.230000 0.235000
809 -0.165000 0.235000
810 -0.105000 0.235000
811 -0.040000 0.235000
812 0.020000 0.235000
813 0.085000 0.235000
814 0.310000 0.235000
815 -0.315000 0.240000
816 -0.250000 0.240000
817 -0.190000 0.240000
818 -0.125000 0.240000
819 -0.065000 0.240000
820 0.000000 0.240000
821 0.065000 0.240000
822 0.240000 0.240000
823 -0.330000 0.245000
824 -0.270000 0.245000
825 -0.205000 0.245000
826 -0.140000 0.245000
827 -0.080000 0.245000
828 -0.015000 0.245000
829 0.045000 0.245000
830 0.230000 0.245000
831 -0.340000 0.250000
832 -0.275000 0.250000
833 -0.210000 0.250000
834 -0.150000 0.250000
835 -0.085000 0.250000
836 -0.020000 0.250000
837 0.045000 0.250000
838 0.225000 0.250000
839 -0.340000 0.255000
840 -0.275000 0.255000
841 -0.215000 0.255000
842 -0.150000 0.255000
843 -0.085000 0.255000
844 -0.020000 0.255000
845 0.045000 0.255000
846 0.215000 0.255000
847 -0.335000 0.260000
848 -0.275000 0.260000
849 -0.210000 0.260000
850 -0.145000 0.260000
851 -0.085000 0.260000
852 -0.015000 0.260000
853 0.045000 0.260000
854 0.220000 0.260000
855 -0.315000 0.265000
856 -0.255000 0.265000
857 -0.190000 0.265000
858 -0.130000 0.265000
859 -0.055000 0.265000
860 0.010000 0.265000
861 0.070000 0.265000
862 0.280000 0.265000
863 -0.285000 0.270000
864 -0.220000 0.270000
865 -0.160000 0.270000
866 -0.095000 0.270000
867 -0.020000 0.270000
868 0.040000 0.270000
869 0.255000 0.270000
870 -0.300000 0.275000
871 -0.235000 0.275000
872 -0.170000 0.275000
873 -0.110000 0.275000
874 -0.030000 0.275000
875 0.030000 0.275000
876 0.245000 0.275000
877 -0.295000 0.280000
878 -0.230000 0.280000
879 -0.165000 0.280000
880 -0.105000 0.280000
881 -0.025000 0.280000
882 0.035000 0.280000
883 0.250000 0.280000
884 -0.270000 0.285000
885 -0.210000 0.285000
886 -0.145000 0.285000
887 -0.065000 0.285000
888 0.000000 0.285000
889 0.170000 0.285000
890 0.270000 0.285000
891 -0.235000 0.290000
892 -0.175000 0.290000
893 -0.085000 0.290000
894 -0.025000 0.290000
895 0.040000 0.290000
896 0.245000 0.290000
897 -0.250000 0.295000
898 -0.185000 0.295000
899 -0.090000 0.295000
900 -0.025000 0.295000
901 0.040000 0.295000
902 0.230000 0.295000
903 -0.235000 0.300000
904 -0.175000 0.300000
905 -0.065000 0.300000
906 -0.005000 0.300000
907 0.145000 0.300000
908 0.255000 0.300000
909 -0.190000 0.305000
910 -0.070000 0.305000
911 -0.010000 0.305000
912 0.135000 0.305000
913 0.245000 0.305000
914 -0.070000 0.310000
915 -0.005000 0.310000
916 0.130000 0.310000
917 -0.130000 0.315000
918 -0.065000 0.315000
919 -0.005000 0.315000
920 0.180000 0.315000
921 -0.140000 0.320000
922 -0.075000 0.320000
923 -0.015000 0.320000
924 0.105000 0.320000
925 -0.150000 0.325000
926 -0.090000 0.325000
927 -0.025000 0.325000
928 0.080000 0.325000
929 0.210000 0.325000
930 -0.110000 0.330000
931 -0.050000 0.330000
932 0.145000 0.330000
933 0.205000 0.330000
934 -0.125000 0.335000
935 -0.060000 0.335000
936 0.000000 0.335000
937 0.190000 0.335000
938 -0.155000 0.340000
939 -0.090000 0.340000
940 -0.030000 0.340000
941 0.150000 0.340000
942 -0.190000 0.345000
943 -0.130000 0.345000
944 -0.065000 0.345000
945 -0.005000 0.345000
946 0.160000 0.345000
947 -0.170000 0.350000
948 -0.110000 0.350000
949 -0.045000 0.350000
950 0.110000 0.350000
951 -0.200000 0.355000
952 -0.140000 0.355000
953 -0.075000 0.355000
954 0.070000 0.355000
955 0.130000 0.355000
956 -0.155000 0.360000
957 -0.095000 0.360000
958 -0.030000 0.360000
959 0.085000 0.360000
960 0.145000 0.360000
961 -0.115000 0.365000
962 -0.055000 0.365000
963 0.010000 0.365000
964 0.075000 0.365000
965 -0.160000 0.370000
966 -0.095000 0.370000
967 -0.035000 0.370000
968 0.030000 0.370000
969 0.090000 0.370000
970 -0.110000 0.375000
971 -0.045000 0.375000
972 0.015000 0.375000
973 0.080000 0.375000
974 -0.095000 0.380000
975 -0.030000 0.380000
976 0.035000 0.380000
977 -0.095000 0.385000
978 -0.030000 0.385000
979 0.030000 0.385000
980 -0.040000 0.390000
+55
View File
@@ -0,0 +1,55 @@
// Generate tape.tsv (maketape) or run it (run). Usage:
// node run-tape.ts maketape > tape.tsv
// node run-tape.ts run < tape.tsv > digests.txt
import { readFileSync } from 'node:fs';
import { PetSim, runTape, REST_STATE } from './PetSim.ts';
import type { PetState } from './PetSim.ts';
const DIR = new URL('.', import.meta.url).pathname;
const points = readFileSync(DIR + 'whale-points.tsv', 'utf8')
.trim().split('\n').map(l => l.split('\t').map(Number) as [number, number]);
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
// The tape: the same beats as the demo session, as keyframes eased at 30fps.
const BEATS: Array<[number, Partial<PetState>]> = [
[6, { channel: 'reasoning', activity: .12, coherence: .95, attention: 0, observed: 1 }],
[5, { channel: 'tool', activity: .62, coherence: .90 }],
[7, { channel: 'code', activity: .70, coherence: .90 }],
[5, { channel: 'filesystem', activity: .64, coherence: .90 }],
[5, { channel: 'network', activity: .60, coherence: .88 }],
[3, { channel: 'error', activity: .80, coherence: .50 }],
[5, { channel: 'code', activity: .72, coherence: .58 }],
[5, { channel: 'agent', activity: .58, coherence: .86 }],
[5, { channel: 'agent', activity: .50, coherence: .86, observed: .15 }],
[7, { channel: 'human', activity: .50, coherence: .90, attention: .95, observed: 1 }],
[8, { channel: 'reasoning', activity: .12, coherence: .95, attention: 0, roamX: .4, roamY: -.2, flip: -1, lit: .55 }],
[6, { channel: 'reasoning', activity: .12, coherence: .95, lit: 1, roamX: 0, roamY: 0, flip: 1 }],
];
function maketape() {
const lines = ['dt\tactivity\tcoherence\tattention\tchannel\tobserved\troamX\troamY\tflip\tlit'];
const dt = 1 / 30;
let cur: PetState = { ...REST_STATE, activity: .12, coherence: .95 };
for (const [dur, patch] of BEATS) {
const from = { ...cur };
cur = { ...cur, ...patch };
const frames = Math.round(dur * 30);
for (let i = 0; i < frames; i++) {
const e = Math.min(1, i / 42), m = e * e * (3 - 2 * e); // 1.4s morph
const s: PetState = { ...cur };
for (const k of ['activity','coherence','attention','observed','roamX','roamY','flip','lit'] as const)
s[k] = lerp((from as any)[k], (cur as any)[k], m);
lines.push(`${dt}\t${s.activity}\t${s.coherence}\t${s.attention}\t${s.channel}\t${s.observed}\t${s.roamX}\t${s.roamY}\t${s.flip}\t${s.lit}`);
}
}
console.log(lines.join('\n'));
}
if (process.argv[2] === 'maketape') {
maketape();
} else {
const rows = readFileSync(0, 'utf8').trim().split('\n');
const sim = new PetSim(points);
console.log(runTape(sim, rows, { motion: !process.argv.includes('--reduced-motion'), sensitivity: 1 }).join('\n'));
}
+173
View File
@@ -0,0 +1,173 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://whalesong.local/schema/event-v1.json",
"title": "Whalesong normalized event v1",
"description": "Canonical normalized events. Import accepts missing optional normalized defaults; canonical output fills them. Category extension uses other + subtype. Additional source fields survive in raw subject to privacy. endTime >= startTime is additionally checked by the importer.",
"type": "object",
"required": [
"schemaVersion",
"id",
"traceId",
"name",
"startTime",
"endTime",
"agentId",
"category",
"status",
"attributes"
],
"properties": {
"schemaVersion": {
"const": 1
},
"id": {
"type": "string",
"minLength": 1
},
"traceId": {
"type": "string",
"minLength": 1
},
"parentId": {
"type": "string"
},
"name": {
"type": "string",
"minLength": 1
},
"startTime": {
"type": "number",
"minimum": 0,
"description": "Milliseconds relative to the trace origin."
},
"endTime": {
"type": "number",
"minimum": 0,
"description": "Milliseconds; must be >= startTime. Equal endpoints encode a point event."
},
"openEnded": {
"type": "boolean",
"description": "The source duration is unknown. Display as onset-only."
},
"agentId": {
"type": "string"
},
"category": {
"enum": [
"reasoning",
"tool",
"memory",
"code",
"filesystem",
"network",
"browser",
"communication",
"agent",
"orchestration",
"error",
"human",
"other"
]
},
"status": {
"enum": [
"pending",
"running",
"success",
"error",
"unknown"
]
},
"attributes": {
"type": "object",
"additionalProperties": true
},
"payload": {},
"raw": {},
"links": {
"type": "array",
"items": {
"type": "object",
"required": [
"traceId",
"spanId"
],
"properties": {
"traceId": {
"type": "string"
},
"spanId": {
"type": "string"
},
"attributes": {
"type": "object"
}
},
"additionalProperties": false
}
},
"agentType": {
"type": "string"
},
"parentAgentId": {
"type": "string"
},
"subtype": {
"type": "string"
},
"model": {
"type": "string"
},
"provider": {
"type": "string"
},
"tool": {
"type": "string"
},
"costCurrency": {
"type": "string"
},
"sourceId": {
"type": "string"
},
"targetId": {
"type": "string"
},
"targetType": {
"type": "string"
},
"inputTokens": {
"type": "number",
"minimum": 0
},
"outputTokens": {
"type": "number",
"minimum": 0
},
"cachedTokens": {
"type": "number",
"minimum": 0
},
"cost": {
"type": "number",
"minimum": 0
},
"latency": {
"type": "number",
"minimum": 0
},
"contextTokens": {
"type": "number",
"minimum": 0
},
"contextLimit": {
"type": "number",
"minimum": 0
},
"retry": {
"type": "number",
"minimum": 0
}
},
"additionalProperties": false
}
+642
View File
@@ -0,0 +1,642 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:whalesong:evidence:v1",
"title": "Whalesong boundary evidence v1",
"description": "Metadata contract; validation is not source authentication. Runtime also enforces unique source/grant IDs, producer sequences and expiry ordering.",
"type": "object",
"properties": {
"format": {
"const": "whalesong.evidence/v1"
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"records": {
"type": "array",
"maxItems": 100000,
"items": {
"$ref": "#/$defs/observation"
}
},
"policy": {
"$ref": "#/$defs/policy"
},
"asOf": {
"type": "number",
"minimum": 0,
"maximum": 9007199254740991
}
},
"required": [
"format",
"name",
"records",
"policy",
"asOf"
],
"additionalProperties": false,
"$defs": {
"observation": {
"type": "object",
"properties": {
"version": {
"const": 1
},
"id": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"runId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"operationId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"sourceId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"epoch": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"sequence": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"time": {
"type": "object",
"properties": {
"wallMs": {
"type": "number",
"minimum": 0,
"maximum": 9007199254740991
},
"clockId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"monotonicMs": {
"type": "number",
"minimum": 0,
"maximum": 9007199254740991
},
"taskMs": {
"type": "number",
"minimum": 0,
"maximum": 9007199254740991
},
"uncertaintyMs": {
"type": "number",
"minimum": 0,
"maximum": 9007199254740991
}
},
"required": [
"wallMs"
],
"additionalProperties": false
},
"receivedAt": {
"type": "number",
"minimum": 0,
"maximum": 9007199254740991
},
"subject": {
"type": "object",
"properties": {
"agentId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"sandboxId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"isolationGroup": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
}
},
"required": [
"agentId"
],
"additionalProperties": false
},
"stage": {
"enum": [
"intent",
"dispatch",
"runtime",
"effect",
"control"
]
},
"surface": {
"enum": [
"model-api",
"http",
"mcp",
"git",
"filesystem",
"process",
"sandbox",
"dns",
"socket",
"message",
"database",
"artifact",
"browser",
"clipboard",
"other"
]
},
"action": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"effect": {
"enum": [
"read",
"write",
"delete",
"execute",
"publish",
"send",
"receive",
"connect",
"grant",
"revoke",
"none",
"unknown"
]
},
"status": {
"enum": [
"started",
"completed",
"error",
"unknown"
]
},
"target": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"kind": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"version": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"boundary": {
"enum": [
"local",
"external",
"unknown"
]
}
},
"required": [
"id",
"kind"
],
"additionalProperties": false
},
"actionDigest": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"authority": {
"type": "object",
"properties": {
"grantId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"claimed": {
"type": "boolean"
}
},
"required": [],
"additionalProperties": false
},
"correlation": {
"type": "object",
"properties": {
"sessionId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"requestId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"parentOperationId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"messageId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
}
},
"required": [],
"additionalProperties": false
},
"facts": {
"type": "object",
"maxProperties": 48,
"propertyNames": {
"allOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 80,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
{
"not": {
"enum": [
"__proto__",
"prototype",
"constructor"
]
}
}
]
},
"additionalProperties": {
"anyOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 512,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
{
"type": "number",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": [
"boolean",
"null"
]
}
]
}
}
},
"required": [
"version",
"id",
"runId",
"sourceId",
"epoch",
"sequence",
"time",
"subject",
"stage",
"surface",
"action",
"effect",
"status"
],
"additionalProperties": false
},
"source": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"stages": {
"type": "array",
"items": {
"enum": [
"intent",
"dispatch",
"runtime",
"effect",
"control"
]
},
"minItems": 1,
"maxItems": 256,
"uniqueItems": true
},
"surfaces": {
"type": "array",
"items": {
"enum": [
"model-api",
"http",
"mcp",
"git",
"filesystem",
"process",
"sandbox",
"dns",
"socket",
"message",
"database",
"artifact",
"browser",
"clipboard",
"other"
]
},
"minItems": 1,
"maxItems": 256,
"uniqueItems": true
},
"runIds": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"minItems": 1,
"maxItems": 256,
"uniqueItems": true
},
"sandboxIds": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"minItems": 1,
"maxItems": 256,
"uniqueItems": true
},
"isolationGroups": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"minItems": 1,
"maxItems": 256,
"uniqueItems": true
},
"heartbeatMs": {
"type": "number",
"minimum": 1,
"maximum": 9007199254740991
},
"description": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
}
},
"required": [
"id",
"stages",
"surfaces",
"runIds",
"heartbeatMs"
],
"additionalProperties": false
},
"grant": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"runIds": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"minItems": 1,
"maxItems": 256,
"uniqueItems": true
},
"sandboxIds": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"minItems": 1,
"maxItems": 256,
"uniqueItems": true
},
"targetIds": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"minItems": 1,
"maxItems": 256,
"uniqueItems": true
},
"actions": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"minItems": 1,
"maxItems": 256,
"uniqueItems": true
},
"effects": {
"type": "array",
"items": {
"enum": [
"read",
"write",
"delete",
"execute",
"publish",
"send",
"receive",
"connect",
"grant",
"revoke",
"none",
"unknown"
]
},
"minItems": 1,
"maxItems": 256,
"uniqueItems": true
},
"notBefore": {
"type": "number",
"minimum": 0,
"maximum": 9007199254740991
},
"expiresAt": {
"type": "number",
"minimum": 0,
"maximum": 9007199254740991
},
"actionDigest": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
}
},
"required": [
"id",
"runIds",
"sandboxIds",
"targetIds",
"actions",
"effects",
"notBefore",
"expiresAt"
],
"additionalProperties": false
},
"policy": {
"type": "object",
"properties": {
"version": {
"const": 1
},
"id": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001f\\u007f]+$"
},
"sources": {
"type": "array",
"items": {
"$ref": "#/$defs/source"
},
"minItems": 0,
"maxItems": 256,
"uniqueItems": true
},
"grants": {
"type": "array",
"items": {
"$ref": "#/$defs/grant"
},
"minItems": 0,
"maxItems": 2048,
"uniqueItems": true
},
"expectedSurfaces": {
"type": "array",
"items": {
"enum": [
"model-api",
"http",
"mcp",
"git",
"filesystem",
"process",
"sandbox",
"dns",
"socket",
"message",
"database",
"artifact",
"browser",
"clipboard",
"other"
]
},
"minItems": 0,
"maxItems": 256,
"uniqueItems": true
},
"forbiddenCrossGroup": {
"type": "boolean"
}
},
"required": [
"version",
"id",
"sources",
"grants",
"expectedSurfaces",
"forbiddenCrossGroup"
],
"additionalProperties": false
}
}
}
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env node
/** Local TypeScript build plus deterministic embedded-runtime packaging. */
import { readFile, writeFile, cp, mkdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import ts from 'typescript';
const root = fileURLToPath(new URL('../', import.meta.url));
const result = spawnSync(process.execPath, [root + 'node_modules/typescript/bin/tsc', '-p', root + 'tsconfig.json'], { stdio: 'inherit' });
if (result.status !== 0) process.exit(result.status || 1);
for (const name of ['pet.html', 'pet.css', 'whale-points.tsv']) await cp(root + 'public/' + name, root + 'dist/' + name);
const modules = new Map();
async function moduleFor(name) {
if (modules.has(name)) return;
if (!/^[a-z-]+$/.test(name)) throw new Error('Unexpected native module dependency.');
const source = await readFile(root + `src/core/${name}.ts`, 'utf8');
const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText;
modules.set(name, js);
for (const m of js.matchAll(/require\("\.\/([a-z-]+)\.js"\)/g)) await moduleFor(m[1]);
}
await moduleFor('pet-native');
// JavaScriptCore has no structuredClone host API. Pet inputs are validated JSON.
const bundle = `/* Generated from pet/src/core. Run npm --prefix pet run sync. */\n(function(global){\n'use strict';\nif(!global.structuredClone)global.structuredClone=value=>JSON.parse(JSON.stringify(value));\nconst factories={},cache={};\n`
+ [...modules].map(([id, js]) => `factories[${JSON.stringify(id)}]=function(exports,require){\n${js}\n};\n`).join('')
+ `function load(id){id=id.replace(/^\\.\\//,'').replace(/\\.js$/,'');if(cache[id])return cache[id];if(!factories[id])throw Error('Missing core module');const e=cache[id]={};factories[id](e,load);return e;}\nglobal.PetNative=load('pet-native').PetNative;\n})(globalThis);\n`;
await writeFile(root + 'dist/pet-native.js', bundle);
if (process.argv.includes('--tui')) {
const destination = root + '../crates/tui/src/tui/pet_watch/';
await mkdir(destination, { recursive: true });
await writeFile(destination + 'pet-native.js', bundle);
}
if (process.argv.includes('--study')) {
const study = root + 'world/';
await mkdir(study, { recursive: true });
for (const name of ['pet.html', 'pet.css', 'whale-points.tsv', 'core', 'ui/pet.js', 'ui/storage.js']) {
await mkdir(study + name.split('/').slice(0, -1).join('/'), { recursive: true });
await cp(root + 'dist/' + name, study + name, { recursive: true });
}
// The original conformance viewer must not retain a stale browser core.
await mkdir(study + '../dist', { recursive: true });
await writeFile(study + '../dist/PetSim.js', "export * from '../world/core/pet-sim.js';\n");
const renderer = await readFile(study + '../web.ts', 'utf8');
await writeFile(study + '../dist/web.js', ts.transpileModule(renderer, {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ES2022 },
}).outputText.replace("'./PetSim.ts'", "'./PetSim.js'"));
}
if (process.argv.includes('--apple')) {
const { petDemoEvents } = await import('../dist/core/pet-demo.js');
const { compilePetTelemetry, encodePetJSONL } = await import('../dist/core/pet-telemetry.js');
await mkdir(root + 'ios/Resources', { recursive: true });
await writeFile(root + 'ios/Resources/pet-native.js', bundle);
await writeFile(root + 'ios/Resources/demo.jsonl', encodePetJSONL(compilePetTelemetry(petDemoEvents(), 80_000)));
}
console.log(`Built pet viewer and native core (${modules.size} shared modules), entirely local.`);
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
./macos/build.sh
mkdir -p conformance-results
node scripts/long-checkpoint.mjs
swiftc -O -parse-as-library swift/PetSim.swift swift/PetNativeCore.swift swift/PetHabitatStore.swift swift/PetAudioOutput.swift swift/PetHost.swift tests/apple-checkpoint.swift -o conformance-results/apple-checkpoint
./conformance-results/apple-checkpoint "$(pwd)" "$(pwd)/conformance-results"
+89
View File
@@ -0,0 +1,89 @@
import { setTimeout as delay } from 'node:timers/promises';
import { importTrace } from '../../dist/core/ingest.js';
import { isCodewhaleRuntimeRecord, observeRuntimeRequests } from '../../dist/core/codewhale.js';
/** A read-only transport for the existing Runtime journal. All event meaning
* remains in Whalesong's importer and canonical pet bucketer. No raw journal,
* prompt, tool argument or bearer token is written into the pet recording. */
export async function followRuntime({ baseUrl, threadId, token, report = () => {} }) {
const url = new URL(baseUrl);
if (url.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(url.hostname)
|| url.username || url.password || url.pathname !== '/' || url.search || url.hash)
throw new Error('Pet Runtime input requires a plain HTTP loopback IP origin, without credentials or a path.');
if (typeof threadId !== 'string' || !threadId.trim() || threadId.length > 512)
throw new Error('Choose one Runtime --thread ID.');
let sdk;
try { sdk = await import('@codewhale/runtime-sdk'); }
catch { sdk = await import('../../../npm/runtime-sdk/index.js'); }
if (typeof sdk.CodeWhaleRuntimeClient.prototype.threadEvents !== 'function')
throw new Error('The local Runtime SDK needs threadEvents support.');
const client = new sdk.CodeWhaleRuntimeClient({ baseUrl: url.href, token });
const shutdown = new AbortController();
const records = [];
let cursor = 0, bytes = 0, revision = 0, connected = false, fatal = false;
let cachedRevision = -1, cachedTrace;
const done = (async () => {
let backoff = 250;
while (!shutdown.signal.aborted && !fatal) {
// Fifteen-second server heartbeats make a silent, half-open connection
// distinguishable from an idle journal. The timeout is driver time only.
const attempt = new AbortController();
const signal = AbortSignal.any([shutdown.signal, attempt.signal]);
let idleTimer;
const refresh = () => { clearTimeout(idleTimer); idleTimer = setTimeout(() => attempt.abort(), 45_000); };
refresh();
const fetchImpl = client.fetchImpl;
client.fetchImpl = async (input, init) => {
const response = await fetchImpl(input, init);
if (!response.body || !response.ok) return response;
const body = response.body.pipeThrough(new TransformStream({ transform(chunk, controller) { refresh(); controller.enqueue(chunk); } }));
return new Response(body, { status: response.status, headers: response.headers });
};
try {
for await (const record of client.threadEvents(threadId, { sinceSeq: cursor, signal })) {
if (!isCodewhaleRuntimeRecord(record) || record.thread_id !== threadId || !Number.isSafeInteger(record.seq) || record.seq < 0)
throw new Error('Invalid Runtime envelope.');
if (record.seq <= cursor) continue;
// Sequence numbers belong to Runtime, and need not be consecutive.
// Its predecessor cursor detects loss without inventing a new counter.
if (record.previous_seq !== undefined && record.previous_seq !== cursor)
throw new Error('Runtime predecessor cursor does not match.');
if (record.event !== 'item.delta') {
const size = Buffer.byteLength(JSON.stringify(record));
if (records.length >= 250_000 || bytes + size > 64 * 1024 * 1024) {
fatal = true; throw new Error('Runtime recording reached its input limit.');
}
records.push(record); bytes += size; revision++;
}
cursor = record.seq; connected = true; backoff = 250;
}
} catch (error) {
if ([400, 401, 403, 404, 405, 501].includes(error.status)) fatal = true;
if (!shutdown.signal.aborted) report(fatal
? 'Runtime input stopped: check the thread, authentication, SDK support, or recording size. Recording remains unobserved.'
: 'Runtime input interrupted; recording unobserved gaps while reconnecting from the last cursor.');
} finally {
connected = false; clearTimeout(idleTimer); client.fetchImpl = fetchImpl;
}
if (!shutdown.signal.aborted && !fatal) {
await delay(backoff, undefined, { signal: shutdown.signal }).catch(() => {});
backoff = Math.min(8000, backoff * 2);
}
}
})();
return {
get connected() { return connected; },
get revision() { return revision; },
get cursor() { return cursor; },
snapshot(observedThrough = Date.now()) {
if (!connected || !records.length) return undefined;
if (cachedRevision !== revision) {
// Reuse the same validated, metadata-only import path as file replay.
cachedTrace = importTrace(JSON.stringify(records), 'Codewhale Runtime', { privacy: 'metadata' })[0];
cachedRevision = revision;
}
return observeRuntimeRequests(cachedTrace, observedThrough);
},
async close() { shutdown.abort(); await done; },
};
}
+11
View File
@@ -0,0 +1,11 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { PetNative } from '../dist/core/pet-native.js';
import { compilePetTelemetry, encodePetJSONL } from '../dist/core/pet-telemetry.js';
const points = readFileSync(new URL('../public/whale-points.tsv', import.meta.url), 'utf8').trim().split('\n').map(l=>l.split(/\s+/).map(Number));
const started = performance.now();
const tape = compilePetTelemetry([], 7_200_000);
const pet = new PetNative(JSON.stringify(points), encodePetJSONL(tape));
for(let i=0;i<720;i++) pet.step(10,false);
const text=pet.recording(true);
writeFileSync(process.argv[2] ?? new URL('../conformance-results/long-habitat.json', import.meta.url),text);
console.log(JSON.stringify({fixture:'Two hours of synthetic, entirely unknown telemetry',buckets:tape.length,bytes:Buffer.byteLength(text),ticks:JSON.parse(text).checkpoint.tick,generationMs:performance.now()-started}));
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env node
/** Read-only adapter: existing Whalesong ingestion is the only event parser. */
import { readFile, stat, open } from 'node:fs/promises';
import { watch } from 'node:fs';
import { basename, dirname } from 'node:path';
import { importTrace } from '../dist/core/ingest.js';
import { compilePetTelemetry, encodePetJSONL, encodePetTSV } from '../dist/core/pet-telemetry.js';
import { petDemoEvents } from '../dist/core/pet-demo.js';
import { followRuntime } from './lib/pet-runtime.mjs';
const args = process.argv.slice(2);
const option = name => args.find(a => a.startsWith(`--${name}=`))?.slice(name.length + 3);
if (args.includes('--help')) {
console.log('node scripts/pet.mjs --input=trace.jsonl --output=pet.jsonl [--trace=ID] [--format=jsonl|tsv] [--watch]\nnode scripts/pet.mjs --runtime=http://127.0.0.1:7878 --thread=ID --output=pet.jsonl\nUse --demo instead of --input for synthetic telemetry. Output must not already exist.\nRuntime reads only the existing local event journal. Optional authentication comes from CODEWHALE_RUNTIME_TOKEN; never put a token in the URL. No agent or provider is started.');
process.exit(0);
}
let output, monitor, timer, runtime;
try {
for (const a of args) if (!['--demo', '--watch'].includes(a) && !/^--(input|output|trace|format|runtime|thread)=.+/.test(a)) throw new Error('Unknown or empty option. Use --help.');
const input = option('input'), runtimeURL = option('runtime'), path = option('output'), format = option('format') ?? 'jsonl', live = args.includes('--watch') || !!runtimeURL;
if (!path || [!!input, args.includes('--demo'), !!runtimeURL].filter(Boolean).length !== 1
|| !['jsonl', 'tsv'].includes(format) || live && format !== 'jsonl' || args.includes('--watch') && !input
|| !!runtimeURL !== !!option('thread') || option('trace') && !input)
throw new Error('Choose one input source, an unused --output path, and JSONL for live recording. Runtime requires --thread.');
const load = async () => {
if (!input) return { events: petDemoEvents(), duration: 80_000 };
if ((await stat(input)).size > 64 * 1024 * 1024) throw new Error('Input exceeds 64 MiB.');
const traces = importTrace(await readFile(input, 'utf8'), input, { privacy: 'metadata' });
const trace = option('trace') ? traces.find(t => t.id === option('trace')) : traces.length === 1 ? traces[0] : undefined;
if (!trace) throw new Error('Select an existing --trace ID when input contains multiple traces.');
return trace;
};
let trace = runtimeURL ? undefined : await load(), buckets = compilePetTelemetry(trace?.events ?? [], trace?.duration ?? 0);
output = await open(path, 'wx', 0o600);
if (runtimeURL) runtime = await followRuntime({ baseUrl: runtimeURL, threadId: option('thread'),
token: process.env.CODEWHALE_RUNTIME_TOKEN, report: text => console.error(text) });
if (!live) {
await output.writeFile(format === 'tsv' ? encodePetTSV(buckets) : encodePetJSONL(buckets));
await output.close(); output = undefined;
console.log(`Wrote ${buckets.length} pet buckets (${args.includes('--demo') ? 'demo' : 'trace replay'}).`);
} else {
// The driver owns wall time. The core only sees recorded relative timestamps.
const started = performance.now(), startedWall = Date.now();
const origin = trace && 'originTime' in trace && trace.originTime ? Date.parse(trace.originTime) : NaN;
const offset = Number.isFinite(origin) ? Math.max(0, Date.now() - origin) : trace?.duration ?? 0;
let dirty = false, running = false, sequence = 0, failed = false, stopping = false, lastBin = -1;
const empty = compilePetTelemetry([])[0];
if (input) {
monitor = watch(dirname(input), (_event, filename) => { if (!filename || String(filename) === basename(input)) dirty = true; });
monitor.on('error', () => { failed = true; dirty = true; });
}
const tick = async () => {
if (running || stopping) return;
running = true;
try {
const elapsed = performance.now() - started, target = Math.floor(elapsed / 400);
if (sequence > target) return;
if (target >= 216_000) throw new Error('Start a new pet recording after 24 hours.');
if (runtime) {
failed = !runtime.connected;
if (!failed) {
try {
trace = runtime.snapshot(startedWall + elapsed);
} catch { failed = true; console.error('Runtime snapshot is invalid; recording an unobserved gap.'); }
}
}
if (dirty) {
dirty = false;
try { trace = await load(); buckets = compilePetTelemetry(trace.events, trace.duration); failed = false; }
catch { failed = true; console.error('Source unavailable or invalid; recording an unobserved gap.'); }
}
// A stalled host records skipped intervals as unknown instead of silently
// compressing time. Never repeat onsets when timer jitter hits a source bin twice.
while (sequence < target) {
await output.write(encodePetJSONL([{ ...empty, sequence, simTimeMs: sequence * 400 }])); sequence++;
}
let state = empty;
if (runtime) {
// Seal the preceding observation interval before recording its state.
// A fixed recorder origin survives imports discovering older starts.
// Accepting this state one bucket later matches the foreground host.
if (!failed && trace && sequence > 0) {
try { state = compilePetTelemetry(trace.events, trace.duration,
sequence - 1, startedWall - Date.parse(trace.originTime))[0] ?? empty; }
catch { console.error('Runtime snapshot is invalid; recording an unobserved gap.'); }
}
} else {
const bin = Math.floor((offset + elapsed) / 400);
state = failed ? empty : buckets[bin] ?? empty;
if (bin === lastBin) state = { ...state, onsets: Array(13).fill(0), errors: 0 };
lastBin = bin;
}
await output.write(encodePetJSONL([{ ...state, sequence, simTimeMs: sequence * 400 }]));
sequence++;
} finally { running = false; }
};
await tick();
timer = setInterval(() => { tick().catch(async error => { console.error(error.message); process.exitCode = 1; await stop(); }); }, 400);
const stop = async () => {
stopping = true; clearInterval(timer); monitor?.close(); await runtime?.close();
while (running) await new Promise(resolve => setTimeout(resolve, 5));
if (output) { await output.sync(); await output.close(); output = undefined; }
};
process.once('SIGINT', stop); process.once('SIGTERM', stop);
console.log('Recording local live pet states. Ctrl+C to stop.');
}
} catch (error) {
console.error(error.message); process.exitCode = 1;
clearInterval(timer); monitor?.close(); await runtime?.close(); if (output) await output.close();
}
+504
View File
@@ -0,0 +1,504 @@
/** Read-only Codewhale session/runtime adapter. Does not record, mutate, or own receipts. */
import { CATEGORIES, errorOnsetOf, type Category, type Status, type Trace, type WhaleEvent } from './model.js';
const PAYLOAD_LIMIT = 2000;
const COLLAPSED_SPAN_MS = 1000;
const ENVELOPE_MIN_MS = 60_000;
type Obj = Record<string, any>;
const obj = (v: unknown): Obj => v !== null && typeof v === 'object' && !Array.isArray(v) ? v as Obj : {};
const str = (v: unknown): string | undefined => typeof v === 'string' && v.length ? v : undefined;
const num = (v: unknown): number | undefined => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
export function isCodewhaleSession(value: unknown): boolean {
const root = obj(value);
const metadata = obj(root.metadata);
if (!str(metadata.id)) return false;
if (root.format === 'whalesong.evidence/v1' || Array.isArray(root.resourceSpans) || root.schemaVersion === 1) return false;
const journal = obj(root.journal);
return Array.isArray(root.messages) || Array.isArray(journal.entries);
}
export function isCodewhaleRuntimeRecord(value: unknown): boolean {
const rec = obj(value);
return Number.isSafeInteger(rec.seq) && rec.seq >= 0 && typeof rec.event === 'string' && !!rec.event
&& typeof rec.thread_id === 'string' && !!rec.thread_id && rec.timestamp != null;
}
export function isCodewhaleRuntimeDocument(value: unknown): boolean {
if (!Array.isArray(value) || !value.length) return false;
const n = Math.min(value.length, 8);
let hits = 0;
for (let i = 0; i < n; i++) if (isCodewhaleRuntimeRecord(value[i])) hits++;
return hits === n;
}
function clip(value: unknown): unknown {
if (value == null) return value;
const text = typeof value === 'string' ? value : JSON.stringify(value);
if (text.length <= PAYLOAD_LIMIT) return typeof value === 'string' ? value : JSON.parse(text);
return `${text.slice(0, PAYLOAD_LIMIT)}…[truncated ${text.length - PAYLOAD_LIMIT} source bytes]`;
}
function parseTime(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value !== 'string' || !value) return undefined;
const ms = Date.parse(value);
return Number.isFinite(ms) ? ms : undefined;
}
function statusOf(value: unknown, isError?: boolean): Status {
if (isError === true) return 'error';
if (isError === false) return 'success';
const s = String(value ?? '').toLowerCase();
if (s === 'completed' || s === 'success' || s === 'ok') return 'success';
if (s === 'failed' || s === 'error' || s === 'errored') return 'error';
if (s === 'canceled' || s === 'cancelled' || s === 'interrupted') return 'error';
if (s === 'in_progress' || s === 'running') return 'running';
if (s === 'pending') return 'pending';
return 'unknown';
}
function classify(name: string): Category {
const n = name.toLowerCase();
if (/exception|^error\b/.test(n)) return 'error';
if (/spawn|fork|subagent|^agent$/.test(n)) return 'agent';
if (/message\.send|handoff|agent\.message|assistant_message/.test(n)) return 'communication';
if (/retrieve|retrieval|context|embedding|vector|memory|rag/.test(n)) return 'memory';
if (/browser|navigate|screenshot|click|playwright/.test(n)) return 'browser';
if (/read_file|write_file|list_dir|^read$|^write$|^edit$|glob|grep|file\.|filesystem/.test(n)) return 'filesystem';
if (/bash|exec|shell|run_test|cargo|pytest|compile/.test(n)) return 'code';
if (/reason|thinking|completion|generate|chat|llm/.test(n)) return 'reasoning';
if (/http|request|api|fetch|network|mcp_/.test(n)) return 'network';
if (/user_message|human|approval/.test(n)) return 'human';
if (/orchestrat|workflow|phase|join|session|thread|turn|todo|plan|operate_contract|status/.test(n)) return 'orchestration';
if (/tool/.test(n)) return 'tool';
return CATEGORIES.includes(n as Category) ? n as Category : 'other';
}
export function toolCategory(name: string): Category {
const category = classify(name);
return category === 'other' ? 'tool' : category;
}
function pointer(source: string, ids: Obj): Obj {
return { format: source, ...ids };
}
function titleOfSession(metadata: Obj, filename: string): string {
const title = str(metadata.title)?.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
if (title && !title.startsWith('codewhale:runtime_event')) return title.slice(0, 120);
return `Codewhale session · ${(str(metadata.id) ?? filename).slice(0, 8)}`;
}
function activeJournalEntries(journal: Obj): { entries: Obj[]; warnings: string[] } {
const entries = Array.isArray(journal.entries) ? journal.entries.map(obj) : [];
const leaf = str(journal.leaf_id);
if (!leaf || !entries.length) return { entries, warnings: [] };
const byId = new Map(entries.filter(e => str(e.id)).map(e => [e.id as string, e]));
const chain: Obj[] = [];
const seen = new Set<string>();
let id: string | undefined = leaf;
while (id && !seen.has(id)) {
seen.add(id);
const entry = byId.get(id);
if (!entry) break;
chain.push(entry);
id = str(entry.parent_id);
}
if (!chain.length) return { entries, warnings: ['Journal leaf_id did not resolve; using append order instead of the active branch.'] };
if (chain.length < entries.length) {
return {
entries: chain.reverse(),
warnings: [`Active journal branch has ${chain.length} of ${entries.length} entries. Forked history was not invented into the timeline.`],
};
}
return { entries: chain.reverse(), warnings: [] };
}
function collapsedTimestamps(entries: Obj[], created?: number, updated?: number): boolean {
const times = entries.map(e => parseTime(e.created_at)).filter((n): n is number => n !== undefined);
if (times.length < 2) return false;
const span = Math.max(...times) - Math.min(...times);
const envelope = created !== undefined && updated !== undefined ? updated - created : 0;
return envelope >= ENVELOPE_MIN_MS && span < COLLAPSED_SPAN_MS;
}
function pushEvent(events: WhaleEvent[], event: WhaleEvent): void {
events.push(event);
}
export function fromCodewhaleSession(document: unknown, filename = 'Codewhale session', maxEvents = 250_000): Trace {
const root = obj(document);
const metadata = obj(root.metadata);
const sessionId = str(metadata.id) ?? filename;
const journal = obj(root.journal);
const { entries, warnings } = activeJournalEntries(journal);
const sourceEntries: Obj[] = entries.length ? entries : (Array.isArray(root.messages) ? root.messages.map((message: unknown, i: number) => ({ id: `${sessionId}/message/${i}`, kind: 'message', message })) : []);
if (!sourceEntries.length) throw new Error('Codewhale session contains no journal entries or messages.');
const created = parseTime(metadata.created_at);
const updated = parseTime(metadata.updated_at);
const orderOnly = collapsedTimestamps(sourceEntries, created, updated);
if (orderOnly) {
warnings.push('Journal created_at values are collapsed to last-save time, not execution time. The time axis is journal order (1 ms per emitted event), not wall-clock duration. Gap, burst, and cycle-period findings are not execution-time claims.');
} else {
const times = sourceEntries.map(e => parseTime(e.created_at)).filter((n): n is number => n !== undefined);
if (!times.length) warnings.push('Journal entries have no usable timestamps. The time axis is journal order.');
}
const events: WhaleEvent[] = [];
const pending = new Map<string, number>();
let seq = 0;
const originWall = orderOnly ? undefined : sourceEntries.map(e => parseTime(e.created_at)).find((n): n is number => n !== undefined);
const agentId = 'parent';
const model = str(metadata.model);
const provider = str(metadata.model_provider);
const when = (entry: Obj, fallback: number): { start: number; open: boolean } => {
if (orderOnly || originWall === undefined) return { start: fallback, open: false };
const t = parseTime(entry.created_at);
if (t === undefined) return { start: fallback, open: true };
return { start: t - originWall, open: false };
};
for (const entry of sourceEntries) {
if (events.length >= maxEvents) throw new Error(`Import exceeds the ${maxEvents.toLocaleString()} event limit.`);
const entryId = str(entry.id) ?? `${sessionId}/entry/${seq}`;
const message = obj(entry.message ?? (entry.kind === 'message' ? entry : {}));
const role = str(message.role) ?? (str(entry.kind) === 'user' ? 'user' : str(entry.kind) === 'assistant' ? 'assistant' : undefined);
const blocks: Obj[] = Array.isArray(message.content) ? message.content.map(obj) : [];
if (!blocks.length) {
const text = str(entry.text) ?? str(message.text);
if (text) blocks.push({ type: role === 'user' ? 'text' : 'text', text });
}
if (!blocks.length) continue;
const parentEventId = events.length ? events[events.length - 1]!.id : undefined;
for (const block of blocks) {
const t = when(entry, seq);
const idBase = `${entryId}/${seq}`;
const type = str(block.type) ?? 'text';
const raw = pointer('codewhale.session/v1', { sessionId, entryId, seq, blockType: type, toolUseId: block.id ?? block.tool_use_id });
if (type === 'tool_use' || type === 'server_tool_use') {
const tool = str(block.name) ?? 'tool';
const callId = str(block.id) ?? idBase;
const started = tool === 'agent' && obj(block.input).action === 'start';
const event: WhaleEvent = {
schemaVersion: 1, id: callId, traceId: sessionId, parentId: parentEventId,
startTime: t.start, endTime: t.start, openEnded: true,
agentId, name: started ? 'agent.spawn' : tool, tool, category: toolCategory(tool),
subtype: started ? 'fork' : undefined, model, provider,
status: 'running', attributes: { 'codewhale.entry_id': entryId, 'codewhale.seq': seq, 'tool.name': tool },
payload: { arguments: clip(block.input) }, raw,
};
pending.set(callId, events.length);
pushEvent(events, event);
} else if (type === 'tool_result') {
const callId = str(block.tool_use_id);
const isError = block.is_error === true;
const target = callId !== undefined ? pending.get(callId) : undefined;
if (target !== undefined) {
const prior = events[target]!;
prior.endTime = t.start;
prior.openEnded = false;
prior.status = statusOf('completed', isError);
prior.payload = { ...(obj(prior.payload)), result: clip(block.content) };
prior.attributes = { ...prior.attributes, 'codewhale.result_entry_id': entryId };
pending.delete(callId as string);
} else {
pushEvent(events, {
schemaVersion: 1, id: idBase, traceId: sessionId, parentId: callId ?? parentEventId,
startTime: t.start, endTime: t.start, agentId,
name: 'tool_result', category: 'tool', model, provider,
status: statusOf(undefined, isError),
attributes: { 'codewhale.entry_id': entryId, 'codewhale.seq': seq, tool_use_id: callId },
payload: { result: clip(block.content) }, raw,
});
}
} else if (type === 'thinking') {
pushEvent(events, {
schemaVersion: 1, id: idBase, traceId: sessionId, parentId: parentEventId,
startTime: t.start, endTime: t.start, agentId, name: 'thinking', category: 'reasoning',
model, provider, status: 'success',
attributes: { 'codewhale.entry_id': entryId, 'codewhale.seq': seq },
payload: { thinking: clip(block.thinking ?? block.text) }, raw,
});
} else {
const text = str(block.text) ?? '';
const operate = text.includes('codewhale:runtime_event');
const user = role === 'user' || role === 'User';
pushEvent(events, {
schemaVersion: 1, id: idBase, traceId: sessionId, parentId: parentEventId,
startTime: t.start, endTime: t.start, agentId,
name: operate ? 'operate_contract' : user ? 'user_message' : 'assistant_message',
category: operate ? 'orchestration' : user ? 'human' : 'communication',
model, provider, status: 'success',
attributes: { 'codewhale.entry_id': entryId, 'codewhale.seq': seq, role: role ?? 'unknown' },
payload: { text: clip(text) }, raw,
});
}
seq += 1;
}
}
if (!events.length) throw new Error('Codewhale session produced no inspectable events.');
for (const event of events) {
if (event.openEnded && event.tool) warnings.push(`Tool ${event.id} has no matching tool_result in this snapshot; duration remains unknown.`);
}
const base = events.reduce((m, e) => Math.min(m, e.startTime), events[0]!.startTime);
for (const event of events) { event.startTime -= base; event.endTime -= base; }
const cost = obj(metadata.cost);
const sessionCost = num(cost.session_cost_usd);
const duration = Math.max(1, events.reduce((m, e) => Math.max(m, e.endTime, e.startTime), 0));
const uniqueWarnings = [...new Set(warnings)];
return {
id: sessionId,
name: titleOfSession(metadata, filename),
events,
duration,
originTime: orderOnly ? 'journal-order' : (str(metadata.created_at) ?? `${base} ms`),
source: 'codewhale',
privacy: 'redact',
warnings: uniqueWarnings,
metadata: {
sourceFormat: 'codewhale.session/v1',
timeBasis: orderOnly || originWall === undefined ? 'journal-order' : 'wall-clock',
sourceFilename: filename,
sessionId,
model,
provider,
workspace: metadata.workspace,
mode: metadata.mode,
envelopeCreatedAt: metadata.created_at,
envelopeUpdatedAt: metadata.updated_at,
cumulativeTurnSecs: metadata.cumulative_turn_secs,
messageCount: metadata.message_count,
journalEntries: sourceEntries.length,
totalTokens: metadata.total_tokens,
sessionCostUsd: sessionCost,
pricedTurns: cost.priced_turns,
unpricedTurns: cost.unpriced_turns,
runtimeStore: metadata.runtime_store,
timeUnit: 'ms',
},
};
}
function itemToolName(item: Obj, payload: Obj): string | undefined {
const named = str(payload.tool) ?? str(item.tool) ?? str(item.name);
if (named) return named;
if (str(item.kind) !== 'tool_call') return undefined;
const head = str(item.summary)?.split(':')[0]?.trim();
if (head && head.length < 80 && !/\s/.test(head)) return head;
return undefined;
}
function itemCategory(kind: string, tool?: string): Category {
if (kind === 'user_message') return 'human';
if (kind === 'agent_reasoning') return 'reasoning';
if (kind === 'agent_message') return 'communication';
if (kind === 'status') return 'orchestration';
if (kind === 'tool_call' && tool) return toolCategory(tool);
if (kind === 'tool_call') return 'tool';
return classify(kind);
}
export function fromCodewhaleRuntime(records: unknown[], filename = 'Codewhale runtime', maxEvents = 250_000): Trace {
if (!records.length) throw new Error('Codewhale runtime event file is empty.');
const first = obj(records[0]);
const threadId = str(first.thread_id) ?? filename;
const warnings: string[] = [];
let skippedDeltas = 0;
const open = new Map<string, { index: number; startWall: number }>();
const requests = new Map<string, WhaleEvent>();
const events: WhaleEvent[] = [];
let origin: number | undefined;
let model: string | undefined;
let threadName = threadId;
const stamp = (rec: Obj): number => {
const t = parseTime(rec.timestamp);
if (t === undefined) throw new Error(`Runtime event seq ${rec.seq} is missing a usable timestamp.`);
if (origin === undefined) origin = t;
return t - origin;
};
for (const raw of records) {
if (!isCodewhaleRuntimeRecord(raw)) throw new Error('Runtime import cancelled: a line is not a Codewhale runtime event record. No rows were skipped.');
const rec = obj(raw);
if (rec.thread_id !== threadId) throw new Error('Runtime import contains multiple threads. Export one thread before importing.');
const eventName = rec.event as string;
if (eventName === 'item.delta') { skippedDeltas++; continue; }
if (events.length >= maxEvents) throw new Error(`Import exceeds the ${maxEvents.toLocaleString()} event limit.`);
const payload = obj(rec.payload);
const item = obj(payload.item);
const turn = obj(payload.turn);
const thread = obj(payload.thread);
const relative = stamp(rec);
const turnId = str(rec.turn_id) ?? str(payload.turn_id);
const itemId = str(rec.item_id) ?? str(item.id);
const agentId = 'parent';
if (str(thread.model)) model = str(thread.model);
if (str(turn.model)) model = str(turn.model) ?? model;
if (eventName === 'thread.started') {
model = str(thread.model) ?? model;
threadName = str(thread.id) ?? threadId;
pushEvent(events, {
schemaVersion: 1, id: `thread:${threadId}`, traceId: threadId,
startTime: relative, endTime: relative, openEnded: true,
agentId, name: 'thread', category: 'orchestration', model, status: 'running',
attributes: { 'codewhale.seq': rec.seq, 'whalesong.container': true }, raw: rec,
});
continue;
}
if (eventName === 'turn.started' || eventName === 'turn.completed') {
const id = `turn:${turnId ?? rec.seq}`;
if (eventName === 'turn.completed') for (const [key, request] of requests) {
if (request.parentId !== id) continue;
request.endTime = Math.max(request.startTime, relative);
request.openEnded = false; request.status = 'unknown'; requests.delete(key);
}
const startWall = parseTime(turn.started_at) ?? parseTime(turn.created_at);
const endWall = parseTime(turn.ended_at);
const start = startWall !== undefined && origin !== undefined ? startWall - origin : relative;
const end = eventName === 'turn.completed' && endWall !== undefined && origin !== undefined ? endWall - origin : relative;
const usage = obj(turn.usage);
const existing = events.findIndex(e => e.id === id);
const next: WhaleEvent = {
schemaVersion: 1, id, traceId: threadId, parentId: `thread:${threadId}`,
startTime: start, endTime: Math.max(start, end), openEnded: eventName !== 'turn.completed',
agentId, name: 'turn', category: 'orchestration', model,
inputTokens: num(usage.input_tokens), outputTokens: num(usage.output_tokens),
status: statusOf(turn.status ?? payload.status), latency: num(turn.duration_ms),
attributes: { 'codewhale.seq': rec.seq, 'codewhale.turn_id': turnId, 'whalesong.container': true,
...(statusOf(turn.status ?? payload.status) === 'error' ? { 'whalesong.error_onset_ms': relative } : {}) },
payload: { input_summary: clip(turn.input_summary) }, raw: rec,
};
if (existing >= 0) events[existing] = { ...events[existing]!, ...next, startTime: events[existing]!.startTime };
else pushEvent(events, next);
continue;
}
if (eventName === 'turn.lifecycle') continue;
if (['approval.required', 'approval.decided', 'approval.timeout', 'user_input.required', 'user_input.answered', 'user_input.canceled'].includes(eventName)) {
const kind = eventName.startsWith('approval.') ? 'approval' : 'user_input';
const requestId = str(payload[kind === 'approval' ? 'approval_id' : 'input_id']) ?? str(payload.id);
if (!requestId) throw new Error(`Runtime ${eventName} is missing its request identity.`);
const key = JSON.stringify([turnId ?? '', kind, requestId]);
const prior = requests.get(key), required = eventName.endsWith('.required');
if (required && prior) continue;
if (!required && prior) {
prior.endTime = Math.max(prior.startTime, relative); prior.openEnded = false;
prior.status = eventName === 'approval.decided' || eventName === 'user_input.answered' ? 'success' : 'unknown';
if (payload.auto === true) {
// Automatic consent has a receipt, but never asked the human to wait.
prior.category = 'orchestration'; delete prior.attributes['whalesong.waiting'];
prior.attributes['whalesong.container'] = true;
}
requests.delete(key); continue;
}
const automatic = payload.auto === true;
const event: WhaleEvent = {
schemaVersion: 1, id: `request:${key}:${rec.seq}`, traceId: threadId,
parentId: turnId ? `turn:${turnId}` : undefined, startTime: relative, endTime: relative,
openEnded: required, agentId, name: eventName, category: automatic ? 'orchestration' : 'human',
status: required ? 'pending' : 'success', model,
attributes: { 'codewhale.seq': rec.seq, 'whalesong.waiting': required, 'whalesong.container': automatic }, raw: rec,
};
if (required) requests.set(key, event);
pushEvent(events, event); continue;
}
if (eventName === 'tool_call.requested' || eventName === 'tool_call.canceled') {
const callId = str(payload.call_id) ?? `call:${rec.seq}`;
const tool = str(payload.tool);
const canceled = eventName === 'tool_call.canceled';
pushEvent(events, {
schemaVersion: 1, id: `${eventName}:${callId}`, traceId: threadId, parentId: turnId ? `turn:${turnId}` : undefined,
startTime: relative, endTime: relative, agentId, name: tool ?? eventName, tool,
category: tool ? toolCategory(tool) : 'tool', model, status: canceled ? 'error' : 'pending',
attributes: { 'codewhale.seq': rec.seq, 'codewhale.turn_id': turnId, 'codewhale.call_id': callId, reason: payload.reason },
payload: { arguments: clip(payload.arguments) }, raw: rec,
});
continue;
}
if (eventName === 'item.started' || eventName === 'item.completed') {
const kind = str(item.kind) ?? 'item';
const tool = itemToolName(item, payload);
const id = itemId ?? `item:${rec.seq}`;
const startWall = parseTime(item.started_at);
const endWall = parseTime(item.ended_at);
const start = startWall !== undefined && origin !== undefined ? startWall - origin : relative;
const end = eventName === 'item.completed' && endWall !== undefined && origin !== undefined ? endWall - origin : relative;
const openEnded = eventName === 'item.started' && endWall === undefined;
const existing = open.get(id);
if (existing && eventName === 'item.completed') {
const prior = events[existing.index]!;
prior.endTime = Math.max(prior.startTime, end);
prior.openEnded = false;
prior.status = statusOf(item.status);
if (prior.status === 'error') prior.attributes['whalesong.error_onset_ms'] = relative;
prior.payload = { summary: clip(item.summary), detail: clip(item.detail) };
open.delete(id);
continue;
}
const event: WhaleEvent = {
schemaVersion: 1, id, traceId: threadId, parentId: turnId ? `turn:${turnId}` : undefined,
startTime: start, endTime: Math.max(start, end), openEnded,
agentId, name: tool ?? kind, tool, category: itemCategory(kind, tool), model,
status: statusOf(item.status ?? (eventName === 'item.started' ? 'running' : undefined)),
attributes: { 'codewhale.seq': rec.seq, 'codewhale.turn_id': turnId, 'codewhale.item_kind': kind,
...(statusOf(item.status) === 'error' ? { 'whalesong.error_onset_ms': relative } : {}) },
payload: { summary: clip(item.summary), detail: clip(item.detail) }, raw: rec,
};
if (eventName === 'item.started') open.set(id, { index: events.length, startWall: (origin ?? 0) + start });
pushEvent(events, event);
continue;
}
pushEvent(events, {
schemaVersion: 1, id: `${eventName}:${rec.seq}`, traceId: threadId, parentId: turnId ? `turn:${turnId}` : undefined,
startTime: relative, endTime: relative, agentId, name: eventName, category: classify(eventName),
model, status: 'unknown', attributes: { 'codewhale.seq': rec.seq }, raw: rec,
});
}
if (skippedDeltas) warnings.push(`Dropped ${skippedDeltas.toLocaleString()} item.delta records; they are token stream fragments, not spans. Item start/end remain the source of duration.`);
for (const [id] of open) warnings.push(`Item ${id} started and never completed in this file; duration remains unknown.`);
for (const request of requests.values()) warnings.push(`Request ${request.id} has no terminal receipt; its duration remains unknown in this file.`);
if (!events.length) throw new Error('Codewhale runtime file contained only stream deltas or unreadable records.');
const base = events.reduce((m, e) => Math.min(m, e.startTime), events[0]!.startTime);
for (const event of events) {
if (event.attributes['whalesong.error_onset_ms'] !== undefined) event.attributes['whalesong.error_onset_ms'] = errorOnsetOf(event) - base;
event.startTime -= base; event.endTime -= base;
}
return {
id: threadId,
name: `Codewhale runtime · ${threadName}`,
events,
duration: Math.max(1, events.reduce((m, e) => Math.max(m, e.endTime, e.startTime, e.status === 'error' ? errorOnsetOf(e) : 0), 0)),
originTime: origin !== undefined ? new Date(origin + base).toISOString() : '0 ms',
source: 'codewhale',
privacy: 'redact',
warnings: [...new Set(warnings)],
metadata: {
sourceFormat: 'codewhale.runtime-events/v2',
timeBasis: 'wall-clock',
sourceFilename: filename,
threadId,
model,
skippedDeltas,
recordCount: records.length,
timeUnit: 'ms',
},
};
}
/** The journal owns request state until a matching terminal receipt. A live
* driver may confirm that state only while its cursor-checked stream is healthy.
* Ordinary open tool spans remain unknown-duration; no execution is inferred. */
export function observeRuntimeRequests(trace: Trace, observedThrough: number): Trace {
const origin = Date.parse(trace.originTime ?? '');
if (trace.metadata.sourceFormat !== 'codewhale.runtime-events/v2' || !Number.isFinite(origin)
|| !Number.isFinite(observedThrough)) throw new Error('Invalid Runtime observation horizon.');
const at = observedThrough - origin;
const events = trace.events.map(e => e.openEnded && e.attributes['whalesong.waiting'] === true && at >= e.startTime
? { ...e, endTime: at, openEnded: false } : e);
return { ...trace, events, duration: Math.max(trace.duration, at) };
}
+255
View File
@@ -0,0 +1,255 @@
/** Boundary evidence v1. Pure, deterministic, metadata-only; no network or trust-by-text. */
import { type Category, type Trace, type WhaleEvent, type Finding, stableHash } from './model.js';
export const SURFACES = ['model-api','http','mcp','git','filesystem','process','sandbox','dns','socket','message','database','artifact','browser','clipboard','other'] as const;
export type Surface = typeof SURFACES[number];
export const STAGES = ['intent','dispatch','runtime','effect','control'] as const;
export type Stage = typeof STAGES[number];
export const EFFECTS = ['read','write','delete','execute','publish','send','receive','connect','grant','revoke','none','unknown'] as const;
export type Effect = typeof EFFECTS[number];
export interface Observation {
version: 1; id: string; runId: string; operationId?: string;
sourceId: string; epoch: string; sequence: number;
time: { wallMs: number; clockId?: string; monotonicMs?: number; taskMs?: number; uncertaintyMs?: number };
receivedAt?: number;
subject: { agentId: string; sandboxId?: string; isolationGroup?: string };
stage: Stage; surface: Surface; action: string; effect: Effect;
status: 'started' | 'completed' | 'error' | 'unknown';
target?: { id: string; kind: string; version?: string; boundary?: 'local'|'external'|'unknown' };
actionDigest?: string;
authority?: { grantId?: string; claimed?: boolean };
correlation?: { sessionId?: string; requestId?: string; parentOperationId?: string; messageId?: string };
facts: Record<string, string | number | boolean | null>;
}
export interface EvidenceSource {
id: string; stages: Stage[]; surfaces: Surface[];
runIds: string[]; sandboxIds?: string[]; isolationGroups?: string[];
heartbeatMs: number; description?: string;
}
export interface Grant {
id: string; runIds: string[]; sandboxIds: string[]; targetIds: string[];
actions: string[]; effects: Effect[]; notBefore: number; expiresAt: number;
actionDigest?: string;
}
export interface BoundaryPolicy {
version: 1; id: string; sources: EvidenceSource[]; grants: Grant[];
expectedSurfaces: Surface[]; forbiddenCrossGroup: boolean;
}
export interface EvidenceBundle {
format: 'whalesong.evidence/v1'; name: string; records: Observation[];
policy: BoundaryPolicy; asOf: number;
}
export interface CoverageRow {
sourceId: string; surface: Surface; status: 'observed'|'stale'|'missing'|'unconfigured'|'disabled'|'unverified-time';
count: number; lastReceivedAt: number | null; sequenceGaps: number;
}
export interface BoundaryFinding {
id: string; code: string; severity: 'info'|'warning'|'critical';
title: string; detail: string; recordIds: string[]; time: number;
}
export interface Interaction {
runId: string; sandboxId?: string; group?: string; target: string;
kind: string; stage: Stage; effect: Effect; count: number; recordIds: string[];
}
export interface BoundaryAnalysis {
version: 1; omittedFindings: number; asOf: number; inputRecords: number; visibleRecords: number; uniqueRecords: number;
futureRecords: number; findings: BoundaryFinding[]; coverage: CoverageRow[];
interactions: Interaction[]; runs: number; sandboxes: number; externalEffects: number;
unknownEffects: number; authority: { permitted: number; denied: number; unknown: number };
provenance: string;
}
const enumValue = <T extends string>(v:unknown, values: readonly T[], field:string): T => {
if(typeof v!=='string'||!values.includes(v as T))throw new Error(`Invalid ${field}.`);return v as T;
};
const object = (v:unknown, field:string): Record<string,unknown> => {
if(!v||typeof v!=='object'||Array.isArray(v))throw new Error(`Invalid ${field}: object required.`);return v as Record<string,unknown>;
};
const text = (v:unknown, field:string, max=256):string => {
if(typeof v!=='string'||!v.length||v.length>max||/[\u0000-\u001f\u007f]/.test(v))throw new Error(`Invalid ${field}: nonempty bounded text required.`);return v;
};
const num = (v:unknown,field:string,min=0):number => {
if(typeof v!=='number'||!Number.isFinite(v)||v<min||Math.abs(v)>Number.MAX_SAFE_INTEGER)throw new Error(`Invalid ${field}.`);return v;
};
const optionalText = (o:Record<string,unknown>,key:string):string|undefined => o[key]===undefined?undefined:text(o[key],key);
const strings = (v:unknown,field:string,allowEmpty=false):string[]=>{
if(!Array.isArray(v)||(!allowEmpty&&!v.length)||v.length>256)throw new Error(`Invalid ${field}.`);
const out=v.map(x=>text(x,field));if(new Set(out).size!==out.length)throw new Error(`Duplicate ${field}.`);return out;
};
const boolean = (v:unknown,field:string):boolean=>{if(typeof v!=='boolean')throw new Error(`Invalid ${field}.`);return v;};
const optionalNumber=(o:Record<string,unknown>,key:string)=>o[key]===undefined?undefined:num(o[key],key);
function onlyKeys(o:Record<string,unknown>,keys:string[],label:string):void{
for(const k of Object.keys(o))if(!keys.includes(k))throw new Error(`Unknown ${label} field: ${k}.`);
}
/** All unrecognized fields are rejected, never secretly retained in metadata-only evidence. */
export function validateObservation(input:unknown):Observation {
const o=object(input,'observation');
onlyKeys(o,['version','id','runId','operationId','sourceId','epoch','sequence','time','receivedAt','subject','stage','surface','action','effect','status','target','actionDigest','authority','correlation','facts'],'observation');
if(o.version!==1)throw new Error('Unsupported observation version.');
const t=object(o.time,'time'),s=object(o.subject,'subject');
onlyKeys(t,['wallMs','clockId','monotonicMs','taskMs','uncertaintyMs'],'time');onlyKeys(s,['agentId','sandboxId','isolationGroup'],'subject');
const seq=num(o.sequence,'sequence',1);if(!Number.isSafeInteger(seq))throw new Error('sequence must be an integer.');
const facts=object(o.facts??{},'facts'),safeFacts:Observation['facts']={};
if(Object.keys(facts).length>48)throw new Error('Too many observation facts.');
for(const [k,v] of Object.entries(facts)){
text(k,'fact key',80);if(['__proto__','prototype','constructor'].includes(k))throw new Error('Unsafe fact key.');
if(typeof v==='string')safeFacts[k]=text(v,'fact value',512);
else if(typeof v==='number')safeFacts[k]=num(v,'fact value',-Number.MAX_SAFE_INTEGER);
else if(v===null||typeof v==='boolean')safeFacts[k]=v;
else throw new Error('Facts must be scalar metadata, not content objects.');
}
let target:Observation['target'];if(o.target!==undefined){const a=object(o.target,'target');onlyKeys(a,['id','kind','version','boundary'],'target');target={id:text(a.id,'target.id'),kind:text(a.kind,'target.kind',64),version:optionalText(a,'version'),boundary:a.boundary===undefined?undefined:enumValue(a.boundary,['local','external','unknown'] as const,'boundary')};}
let authority:Observation['authority'];if(o.authority!==undefined){const a=object(o.authority,'authority');onlyKeys(a,['grantId','claimed'],'authority');authority={grantId:optionalText(a,'grantId'),claimed:a.claimed===undefined?undefined:boolean(a.claimed,'claimed')};}
let correlation:Observation['correlation'];if(o.correlation!==undefined){const a=object(o.correlation,'correlation');onlyKeys(a,['sessionId','requestId','parentOperationId','messageId'],'correlation');correlation={sessionId:optionalText(a,'sessionId'),requestId:optionalText(a,'requestId'),parentOperationId:optionalText(a,'parentOperationId'),messageId:optionalText(a,'messageId')};}
return {version:1,id:text(o.id,'id'),runId:text(o.runId,'runId'),operationId:optionalText(o,'operationId'),sourceId:text(o.sourceId,'sourceId'),epoch:text(o.epoch,'epoch'),sequence:seq,
time:{wallMs:num(t.wallMs,'wallMs'),clockId:optionalText(t,'clockId'),monotonicMs:optionalNumber(t,'monotonicMs'),taskMs:optionalNumber(t,'taskMs'),uncertaintyMs:optionalNumber(t,'uncertaintyMs')},receivedAt:optionalNumber(o,'receivedAt'),
subject:{agentId:text(s.agentId,'agentId'),sandboxId:optionalText(s,'sandboxId'),isolationGroup:optionalText(s,'isolationGroup')},stage:enumValue(o.stage,STAGES,'stage'),surface:enumValue(o.surface,SURFACES,'surface'),action:text(o.action,'action',128),effect:enumValue(o.effect,EFFECTS,'effect'),status:enumValue(o.status,['started','completed','error','unknown'],'status'),target,actionDigest:optionalText(o,'actionDigest'),authority,correlation,facts:safeFacts};
}
export function validatePolicy(input:unknown):BoundaryPolicy {
const o=object(input,'policy');onlyKeys(o,['version','id','sources','grants','expectedSurfaces','forbiddenCrossGroup'],'policy');
if(o.version!==1||!Array.isArray(o.sources)||o.sources.length>256||!Array.isArray(o.grants)||o.grants.length>2048)throw new Error('Invalid policy version or limits.');
const sources=o.sources.map(x=>{const a=object(x,'source');onlyKeys(a,['id','stages','surfaces','runIds','sandboxIds','isolationGroups','heartbeatMs','description'],'source');return {id:text(a.id,'source.id'),stages:strings(a.stages,'stages').map(v=>enumValue(v,STAGES,'stage')),surfaces:strings(a.surfaces,'surfaces').map(v=>enumValue(v,SURFACES,'surface')),runIds:strings(a.runIds,'runIds'),sandboxIds:a.sandboxIds===undefined?undefined:strings(a.sandboxIds,'sandboxIds'),isolationGroups:a.isolationGroups===undefined?undefined:strings(a.isolationGroups,'isolationGroups'),heartbeatMs:num(a.heartbeatMs,'heartbeatMs',1),description:optionalText(a,'description')};});
const grants=o.grants.map(x=>{const a=object(x,'grant');onlyKeys(a,['id','runIds','sandboxIds','targetIds','actions','effects','notBefore','expiresAt','actionDigest'],'grant');const g={id:text(a.id,'grant.id'),runIds:strings(a.runIds,'runIds'),sandboxIds:strings(a.sandboxIds,'sandboxIds'),targetIds:strings(a.targetIds,'targetIds'),actions:strings(a.actions,'actions'),effects:strings(a.effects,'effects').map(v=>enumValue(v,EFFECTS,'effect')),notBefore:num(a.notBefore,'notBefore'),expiresAt:num(a.expiresAt,'expiresAt'),actionDigest:optionalText(a,'actionDigest')};if(g.expiresAt<=g.notBefore)throw new Error('Grant expiry must follow its start.');return g;});
if(new Set(sources.map(s=>s.id)).size!==sources.length||new Set(grants.map(g=>g.id)).size!==grants.length)throw new Error('Duplicate policy identity.');
return {version:1,id:text(o.id,'policy.id'),sources,grants,expectedSurfaces:strings(o.expectedSurfaces,'expectedSurfaces',true).map(v=>enumValue(v,SURFACES,'surface')),forbiddenCrossGroup:boolean(o.forbiddenCrossGroup,'forbiddenCrossGroup')};
}
export function validateBundle(input:unknown,maxRecords=100_000):EvidenceBundle {
const o=object(input,'bundle');onlyKeys(o,['format','name','records','policy','asOf'],'bundle');
if(o.format!=='whalesong.evidence/v1'||!Array.isArray(o.records)||o.records.length>maxRecords)throw new Error('Invalid evidence bundle or record limit exceeded.');
const records=o.records.map(validateObservation),seen=new Set<string>();
for(const record of records){const key=observationKey(record);if(seen.has(key))throw new Error('Duplicate producer incarnation/sequence in evidence bundle. Import cancelled; resolve identity before import.');seen.add(key);}
return {format:o.format,name:text(o.name,'name'),records,policy:validatePolicy(o.policy),asOf:num(o.asOf,'asOf')};
}
export function sourceAccepts(source:EvidenceSource,o:Observation):boolean {
return source.id===o.sourceId&&source.stages.includes(o.stage)&&source.surfaces.includes(o.surface)&&source.runIds.includes(o.runId)&&(!source.sandboxIds||source.sandboxIds.includes(o.subject.sandboxId??''))&&(!source.isolationGroups||source.isolationGroups.includes(o.subject.isolationGroup??''));
}
export type Authorization = { decision:'permitted'|'denied'|'unknown'; reason:string };
/** Exact allowlists; no prefix matching, text approval, or ambient default allow. Not enforcement. */
export function authorize(o:Observation,policy:BoundaryPolicy):Authorization {
const source=policy.sources.find(s=>s.id===o.sourceId);
if(!source||!sourceAccepts(source,o))return {decision:'unknown',reason:'Source is not bound to this scope.'};
if(o.effect==='unknown')return {decision:'unknown',reason:'Actual effect not established.'};
const grant=policy.grants.find(g=>g.id===o.authority?.grantId);
if(!grant)return {decision:'denied',reason:'No matching operator-configured grant.'};
const u=o.time.uncertaintyMs;
if(u===undefined)return {decision:'unknown',reason:'Clock uncertainty absent; grant validity cannot be established.'};
if(o.time.wallMs-u<grant.notBefore||o.time.wallMs+u>=grant.expiresAt)return {decision:'denied',reason:'Outside grant validity interval (including clock uncertainty).'};
if(!grant.runIds.includes(o.runId)||!grant.sandboxIds.includes(o.subject.sandboxId??'')||!grant.targetIds.includes(o.target?.id??'')||!grant.actions.includes(o.action)||!grant.effects.includes(o.effect))return {decision:'denied',reason:'Action, effect, target, run, or sandbox is outside the grant.'};
if(grant.actionDigest&&o.actionDigest!==grant.actionDigest)return {decision:'denied',reason:'Approved action digest does not match.'};
return {decision:'permitted',reason:'Matches the supplied policy; source claims still require independent verification.'};
}
export const observationKey=(o:Observation):string=>JSON.stringify([o.sourceId,o.epoch,o.sequence]);
export const observationEventId=(o:Observation):string=>`obs:${encodeURIComponent(o.sourceId)}:${encodeURIComponent(o.epoch)}:${o.sequence}`;
export function canonicalJSON(v:unknown):string {
if(v===undefined)return 'null';if(v===null||typeof v!=='object')return JSON.stringify(v);
if(Array.isArray(v))return '['+v.map(canonicalJSON).join(',')+']';
return '{'+Object.entries(v).filter(([,x])=>x!==undefined).sort(([a],[b])=>a<b?-1:a>b?1:0).map(([k,x])=>JSON.stringify(k)+':'+canonicalJSON(x)).join(',')+'}';
}
const writeEffects=new Set<Effect>(['write','delete','publish','send','grant','revoke']);
const consequential=(o:Observation)=>o.stage==='effect'&&o.status==='completed'&&writeEffects.has(o.effect);
function definitelyAfter(a:Observation,b:Observation):boolean {
if(a.sourceId===b.sourceId&&a.epoch===b.epoch&&a.time.clockId&&a.time.clockId===b.time.clockId&&a.time.monotonicMs!==undefined&&b.time.monotonicMs!==undefined)return a.time.monotonicMs>b.time.monotonicMs;
return a.time.uncertaintyMs!==undefined&&b.time.uncertaintyMs!==undefined&&a.time.wallMs-a.time.uncertaintyMs>b.time.wallMs+b.time.uncertaintyMs;
}
/** asOf is receipt time for operator replay. Missing receipt stamps are excluded in replay. */
export function analyzeEvidence(bundle:EvidenceBundle,asOf=bundle.asOf,operatorReplay=false):BoundaryAnalysis {
let omittedFindings=0;
const policy=bundle.policy,findings:BoundaryFinding[]=[],rows:CoverageRow[]=[],unique=new Map<string,Observation>();
const visible=bundle.records.filter(o=>operatorReplay?o.receivedAt!==undefined&&o.receivedAt<=asOf:(o.receivedAt??o.time.wallMs)<=asOf);
const add=(code:string,title:string,detail:string,records:Observation[],severity:BoundaryFinding['severity']='warning')=>{
if(findings.length>=2000){omittedFindings++;return;}
const recordIds=records.slice(0,24).map(observationEventId),time=records.reduce((n,o)=>Math.min(n,o.time.wallMs),asOf);
findings.push({id:`boundary-${stableHash(code+canonicalJSON(recordIds))}`,code,title,detail,recordIds,time,severity});
};
for(const o of visible){const key=observationKey(o),old=unique.get(key);if(old){if(canonicalJSON(old)!==canonicalJSON(o))add('sequence-conflict','Conflicting producer sequence','Two different records claim the same producer incarnation and sequence. Neither is silently treated as corroboration.',[old,o],'critical');}else unique.set(key,o);}
const records=[...unique.values()].sort((a,b)=>a.time.wallMs-b.time.wallMs||observationKey(a).localeCompare(observationKey(b)));
const sourceMap=new Map(policy.sources.map(s=>[s.id,s])),bySource=new Map<string,Observation[]>(),operations=new Map<string,Observation[]>(),resource=new Map<string,Observation[]>(),bySandbox=new Map<string,Observation[]>();
const interactions=new Map<string,Interaction>(),authority={permitted:0,denied:0,unknown:0};
for(const o of records){
const src=sourceMap.get(o.sourceId);if(!src||!sourceAccepts(src,o))add('unbound-source','Source outside configured scope','This record is retained as an unverified assertion, not attributed to an allowed source for this run, stage, surface, and sandbox.',[o]);
const arr=bySource.get(o.sourceId)??[];arr.push(o);bySource.set(o.sourceId,arr);
if(o.operationId){const key=JSON.stringify([o.runId,o.subject.sandboxId,o.operationId]),a=operations.get(key)??[];a.push(o);operations.set(key,a);}
if(o.subject.sandboxId){const key=JSON.stringify([o.runId,o.subject.sandboxId]),a=bySandbox.get(key)??[];a.push(o);bySandbox.set(key,a);}
if(o.target){const key=JSON.stringify([o.runId,o.subject.sandboxId,o.subject.isolationGroup,o.target.id,o.target.version,o.stage,o.effect]),edge=interactions.get(key)??{runId:o.runId,sandboxId:o.subject.sandboxId,group:o.subject.isolationGroup,target:o.target.id,kind:o.target.kind,stage:o.stage,effect:o.effect,count:0,recordIds:[]};edge.count++;if(edge.recordIds.length<24)edge.recordIds.push(observationEventId(o));interactions.set(key,edge);
if(o.stage==='effect'&&o.status==='completed'){const a=resource.get(o.target.id)??[];a.push(o);resource.set(o.target.id,a);}}
if(o.stage==='dispatch'||o.stage==='effect'){
const result=authorize(o,policy);authority[result.decision]++;
if(o.authority?.claimed&&!policy.grants.some(g=>g.id===o.authority?.grantId))add('claimed-approval','Approval claim is not a grant','Peer, tool, and transcript assertions cannot create operator policy.',[o]);
if(consequential(o)&&result.decision==='denied')add('out-of-scope-effect','Effect outside supplied authorization',result.reason+' This is a policy comparison, not proof of malicious intent.',[o],'critical');
}
if(consequential(o)&&o.target?.boundary==='external')add('external-effect','External state-changing effect recorded','A discrete external effect is preserved even at whole-run zoom. Inspect its evidence and authorization; volume is not an impact measure.',[o],'info');
if(o.surface==='sandbox'&&(o.facts.privileged===true||o.facts.hostNetwork===true||o.facts.engineSocket===true))add('sandbox-exposure','Broad sandbox capability reported','The recorded profile reports privilege, host networking, or engine-socket access. A sandbox label is not evidence of confinement.',[o]);
if(consequential(o)&&o.facts.evaluatorArtifact===true)add('evaluator-change','Evaluation artifact changed','A recorded write changed an object identified as part of evaluation. Passing results before and after may not use the same acceptance criteria.',[o]);
if(o.facts.monitorState==='disabled'||o.facts.monitorState==='failed')add('monitor-offline','Monitor not evaluating','This source explicitly reports disabled or failed monitoring; no clean verdict is inferred.',[o],'critical');
if(typeof o.facts.dropped==='number'&&o.facts.dropped>0)add('telemetry-loss','Telemetry loss reported',`${o.facts.dropped} records were reported dropped. Recorded totals are incomplete.`,[o]);
}
for(const [key,ops] of operations){
const bound=ops.filter(o=>{const src=sourceMap.get(o.sourceId);return src&&sourceAccepts(src,o);});
const proposed=bound.filter(o=>o.stage==='intent'||o.stage==='dispatch'),actual=bound.filter(o=>o.stage==='runtime'||o.stage==='effect');
// Keep at most two DISTINCT digests per configured source, enough to show disagreement.
const examples:Observation[]=[],seenDigests=new Map<string,Set<string>>();
for(const o of proposed)if(o.actionDigest){const seen=seenDigests.get(o.sourceId)??new Set<string>();
if(seen.size<2&&!seen.has(o.actionDigest)){seen.add(o.actionDigest);examples.push(o);seenDigests.set(o.sourceId,seen);}}
const mismatch=actual.some(b=>b.actionDigest&&examples.some(a=>a.sourceId!==b.sourceId&&a.actionDigest!==b.actionDigest));
if(mismatch)add('action-mismatch','Dispatch and execution digests differ','Distinct configured sources disagree on the action identity for the same run/sandbox/operation. Instrumentation errors remain an alternative explanation.',ops,'critical');
if(proposed.some(o=>o.effect==='read'||o.facts.readOnlyHint===true)&&actual.some(consequential))add('effect-mismatch','Read claim followed by a write effect','The same correlated operation includes a read/read-only claim and a completed state-changing effect. An HTTP method or MCP annotation was not treated as a guarantee.',ops,'critical');
const stop=ops.filter(o=>o.action==='cancel.request'),after=actual.filter(o=>stop.length&&definitelyAfter(o,stop[0]));
if(stop.length&&after.length)add('cancel-not-stop','Execution continues after cancellation request','Cancellation is a request, not evidence of termination. The recorded clocks establish later activity for this operation.',[...stop,...after]);
void key;
}
if(policy.forbiddenCrossGroup)for(const [target,ops] of resource){
const readers=new Map<string,Set<string>>();let unknownVersionReaderGroups=new Set<string>();
for(const o of ops)if(o.effect==='read'&&o.subject.isolationGroup){const version=o.target?.version??'',groups=readers.get(version)??new Set<string>();groups.add(o.subject.isolationGroup);readers.set(version,groups);if(!version)unknownVersionReaderGroups.add(o.subject.isolationGroup);}
const allReaderGroups=new Set([...readers.values()].flatMap(g=>[...g]));
const other=(groups:Set<string>|undefined,group:string)=>!!groups&&(groups.size>1||groups.size===1&&!groups.has(group));
const bridge=ops.some(w=>consequential(w)&&w.subject.isolationGroup&&(other(unknownVersionReaderGroups,w.subject.isolationGroup)||other(w.target?.version?readers.get(w.target.version):allReaderGroups,w.subject.isolationGroup)));
if(bridge)add('shared-write-bridge','Shared writable resource crosses isolation groups',`Multiple groups read/write ${target}. This establishes a shared-state path under the supplied isolation policy, not intent or causal influence. Missing versions weaken the association.`,ops,'critical');
}
for(const ops of bySandbox.values()){
let pendingStop:Observation|undefined,stopped:Observation|undefined,waiting:Observation|undefined;
const afterStop:Observation[]=[],duringWait:Observation[]=[],requests:Observation[]=[];
for(const o of ops){
if(o.action==='sandbox.stop.request'){pendingStop=o;requests.push(o);}
if(o.action==='sandbox.stopped'&&o.stage==='control'){stopped=o;if(pendingStop&&definitelyAfter(o,pendingStop))pendingStop=undefined;}
if(o.action==='sandbox.started'){stopped=undefined;waiting=undefined;}
if(o.action==='sandbox.waiting')waiting=o;
if(o.action==='sandbox.resumed'&&waiting&&definitelyAfter(o,waiting))waiting=undefined;
if(o.stage==='runtime'||o.stage==='effect'){
if(stopped&&definitelyAfter(o,stopped)){if(!afterStop.length)afterStop.push(stopped);afterStop.push(o);}
if(waiting&&definitelyAfter(o,waiting)){if(!duringWait.length)duringWait.push(waiting);duringWait.push(o);}
}
}
if(pendingStop)add('stop-unconfirmed','Sandbox stop is unconfirmed','A stop request was recorded without a later stopped observation. Remote jobs and credentials are outside this lifecycle unless separately instrumented.',[pendingStop]);
if(afterStop.length)add('activity-after-stop','Activity recorded after sandbox stopped','Recorded execution/effects occur after a stopped assertion for this run and sandbox. Inspect identity, clocks, observer correctness, and descendants.',afterStop,'critical');
if(duringWait.length)add('waiting-active','Reported waiting overlaps later activity','Waiting describes one control state, not the absence of work across all observation surfaces.',duringWait);
}
for(const src of policy.sources){
const all=(bySource.get(src.id)??[]).filter(o=>sourceAccepts(src,o));let gaps=0;const gapEvidence:Observation[]=[];
const epochs=new Map<string,Map<number,Observation>>();for(const o of all){const seq=epochs.get(o.epoch)??new Map<number,Observation>();seq.set(o.sequence,o);epochs.set(o.epoch,seq);}
for(const bySequence of epochs.values()){const a=[...bySequence.keys()].sort((a,b)=>a-b);for(let i=1;i<a.length;i++){const gap=Math.max(0,a[i]-a[i-1]-1);gaps+=gap;if(gap)gapEvidence.push(bySequence.get(a[i-1])!,bySequence.get(a[i])!);}}
const timeOrdered=[...all].sort((a,b)=>a.sequence-b.sequence),clockEvidence:Observation[]=[];
for(let i=1;i<timeOrdered.length;i++){const a=timeOrdered[i-1],b=timeOrdered[i];if(a.epoch!==b.epoch||a.time.clockId!==b.time.clockId)continue;
if(a.time.monotonicMs!==undefined&&b.time.monotonicMs!==undefined&&a.time.taskMs!==undefined&&b.time.taskMs!==undefined){const real=b.time.monotonicMs-a.time.monotonicMs,task=b.time.taskMs-a.time.taskMs;if(real>0&&Math.abs(task-real)>Math.max(1000,real*.25))clockEvidence.push(a,b);}}
if(clockEvidence.length)add('clock-divergence','Task clock differs from runtime clock','Same-source monotonic and task-time deltas differ by over 25% and 1 second. Simulation or clock configuration can explain this; it is not proof of evasion.',clockEvidence);
if(gaps)add('sequence-gap','Producer sequence gaps',`${src.id} is missing ${gaps} sequence positions between observed records. Prefix and suffix completeness are unknown.`,gapEvidence);
for(const surface of src.surfaces){const matching=all.filter(o=>o.surface===surface),last=matching.reduce<Observation|undefined>((a,b)=>(a&&(a.receivedAt??a.time.wallMs)>(b.receivedAt??b.time.wallMs))?a:b,undefined),at=last?.receivedAt??null;
rows.push({sourceId:src.id,surface,status:!last?'missing':at===null?'unverified-time':last.facts.monitorState==='disabled'||last.facts.monitorState==='failed'?'disabled':asOf-at!>src.heartbeatMs?'stale':'observed',count:matching.length,lastReceivedAt:at,sequenceGaps:gaps});}
}
for(const surface of policy.expectedSurfaces)if(!policy.sources.some(s=>s.surfaces.includes(surface)))rows.push({sourceId:'—',surface,status:'unconfigured',count:0,lastReceivedAt:null,sequenceGaps:0});
const absent=rows.filter(r=>r.status!=='observed');if(absent.length)add('coverage-gap','Observation coverage is incomplete',`${absent.length} configured/expected source-surface pairs are missing, stale, disabled, unconfigured, or lack recorder receipt stamps. No clean result is inferred for them.`,[]);
return {version:1,omittedFindings,asOf,inputRecords:bundle.records.length,visibleRecords:visible.length,uniqueRecords:records.length,futureRecords:bundle.records.length-visible.length,findings:findings.sort((a,b)=>a.time-b.time||a.id.localeCompare(b.id)),coverage:rows,interactions:[...interactions.values()],runs:new Set(records.map(o=>o.runId)).size,sandboxes:new Set(records.map(o=>o.subject.sandboxId).filter(Boolean)).size,externalEffects:records.filter(o=>consequential(o)&&o.target?.boundary==='external').length,unknownEffects:records.filter(o=>o.effect==='unknown').length,authority,provenance:'Offline source and policy declarations; not authenticated by the viewer. Observed means reported by an attributed source, not independently certified.'};
}
const category:Record<Surface,Category>={'model-api':'reasoning',http:'network',mcp:'tool',git:'code',filesystem:'filesystem',process:'code',sandbox:'orchestration',dns:'network',socket:'network',message:'communication',database:'memory',artifact:'filesystem',browser:'browser',clipboard:'communication',other:'other'};
export function evidenceToTrace(bundle:EvidenceBundle):Trace {
const base=bundle.records.reduce((n,o)=>Math.min(n,o.time.wallMs),bundle.asOf),id=`evidence-${stableHash(bundle.name)}`;
const events:WhaleEvent[]=bundle.records.map(o=>({schemaVersion:1,id:observationEventId(o),traceId:id,startTime:Math.max(0,o.time.wallMs-base),endTime:Math.max(0,o.time.wallMs-base),agentId:`${o.runId}/${o.subject.agentId}`,category:category[o.surface],name:o.action,subtype:o.stage,status:o.status==='error'?'error':o.status==='completed'?'success':o.status==='started'?'running':'unknown',targetId:o.target?.id,targetType:o.target?.kind,attributes:{'evidence.surface':o.surface,'evidence.stage':o.stage,'evidence.effect':o.effect,'evidence.run':o.runId,'evidence.source':o.sourceId,'evidence.sandbox':o.subject.sandboxId??'unknown','evidence.isolationGroup':o.subject.isolationGroup??'unknown',...o.facts},observation:o}));
events.sort((a,b)=>a.startTime-b.startTime||a.id.localeCompare(b.id));
return {id,name:bundle.name,source:'jsonl',privacy:'metadata',events,duration:Math.max(1,bundle.asOf-base,events.reduce((n,e)=>Math.max(n,e.endTime),0)),originTime:String(base),warnings:['Boundary evidence is metadata-only but identifiers and behavioral timing remain sensitive.','Imported policy/provenance are declarations, not authenticated by this viewer.','No universal interception: only instrumented sources can be observed.'],metadata:{evidenceBundle:{...bundle,records:undefined},evidenceBase:base}};
}
export function bundleFromTrace(trace:Trace):EvidenceBundle|undefined {
const records=trace.events.flatMap(e=>e.observation?[e.observation]:[]);if(!records.length)return undefined;
const header=trace.metadata.evidenceBundle as Omit<EvidenceBundle,'records'>|undefined;
if(!header)return undefined;return {...header,records};
}
export function boundaryFindings(trace:Trace):Finding[]{
const bundle=bundleFromTrace(trace);if(!bundle)return [];const base=Number(trace.metadata.evidenceBase??0);
return analyzeEvidence(bundle).findings.map(f=>({id:f.id,kind:'boundary',severity:f.severity,title:f.title,detail:f.detail,startTime:Math.max(0,f.time-base),endTime:Math.max(0,f.time-base)+1,eventIds:f.recordIds,evidence:{code:f.code,provenance:'declared',scope:'cross-run evidence case'}}));
}
+294
View File
@@ -0,0 +1,294 @@
import { fromCodewhaleRuntime, fromCodewhaleSession, isCodewhaleRuntimeDocument, isCodewhaleSession } from './codewhale.js';
import { validateBundle, evidenceToTrace, validateObservation, bundleFromTrace } from './evidence.js';
import { CATEGORIES, errorOnsetOf, type Category, type PrivacyMode, type Status, type Trace, type WhaleEvent } from './model.js';
export interface ImportOptions {
privacy?: PrivacyMode; maxEvents?: number; maxBytes?: number; maxTraces?: number;
/** Optional local-only redaction hook. Never invoked by a network service. */
redactor?: (value: unknown, path: string) => unknown;
}
type Obj = Record<string, any>;
const obj = (v: unknown): Obj => v !== null && typeof v === 'object' && !Array.isArray(v) ? v as Obj : {};
const list = (v: unknown): any[] => Array.isArray(v) ? v : [];
const str = (v: unknown): string | undefined => typeof v === 'string' && v.length ? v : undefined;
const SECRET_KEY = /(?:^|[._-])(?:api[._-]?key|authorization|password|passwd|secret|access[._-]?token|refresh[._-]?token|cookie|private[._-]?key)(?:$|[._-])/i;
const SAFE_META = /^(?:gen_ai\.(?:usage\.(?:input_tokens(?:\.cached)?|output_tokens|cache_read\.input_tokens|cost)|request\.(?:model|max_tokens)|response\.model|provider\.name|operation\.name)|whalesong\.(?:category|agent_id)|agent\.(?:id|parent_id|state)|(?:http|rpc)\.(?:response\.status_code|method)|retry\.(?:count|attempt)|context\.(?:tokens|limit)|phase|spawnedAgentId|parentAgentId|targetType|iteration|benchmark|blocked)$/;
/** Best effort, not a guarantee of de-identification. Preserves structure, not secrets. */
export function redact(value: unknown, path = '', hook?: ImportOptions['redactor'], depth = 0, seen = new WeakSet<object>()): unknown {
if (depth > 48) return '[REDACTED: nesting limit]';
const key = path.split('/').at(-1) ?? '';
if (SECRET_KEY.test(key) || /^(apiKey|accessToken|refreshToken|privateKey)$/i.test(key)) return '[REDACTED]';
// OTLP attributes encode a sensitive name in `key`, not in the JSON path.
// Redact the AnyValue itself while preserving a valid OTLP value envelope.
const record = obj(value);
if (typeof record.key === 'string' && Object.hasOwn(record, 'value') &&
(SECRET_KEY.test(record.key) || /^(apiKey|accessToken|refreshToken|privateKey)$/i.test(record.key))) {
const safe = { ...record, value: { stringValue: '[REDACTED]' } };
// Only the known KeyValue fields are kept; arbitrary siblings cannot bypass redaction.
const cleaned = { key: record.key, value: safe.value };
return hook ? hook(cleaned, path) : cleaned;
}
if (value && typeof value === 'object') { if (seen.has(value)) return '[REDACTED: cycle]'; seen.add(value); }
let out: unknown = value;
if (typeof value === 'string') {
out = value
.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/g, '[REDACTED: private key]')
.replace(/\b(?:sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16})\b/g, '[REDACTED: key]')
.replace(/\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{16,})\b/g, '[REDACTED: token]')
.replace(/(https?:\/\/)[^\s/@:]+:[^\s/@]+@/gi, '$1[REDACTED]@')
.replace(/\bBearer\s+[A-Za-z0-9._~+\/-]+=*/gi, 'Bearer [REDACTED]')
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '[REDACTED: JWT]')
.replace(/((?:api[_-]?key|password|secret)\s*[=:]\s*)[^\s,;"'}]+/gi, '$1[REDACTED]')
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[REDACTED: email]');
} else if (Array.isArray(value)) {
out = value.map((v, i) => redact(v, `${path}/${i}`, hook, depth + 1, seen));
} else if (value && typeof value === 'object') {
out = Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redact(v, `${path}/${k}`, hook, depth + 1, seen)]));
}
if (value && typeof value === 'object') seen.delete(value);
return hook ? hook(out, path) : out;
}
function privacyEvent(e: WhaleEvent, mode: PrivacyMode): WhaleEvent {
if (mode === 'metadata') {
const { payload: _payload, raw: _raw, ...rest } = e;
return { ...rest, links: e.links?.map(link=>({traceId:link.traceId,spanId:link.spanId})), attributes: Object.fromEntries(Object.entries(e.attributes).filter(([k, v]) =>
SAFE_META.test(k) && (typeof v !== 'object' || v === null)
|| ['whalesong.container', 'codewhale.container', 'whalesong.waiting'].includes(k) && typeof v === 'boolean'
|| k === 'whalesong.error_onset_ms' && typeof v === 'number' && Number.isFinite(v) && v >= 0)) };
}
return e;
}
function number(v: unknown, field: string, optional = true): number | undefined {
if (v === undefined || v === null) {
if (optional) return undefined;
throw new Error(`Missing ${field}. Times must be numeric milliseconds.`);
}
if (typeof v !== 'number' || !Number.isFinite(v) || Math.abs(v) > Number.MAX_SAFE_INTEGER) throw new Error(`Invalid ${field}: expected a finite, safely representable number.`);
return v;
}
function nonnegative(v: unknown, field: string): number | undefined {
const n = number(v, field); if (n !== undefined && n < 0) throw new Error(`${field} must be nonnegative.`); return n;
}
function numericAttr(v: unknown): number | undefined {
if (v === undefined || v === null || v === '') return undefined;
const n = Number(v); return Number.isFinite(n) && n >= 0 ? n : undefined;
}
export function categoryFor(name: string, a: Obj): Category {
const explicit = a['whalesong.category'] ?? a.category;
if (CATEGORIES.includes(explicit)) return explicit;
const n = name.toLowerCase(), op = String(a['gen_ai.operation.name'] ?? '').toLowerCase();
if (/exception|^error\b/.test(n)) return 'error';
if (/spawn|fork|subagent/.test(n) || op === 'invoke_agent') return 'agent';
if (/message\.send|handoff|agent\.message/.test(n)) return 'communication';
if (/retrieve|retrieval|context|embedding|vector|memory|rag/.test(n)) return 'memory';
if (/browser|navigate|screenshot|click|playwright/.test(n)) return 'browser';
if (/read_file|write_file|list_dir|file\.|filesystem|fs\.|patch/.test(n)) return 'filesystem';
if (/exec|shell|run_test|cargo|pytest|compile/.test(n)) return 'code';
if (a['gen_ai.request.model'] || a['llm.model_name'] || /reason|completion|generate|chat|llm/.test(n) || ['chat', 'generate_content', 'text_completion'].includes(op)) return 'reasoning';
if (a['gen_ai.tool.name'] || a['tool.name'] || /tool|search|function/.test(n)) return 'tool';
if (a['http.request.method'] || a['http.method'] || a['rpc.system'] || /http|request|api|fetch|network/.test(n)) return 'network';
if (/user|human|approval/.test(n)) return 'human';
if (/orchestrat|workflow|phase|join|session|root/.test(n)) return 'orchestration';
return 'other';
}
export function decodeAnyValue(v: unknown): unknown {
const a = obj(v);
if ('stringValue' in a) return a.stringValue;
if ('boolValue' in a) return a.boolValue;
if ('doubleValue' in a) return a.doubleValue;
if ('intValue' in a) {
const n = Number(a.intValue);
return Number.isSafeInteger(n) ? n : String(a.intValue);
}
if ('bytesValue' in a) return a.bytesValue;
if ('arrayValue' in a) return list(obj(a.arrayValue).values).map(decodeAnyValue);
if ('kvlistValue' in a) return attributes(obj(a.kvlistValue).values);
return v;
}
export function attributes(value: unknown): Obj {
if (!Array.isArray(value)) return obj(value);
return Object.fromEntries(value.filter(x => typeof x?.key === 'string').map(x => [x.key, decodeAnyValue(x.value)]));
}
function ns(v: unknown, field: string): bigint {
if (typeof v === 'number' && !Number.isSafeInteger(v)) throw new Error(`${field} lost precision: encode OTLP nanoseconds as a decimal string.`);
const s = String(v ?? '');
if (!/^\d{1,20}$/.test(s) || BigInt(s) > 18446744073709551615n) throw new Error(`Invalid ${field}: expected an OTLP nanosecond integer string.`);
return BigInt(s);
}
function otelStatus(v: unknown): Status {
const c = obj(v).code;
return c === 2 || c === 'STATUS_CODE_ERROR' ? 'error' : c === 1 || c === 'STATUS_CODE_OK' ? 'success' : 'unknown';
}
function normalizedEvent(v: unknown, index: number): WhaleEvent {
const a = obj(v), id = str(a.id), traceId = str(a.traceId), name = str(a.name);
if (!id || !traceId || !name) throw new Error(`Record ${index + 1} requires nonempty id, traceId, and name.`);
if (a.schemaVersion !== undefined && a.schemaVersion !== 1) throw new Error(`Record ${index + 1}: unsupported schemaVersion ${a.schemaVersion}.`);
const startTime = number(a.startTime, 'startTime', false)!;
const endTime = number(a.endTime, 'endTime') ?? startTime;
if (endTime < startTime) throw new Error(`Record ${index + 1}: endTime precedes startTime.`);
if (a.category !== undefined && !CATEGORIES.includes(a.category)) throw new Error(`Unknown category "${a.category}". Use "other" plus subtype for extensions.`);
const allowed: Status[] = ['pending', 'running', 'success', 'error', 'unknown'];
if (a.status !== undefined && !allowed.includes(a.status)) throw new Error(`Record ${index + 1}: invalid status.`);
const at = obj(a.attributes);
return {
schemaVersion: 1, id, traceId, name, parentId: str(a.parentId), startTime, endTime,
openEnded: a.endTime === undefined || a.openEnded === true,
agentId: str(a.agentId) ?? 'unattributed', agentType: str(a.agentType), parentAgentId: str(a.parentAgentId),
category: a.category ?? categoryFor(name, at), subtype: str(a.subtype),
model: str(a.model), provider: str(a.provider), tool: str(a.tool),
inputTokens: nonnegative(a.inputTokens, 'inputTokens'), outputTokens: nonnegative(a.outputTokens, 'outputTokens'),
cachedTokens: nonnegative(a.cachedTokens, 'cachedTokens'), cost: nonnegative(a.cost, 'cost'),
costCurrency: str(a.costCurrency), latency: nonnegative(a.latency, 'latency'),
contextTokens: nonnegative(a.contextTokens, 'contextTokens'), contextLimit: nonnegative(a.contextLimit, 'contextLimit'),
retry: nonnegative(a.retry, 'retry'), status: a.status ?? 'unknown',
sourceId: str(a.sourceId), targetId: str(a.targetId), targetType: str(a.targetType),
links: list(a.links).filter(x => typeof x?.traceId === 'string' && typeof x?.spanId === 'string'),
attributes: at, payload: a.payload, raw: a.raw ?? v,
observation: a.observation === undefined ? undefined : validateObservation(a.observation),
};
}
interface OtlpRecord { span: Obj; resource: Obj; scope: Obj; resourceSchema?: string; scopeSchema?: string }
function otlpRecords(doc: Obj): OtlpRecord[] {
const out: OtlpRecord[] = [];
for (const r of list(doc.resourceSpans)) {
for (const s of list(r.scopeSpans ?? r.instrumentationLibrarySpans)) {
for (const span of list(s.spans)) out.push({ span: obj(span), resource: obj(r.resource), scope: obj(s.scope ?? s.instrumentationLibrary), resourceSchema: r.schemaUrl, scopeSchema: s.schemaUrl });
}
}
return out;
}
function fromOTLP(doc: Obj, maxEvents: number): { events: WhaleEvent[]; origins: Map<string, string>; warnings: string[] } {
const records = otlpRecords(doc), bases = new Map<string, bigint>(), warnings: string[] = [];
if (!records.length) throw new Error('No spans found in resourceSpans[].scopeSpans[].spans[].');
if (records.length > maxEvents) throw new Error(`Import exceeds the ${maxEvents.toLocaleString()} event limit.`);
for (const { span: s } of records) {
if (!str(s.traceId) || !str(s.spanId)) throw new Error('Every OTLP span requires traceId and spanId.');
const start = ns(s.startTimeUnixNano, 'startTimeUnixNano');
const end = s.endTimeUnixNano === undefined ? start : ns(s.endTimeUnixNano, 'endTimeUnixNano');
if (end < start) throw new Error(`OTLP span ${s.spanId}: end precedes start.`);
let earliest = start;
for (const e of list(s.events)) { const t = ns(e.timeUnixNano, 'event.timeUnixNano'); if (t < earliest) earliest = t; }
if (!bases.has(s.traceId) || earliest < bases.get(s.traceId)!) bases.set(s.traceId, earliest);
if ((s.droppedEventsCount ?? 0) > 0) warnings.push(`Span ${s.spanId} reports ${s.droppedEventsCount} dropped events; coverage is incomplete.`);
}
const events: WhaleEvent[] = [];
for (const rec of records) {
const s = rec.span, a = { ...attributes(rec.resource.attributes), ...attributes(s.attributes) };
const origin = bases.get(s.traceId)!, start = ns(s.startTimeUnixNano, 'startTimeUnixNano');
const end = s.endTimeUnixNano === undefined ? start : ns(s.endTimeUnixNano, 'endTimeUnixNano');
const name = String(s.name ?? 'unnamed span');
const e: WhaleEvent = {
schemaVersion: 1, id: s.spanId, traceId: s.traceId, parentId: str(s.parentSpanId),
name, startTime: Number(start - origin) / 1e6, endTime: Number(end - origin) / 1e6,
openEnded: s.endTimeUnixNano === undefined,
agentId: String(a['whalesong.agent_id'] ?? a['gen_ai.agent.id'] ?? a['agent.id'] ?? a['service.name'] ?? 'unattributed'),
parentAgentId: str(a['agent.parent_id']), agentType: str(a['gen_ai.agent.name']),
category: categoryFor(name, a), model: str(a['gen_ai.request.model'] ?? a['gen_ai.response.model'] ?? a['llm.model_name']),
provider: str(a['gen_ai.provider.name'] ?? a['gen_ai.system']), tool: str(a['gen_ai.tool.name'] ?? a['tool.name']),
inputTokens: numericAttr(a['gen_ai.usage.input_tokens'] ?? a['llm.token_count.prompt']),
outputTokens: numericAttr(a['gen_ai.usage.output_tokens'] ?? a['llm.token_count.completion']),
cachedTokens: numericAttr(a['gen_ai.usage.cache_read.input_tokens'] ?? a['gen_ai.usage.input_tokens.cached']),
cost: numericAttr(a['whalesong.cost'] ?? a['gen_ai.usage.cost']), costCurrency: str(a['whalesong.cost_currency']),
contextTokens: numericAttr(a['context.tokens']), contextLimit: numericAttr(a['context.limit']),
retry: numericAttr(a['retry.count'] ?? a['retry.attempt']), status: otelStatus(s.status),
latency: Number(end - start) / 1e6, attributes: a,
sourceId: str(a['whalesong.source_id']), targetId: str(a['whalesong.target_id']), targetType: str(a['whalesong.target_type']),
links: list(s.links).map(l => ({ traceId: l.traceId, spanId: l.spanId, attributes: attributes(l.attributes) })),
payload: a['gen_ai.input.messages'] !== undefined || a['gen_ai.output.messages'] !== undefined ? {
request: a['gen_ai.input.messages'], response: a['gen_ai.output.messages'],
} : undefined,
raw: rec,
};
events.push(e);
for (const [i, record] of list(s.events).entries()) {
const ea = attributes(record.attributes), time = Number(ns(record.timeUnixNano, 'event.timeUnixNano') - origin) / 1e6;
const ename = String(record.name ?? 'span event');
events.push({ schemaVersion: 1, id: `${s.spanId}/event/${i}`, traceId: s.traceId, parentId: s.spanId,
startTime: time, endTime: time, agentId: e.agentId, name: ename, category: categoryFor(ename, ea),
status: ename === 'exception' ? 'error' : 'unknown', attributes: ea, raw: { event: record, spanId: s.spanId, resource: rec.resource, scope: rec.scope } });
}
if (events.length > maxEvents) throw new Error(`Import exceeds the ${maxEvents.toLocaleString()} event limit, including span events.`);
}
return { events, origins: new Map([...bases].map(([k, v]) => [k, v.toString()])), warnings };
}
/** Parse strictly: malformed lines or duplicate identities never disappear silently. */
export function importTrace(text: string, filename = 'Imported trace', options: ImportOptions = {}): Trace[] {
const mode = options.privacy ?? 'redact', maxEvents = options.maxEvents ?? 250_000;
if (!['redact', 'metadata', 'retain'].includes(mode)) throw new Error('Unknown privacy mode.');
const maxTraces = options.maxTraces ?? 8;
if (!Number.isInteger(maxEvents) || maxEvents < 1 || maxEvents > 250_000) throw new Error('maxEvents must be in [1, 250000].');
if (!Number.isInteger(maxTraces) || maxTraces < 1 || maxTraces > 64) throw new Error('maxTraces must be in [1, 64].');
if (new TextEncoder().encode(text).length > (options.maxBytes ?? 64 * 1024 * 1024)) throw new Error('File exceeds the 64 MiB MVP import limit. Split the export by trace.');
const trimmed = text.replace(/^\uFEFF/, '').trim();
if (!trimmed) throw new Error('The trace file is empty.');
let document: unknown;
try { document = JSON.parse(trimmed); }
catch {
document = trimmed.split(/\r?\n/).filter(l => l.trim()).map((line, i) => {
try { return JSON.parse(line); } catch { throw new Error(`Invalid JSON on nonempty line ${i + 1}. Import cancelled; no rows were skipped.`); }
});
}
// Transform before both normalization and raw retention, so raw cannot bypass redaction.
const safe = mode === 'retain' ? (options.redactor ? options.redactor(document, '') : document) : redact(document, '', options.redactor);
const root = obj(safe);
if(root.format === 'whalesong.evidence/v1') return [evidenceToTrace(validateBundle(root, Math.min(maxEvents, 100_000)))];
if (isCodewhaleSession(safe)) {
const trace = fromCodewhaleSession(safe, filename, maxEvents);
trace.privacy = mode;
trace.events = trace.events.map(e => privacyEvent(e, mode));
return [trace];
}
const records = Array.isArray(safe) ? safe : Array.isArray(root.events) ? root.events : null;
if (isCodewhaleRuntimeDocument(records ?? [safe])) {
const trace = fromCodewhaleRuntime(records ?? [safe], filename, maxEvents);
trace.privacy = mode;
trace.events = trace.events.map(e => privacyEvent(e, mode));
return [trace];
}
const isOTLP = Array.isArray(root.resourceSpans);
let all: WhaleEvent[], origins = new Map<string, string>(), warnings: string[] = [];
if (isOTLP) { const r = fromOTLP(root, maxEvents); all = r.events; origins = r.origins; warnings = r.warnings; }
else {
const incoming = records ?? [safe];
if (incoming.length > maxEvents) throw new Error(`Import exceeds the ${maxEvents.toLocaleString()} event limit.`);
all = incoming.map(normalizedEvent);
}
if (!all.length) throw new Error('The file contains no events.');
const groups = new Map<string, WhaleEvent[]>(), ids = new Set<string>();
for (const e of all) {
const key = `${e.traceId}\0${e.id}`;
if (ids.has(key)) throw new Error(`Duplicate event identity (${e.traceId}, ${e.id}). Import cancelled.`);
ids.add(key);
const group = groups.get(e.traceId) ?? []; group.push(privacyEvent(e, mode)); groups.set(e.traceId, group);
if (groups.size > maxTraces) throw new Error(`This import contains more than ${maxTraces} traces. Split it by trace ID.`);
}
return [...groups].map(([id, events]) => {
events.sort((a, b) => a.startTime - b.startTime || a.id.localeCompare(b.id));
const base = isOTLP ? 0 : events[0].startTime;
if (!isOTLP && events.some(e => Math.abs(e.endTime - base) > Number.MAX_SAFE_INTEGER)) throw new Error('Trace duration exceeds safely representable milliseconds.');
for (const e of events) {
if (e.attributes['whalesong.error_onset_ms'] !== undefined) e.attributes['whalesong.error_onset_ms'] = errorOnsetOf(e) - base;
e.startTime -= base; e.endTime -= base;
}
const localIds = new Set(events.map(e => e.id)), missingParents = events.filter(e => e.parentId && !localIds.has(e.parentId)).length;
const traceWarnings = [...warnings];
if (missingParents) traceWarnings.push(`${missingParents} parent spans are absent from this trace; no parent relationship was invented.`);
if (events.some(e => e.openEnded)) traceWarnings.push('Open spans have unknown duration and are displayed as onset-only, not extended into invented activity.');
const currencies = new Set(events.filter(e => e.cost !== undefined).map(e => e.costCurrency ?? 'unspecified'));
if (currencies.size > 1) traceWarnings.push('Mixed cost currencies: aggregate cost comparison is disabled.');
return { id, name: groups.size > 1 ? `${filename} · ${id.slice(0, 8)}` : String(root.name ?? filename),
events, duration: Math.max(1, events.reduce((m, e) => Math.max(m, e.endTime, e.startTime, e.status === 'error' ? errorOnsetOf(e) : 0), 0)),
originTime: origins.get(id) ?? (root.originTime !== undefined && base === 0 ? str(root.originTime) : `${base} ms`),
source: isOTLP ? 'otlp' as const : 'jsonl' as const, privacy: mode, warnings: [...new Set(traceWarnings)],
metadata: { ...(mode === 'metadata' ? {} : obj(root.metadata)), timeUnit: 'ms', originUnit: isOTLP ? 'unix-nanoseconds' : 'milliseconds', sourceFilename: filename },
};
});
}
export function exportJSONL(trace: Trace): string {
const bundle=bundleFromTrace(trace);
if(bundle)return JSON.stringify(bundle);
return trace.events.map(e => JSON.stringify(e)).join('\n') + '\n';
}
+105
View File
@@ -0,0 +1,105 @@
/** Versioned, vendor-neutral trace and signal contracts. All internal time is ms. */
export const CATEGORIES = [
'reasoning', 'tool', 'memory', 'code', 'filesystem', 'network', 'browser',
'communication', 'agent', 'orchestration', 'error', 'human', 'other',
] as const;
export type Category = typeof CATEGORIES[number];
export type Status = 'pending' | 'running' | 'success' | 'error' | 'unknown';
export type PrivacyMode = 'redact' | 'metadata' | 'retain';
export interface WhaleEvent {
observation?: import('./evidence.js').Observation;
schemaVersion: 1;
id: string; traceId: string; parentId?: string;
startTime: number; endTime: number; openEnded?: boolean;
agentId: string; agentType?: string; parentAgentId?: string;
category: Category; subtype?: string; name: string;
model?: string; provider?: string; tool?: string;
inputTokens?: number; outputTokens?: number; cachedTokens?: number;
cost?: number; costCurrency?: string; latency?: number;
contextTokens?: number; contextLimit?: number; retry?: number;
status: Status;
sourceId?: string; targetId?: string; targetType?: string;
links?: { traceId: string; spanId: string; attributes?: Record<string, unknown> }[];
attributes: Record<string, unknown>; payload?: unknown; raw?: unknown;
}
export interface Trace {
id: string; name: string; description?: string; events: WhaleEvent[];
duration: number; originTime?: string; source: 'demo' | 'jsonl' | 'otlp' | 'codewhale' | 'platform';
privacy: PrivacyMode; warnings: string[]; metadata: Record<string, unknown>;
}
export interface BinLevel {
binMs: number; length: number;
/** Channel-major arrays, index = channel * length + time bin. */
onsets: Float64Array; activeMs: Float64Array; outputTokens: Float64Array;
cost: Float64Array; errors: Float64Array; peak: Float64Array;
}
export interface SignalPyramid {
duration: number; channels: readonly Category[]; levels: BinLevel[];
/** Fixed calibration across zoom levels; sharing this locks A/B scales. */
calibration: Record<Metric, number>;
}
export type Metric = 'activity' | 'onsets' | 'tokens' | 'cost';
export type FindingKind = 'loop' | 'burst' | 'gap' | 'retry' | 'divergence' | 'spawn' | 'context' | 'boundary';
export interface Finding {
id: string; kind: FindingKind; severity: 'info' | 'warning' | 'critical';
title: string; detail: string; startTime: number; endTime: number;
agentId?: string; eventIds: string[]; evidence: Record<string, unknown>;
}
export interface Fingerprint {
version: 1; channels: number[]; temporal: number[]; autocorrelation: number[];
features: Record<string, number>; vector: number[];
}
export interface Statistics {
events: number; agents: number; duration: number; errors: number; retries: number;
inputTokens: number; outputTokens: number; cachedTokens: number;
cost: number; costKnown: boolean; costCount: number; tokenCount: number; outputTokenCount: number; inputTokenCount: number;
latencyP50: number; latencyP95: number; observedGapRatio: number;
}
export interface Analysis {
sessionId?: string;
trace: Trace; pyramid: SignalPyramid; findings: Finding[];
fingerprint: Fingerprint; stats: Statistics; computeMs: number;
}
export interface Filters {
query: string; category: string; agent: string; model: string; tool: string; status: string;
}
export const EMPTY_FILTERS: Filters = { query: '', category: '', agent: '', model: '', tool: '', status: '' };
export function eventMatches(e: WhaleEvent, f: Filters): boolean {
if (f.category && e.category !== f.category) return false;
if (f.agent && e.agentId !== f.agent) return false;
if (f.model && e.model !== f.model) return false;
if (f.tool && e.tool !== f.tool) return false;
if (f.status && e.status !== f.status) return false;
if (f.query) {
const q = f.query.toLowerCase();
// Search is deliberately content-aware but runs only over locally retained fields.
if (![e.name, e.id, e.agentId, e.model, e.tool, e.provider, e.category,
JSON.stringify(e.attributes), JSON.stringify(e.payload), JSON.stringify(e.raw)].filter(Boolean).join(' ').toLowerCase().includes(q)) return false;
}
return true;
}
export const durationOf = (e: WhaleEvent): number => Math.max(0, e.endTime - e.startTime);
/** An explicit failure receipt can arrive after a span began or ended. Keep
* its timestamp distinct from the operation onset in every signal view. */
export function errorOnsetOf(e: WhaleEvent): number {
const time = e.attributes['whalesong.error_onset_ms'];
if (time === undefined) return e.startTime;
if (typeof time !== 'number' || !Number.isFinite(time) || time < e.startTime)
throw new Error('Invalid failure observation time.');
return time;
}
export function stableHash(text: string, seed = 2166136261): number {
let h = seed;
for (let i = 0; i < text.length; i++) { h ^= text.charCodeAt(i); h = Math.imul(h, 16777619); }
return h >>> 0;
}
export function quantile(a: number[], q: number): number {
if (!a.length) return 0;
const s = [...a].sort((a, b) => a - b), x = Math.min(1, Math.max(0, q)) * (s.length - 1);
return s[Math.floor(x)] + (s[Math.ceil(x)] - s[Math.floor(x)]) * (x % 1);
}
export function clamp(x: number, lo: number, hi: number): number { return Math.max(lo, Math.min(hi, x)); }
export function formatTime(ms: number, precise = false): string {
const m = Math.floor(Math.max(0, ms) / 60000), s = Math.floor(Math.max(0, ms) / 1000) % 60;
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}${precise ? '.' + String(Math.floor(ms % 1000)).padStart(3, '0') : ''}`;
}
+95
View File
@@ -0,0 +1,95 @@
import { CHANNELS } from './pet-sim.js';
import { clamp, stableHash } from './model.js';
import type { WorldFrame } from './pet-world.js';
export interface PetVoice {
id: string; start: number; duration: number; frequency: number;
gain: number; pan: number; kind: 'tone' | 'noise';
}
/** Core emits score events; WebAudio / AVAudioEngine only present their PCM.
* Calling at any display cadence produces the same score when all world ticks
* are supplied. Calling twice for a world tick cannot retrigger a voice. */
export class PetScore {
private lastWindow = -1;
private lastSequence = -1;
private lastAddress = false;
checkpoint(): [number, number, boolean] { return [this.lastWindow, this.lastSequence, this.lastAddress]; }
restore(value: unknown): void {
if (!Array.isArray(value) || value.length !== 3
|| !value.slice(0, 2).every(n => Number.isSafeInteger(n) && n >= -1 && n <= 216_000) || typeof value[2] !== 'boolean')
throw new Error('Invalid pet score checkpoint.');
[this.lastWindow, this.lastSequence, this.lastAddress] = value as [number, number, boolean];
}
voices(frame: WorldFrame): PetVoice[] {
const time = frame.timeMs / 1000, window = Math.floor((frame.timeMs + 1e-7) / 400);
const out: PetVoice[] = [];
const add = (id: string, frequency: number, duration: number, gain: number, pan = 0, delay = 0, kind: PetVoice['kind'] = 'tone') =>
out.push({ id, start: time + delay, duration, frequency, gain, pan, kind });
const t = frame.telemetry, fresh = t !== undefined && t.sequence !== this.lastSequence;
if (fresh) {
this.lastSequence = t.sequence;
for (let c = 0; c < CHANNELS.length; c++) {
const channel = CHANNELS[c], n = t.onsets[c];
if (!n || channel.sustained || ['human', 'error'].includes(channel.key)) continue;
add(`onset:${t.sequence}:${c}`, channel.freq, .24, .035 * Math.min(2, Math.sqrt(n)), (c / 12 - .5) * .7);
}
if (t.errors) add(`tear:${t.sequence}`, CHANNELS.find(c => c.key === 'error')!.freq, .22, .05, 0, 0, 'noise');
}
const address = frame.state.channel === 'human' && frame.state.attention > .5;
if (address && (!this.lastAddress || fresh && t!.onsets[11] > 0)) {
const frequency = CHANNELS[11].freq;
for (let i = 0; i < 3; i++) add(`address:${frame.timeMs}:${i}`, frequency * [1, 1.25, 1.5][i], .23, .032, 0, i * .14);
}
this.lastAddress = address;
if (window !== this.lastWindow) {
this.lastWindow = window;
if (frame.state.observed >= .92) {
const channel = CHANNELS.find(c => c.key === frame.state.channel)!;
if (channel.sustained && frame.behaviour !== 'doze') {
const peers = frame.state.channel === 'agent' ? Math.max(1, frame.pod.filter(p => p.present).length) : 1;
for (let i = 0; i < peers; i++) add(`sustain:${window}:${i}`, channel.freq * (1 + (i - (peers - 1) / 2) * .004),
.44, (.02 + frame.state.activity * .02) / Math.sqrt(peers), peers === 1 ? 0 : i / (peers - 1) - .5);
}
if (frame.behaviour === 'doze' && window % 5 === 0) {
add(`heart:${window}:0`, 49, .3, .035); add(`heart:${window}:1`, 49, .24, .023, 0, .33);
}
if (frame.needs === 'call' && window % 10 === 0) add(`call:${window}`, CHANNELS[11].freq * 1.5, .38, .03);
}
}
return out;
}
}
/** Sample-addressed noise: no global RNG, identical samples when chunked/seeking. */
function noise(seed: number, sample: number): number {
let x = (seed + Math.imul(sample, 0x6D2B79F5)) >>> 0;
x = Math.imul(x ^ x >>> 15, x | 1); x ^= x + Math.imul(x ^ x >>> 7, x | 61);
return ((x ^ x >>> 14) >>> 0) / 2147483648 - 1;
}
export function renderPetPCM(voices: readonly PetVoice[], startSample: number, length: number, sampleRate = 48_000): { left: Float32Array; right: Float32Array } {
if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 96000
|| !Number.isSafeInteger(startSample) || startSample < 0 || !Number.isInteger(length) || length < 0 || length > sampleRate * 120)
throw new Error('Invalid pet PCM range.');
const left = new Float32Array(length), right = new Float32Array(length);
for (const v of voices) {
if (![v.start, v.duration, v.frequency, v.gain, v.pan].every(Number.isFinite)
|| v.start < 0 || v.duration <= 0 || v.duration > 10 || v.frequency <= 0 || v.frequency > sampleRate / 2
|| v.gain < 0 || v.gain > 1 || Math.abs(v.pan) > 1 || !['tone', 'noise'].includes(v.kind)) throw new Error('Invalid pet voice.');
const first = Math.max(startSample, Math.ceil(v.start * sampleRate));
const last = Math.min(startSample + length, Math.ceil((v.start + v.duration) * sampleRate));
const pan = (v.pan + 1) * Math.PI / 4, seed = (0xC0FFEE ^ stableHash(v.id)) >>> 0;
for (let absolute = first; absolute < last; absolute++) {
const age = absolute / sampleRate - v.start;
const envelope = Math.min(1, age / .015, (v.duration - age) / .045);
const sample = v.kind === 'noise' ? noise(seed, absolute) * Math.exp(-age * 14)
: Math.sin(2 * Math.PI * v.frequency * age) * .88 + Math.sin(4 * Math.PI * v.frequency * age) * .12;
const value = sample * Math.max(0, envelope) * v.gain, at = absolute - startSample;
left[at] += value * Math.cos(pan); right[at] += value * Math.sin(pan);
}
}
// Limiting is a presentation operation and cannot perturb voice scheduling.
for (let i = 0; i < length; i++) { left[i] = clamp(left[i], -1, 1); right[i] = clamp(right[i], -1, 1); }
return { left, right };
}
+21
View File
@@ -0,0 +1,21 @@
import type { Category, WhaleEvent } from './model.js';
/** Authored event-v1 fixture, explicitly labelled demo by every driver. */
export function petDemoEvents(): WhaleEvent[] {
const out: WhaleEvent[] = [];
const span = (startTime: number, endTime: number, category: Category, name: string, agentId = 'parent', status: WhaleEvent['status'] = 'success') => {
out.push({ schemaVersion: 1, id: `demo-${out.length}`, traceId: 'pet-demo', startTime, endTime,
category, name, agentId, status, attributes: {} });
};
span(0, 6000, 'reasoning', 'Plan');
for (let i = 0; i < 5; i++) span(6400 + i * 800, 6900 + i * 800, 'filesystem', `Read source ${i}`);
span(11_000, 16_000, 'code', 'Check implementation');
for (const [i, agent] of ['parent', 'reviewer', 'tester'].entries()) span(17_000 + i * 400, 25_000, 'reasoning', 'Parallel work', agent);
for (let i = 0; i < 7; i++) span(26_000 + i * 700, 26_450 + i * 700, 'tool', 'Repeated tool');
for (let i = 0; i < 3; i++) span(31_000 + i * 400, 31_000 + i * 400, 'error', 'Reported failure', 'parent', 'error');
span(34_000, 63_000, 'human', 'Awaiting input', 'parent', 'pending');
span(63_000, 63_000, 'human', 'User response');
span(64_000, 69_000, 'memory', 'Recall');
span(70_000, 75_000, 'reasoning', 'Resolve');
return out;
}
+100
View File
@@ -0,0 +1,100 @@
import type { Category, WhaleEvent } from './model.js';
import { toolCategory } from './codewhale.js';
import { compilePetTelemetry, PET_BIN_MS, type PetBucket } from './pet-telemetry.js';
/** Read-only adapter for codewhale_protocol::EventMsg metadata. The foreground
* Engine is the event owner. This replaces no turn loop: it only translates
* lifecycle observations to event-v1 for the same pet bucketer used by imports.
* Text, inputs and results are neither accepted nor retained. */
export class PetEngineTelemetry {
private events: WhaleEvent[] = [];
private active = new Map<string, WhaleEvent>();
private waiting: WhaleEvent | undefined;
private sequence = 0;
private lastTime = 0;
private add(name: string, category: Category, at: number, agentId = 'parent', continuation = false): WhaleEvent {
if (this.events.length >= 8192) throw new Error('Pet Engine observation window is full.');
const e: WhaleEvent = { schemaVersion: 1, id: `engine:${this.sequence++}`, traceId: 'foreground',
startTime: at, endTime: at, name, category, agentId, status: 'running',
attributes: continuation ? { 'whalesong.continuation': true } : {} };
this.events.push(e); return e;
}
private pulse(key: string, at: number): void {
const e = this.active.get(key);
if (!e) return;
// A resumed stream does not assert coverage across its silent interval.
if (at - e.endTime > PET_BIN_MS * 2) {
this.active.set(key, this.add(e.name, e.category, at, e.agentId, true));
} else e.endTime = at;
}
observe(value: unknown, at: number): void {
if (!Number.isFinite(at) || at < this.lastTime || at > 86_400_000) throw new Error('Invalid Engine pet clock.');
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid Engine pet metadata.');
const e = value as Record<string, unknown>;
const allowed = ['event', 'index', 'channel', 'tool_call_id', 'tool_name', 'id', 'worker_status', 'failed'];
if (Object.keys(e).some(k => !allowed.includes(k)) || typeof e.event !== 'string'
|| Object.values(e).some(v => typeof v === 'string' && v.length > 4096)
|| e.channel !== undefined && !['text', 'reasoning'].includes(e.channel as string)
|| ['tool_call_id', 'tool_name', 'id', 'worker_status'].some(k => e[k] !== undefined && typeof e[k] !== 'string')
|| e.failed !== undefined && typeof e.failed !== 'boolean'
|| e.index !== undefined && (!Number.isSafeInteger(e.index) || (e.index as number) < 0))
throw new Error('Invalid Engine pet metadata fields.');
this.lastTime = at;
this.events = this.events.filter(span => span.endTime >= at - 12_800);
const id = (field: string) => { const s = e[field]; if (typeof s !== 'string' || !s) throw new Error(`Missing Engine ${field}.`); return s; };
const index = () => { if (!Number.isSafeInteger(e.index)) throw new Error('Missing Engine index.'); return String(e.index); };
const start = (key: string, name: string, category: Category, agentId?: string) => {
if (this.active.size >= 256 && !this.active.has(key)) throw new Error('Too many active Engine pet spans.');
this.active.set(key, this.add(name, category, at, agentId));
};
const finish = (key: string) => { this.pulse(key, at); this.active.delete(key); };
switch (e.event) {
case 'turn_started': this.active.clear(); this.waiting = undefined; break;
case 'message_started': start(`message:${index()}`, 'assistant_message', 'communication'); this.waiting = undefined; break;
case 'thinking_started': start(`thinking:${index()}`, 'thinking', 'reasoning'); this.waiting = undefined; break;
case 'response_delta': {
const reasoning = e.channel === 'reasoning';
const key = `${reasoning ? 'thinking' : 'message'}:${index()}`;
if (!this.active.has(key)) start(key, reasoning ? 'thinking' : 'assistant_message', reasoning ? 'reasoning' : 'communication');
else this.pulse(key, at);
this.waiting = undefined; break;
}
case 'message_complete': finish(`message:${index()}`); break;
case 'thinking_complete': finish(`thinking:${index()}`); break;
case 'tool_call_started': start(`tool:${id('tool_call_id')}`, id('tool_name'), toolCategory(id('tool_name'))); this.waiting = undefined; break;
case 'tool_call_heartbeat': for (const key of this.active.keys()) if (key.startsWith('tool:')) this.pulse(key, at); break;
case 'tool_call_complete': finish(`tool:${id('tool_call_id')}`); this.waiting = undefined; break;
case 'agent_spawned': start(`agent:${id('id')}`, 'agent', 'agent', id('id')); break;
case 'agent_progress': {
const key = `agent:${id('id')}`;
if (['completed', 'failed', 'cancelled', 'interrupted', 'budget_exhausted'].includes(e.worker_status as string)) { finish(key); break; }
if (!this.active.has(key)) start(key, 'agent', 'agent', id('id')); else this.pulse(key, at);
break;
}
case 'agent_complete': finish(`agent:${id('id')}`); break;
case 'approval_required': case 'user_input_required': this.waiting = this.add('human', 'human', at); this.waiting.status = 'pending'; break;
case 'turn_complete': this.active.clear(); this.waiting = undefined; break;
case 'error': break;
default: throw new Error('Unsupported Engine pet event.');
}
// Receipt time is the error onset; never rewrite the operation's old start.
if (e.event === 'error' || e.failed === true) this.add('error', 'error', at).status = 'error';
}
/** Waiting coverage comes from the existing typed shell's current request.
* It can extend a witnessed request, never invent one on a mid-turn attach. */
confirmWaiting(at: number, waiting: boolean): void {
if (!waiting) { this.waiting = undefined; return; }
if (this.waiting && at >= this.waiting.endTime) this.waiting.endTime = at;
}
bucket(sequence: number): PetBucket {
const end = (sequence + 1) * PET_BIN_MS;
const input = this.events.filter(e => e.startTime < end && e.endTime >= end - 12_400)
.map(e => ({ ...e, endTime: Math.min(e.endTime, end) }));
return compilePetTelemetry(input, end, sequence)[0];
}
}
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff. Show more