Tide-based Rust server that ports the Roda-era behavior and now uses LineDB-backed persistence.
For the full maintainer and operator reference, see COMPLETE_DOCUMENTATION.md. It consolidates setup, configuration, persistence, HTTP routes, chatlog moderation, Forth/RubyForth, the Second Life bridge, LSL operation, security, and troubleshooting.
GET /program/example returns a runnable decrementing-loop program.POST /program/run executes a JSON program against the shared partitioned variable store.Programs use named integer registers in the existing partitioned array. Supported
operations are set, increment, decrement, label, jump,
jump_if_nonzero, and halt. jump and jump_if_nonzero target a label name;
set accepts an integer value. Each request may set max_steps from 1 through
1,000,000 and returns the final variable snapshot. The per-request limit prevents
an accidental infinite loop from holding the HTTP handler indefinitely.
{
"max_steps": 100,
"program": [
{ "op": "set", "name": "counter", "value": 3 },
{ "op": "label", "name": "loop" },
{ "op": "decrement", "name": "counter" },
{ "op": "jump_if_nonzero", "name": "counter", "target": "loop" },
{ "op": "halt" }
]
}
GET /forth returns the machine-readable Forth API catalog.GET /forth/ui provides a browser console for evaluation, algebra, and bridge jobs.POST /forth/eval executes Forth/Ruby-like source and returns output, stack state,
partitioned variables, matrices, and allowlisted Second Life calls.POST /forth/bridge/enqueue queues source for an in-world bridge object.POST /forth/bridge/poll delivers one queued source program to that object.Forth variables, history, notecards, matrices, and pending Second Life bridge
jobs are persisted through partitioned_array_rust. The file routes remain a
separate sandboxed UTF-8 file store because they intentionally model file I/O.
To enable the bridge, set the same nonempty token in the server environment and
the BRIDGE_TOKEN constant in src/Relote.Stack.Service.lsl:
export MSSL_FORTH_BRIDGE_TOKEN='replace-with-a-long-random-token'
After deploying the script, the owner can use /1111 bridge-on. The browser
console queues Forth source with that token; the object polls, evaluates it
through the normal runtime, and executes only documented sl.* calls. Second
Life permissions, parcel restrictions, and LSL function availability still
apply.
Bridge jobs may include "language":"forth" or "language":"ruby"; the
browser console provides a separate queue action for each. The LSL poller sends
Ruby jobs through POST /ruby/eval and Forth jobs through POST /forth/eval.
POST /ruby/eval compiles a Ruby-shaped language into the same Forth runtime,
so variables, matrices, notecards, bridge jobs, and allowed sl.* actions use
the partitioned store and the existing execution limits. The browser console at
GET /forth/ui includes a Run RubyForth command, and the LSL script supports
/1111 ruby ..., /1111 ruby-say <channel> ..., /1111 ruby-run <notecard>,
and /1111 ruby-run-say <channel> <notecard>.
Supported syntax includes integer and string expressions, =, +=, -=,
*=, /=, puts, p, if, unless, elsif, else, while, until,
n.times do ... end, and end.
It deliberately does not expose Ruby eval, require, gems, classes, methods,
or arbitrary subprocess/file access.
count = 0
while count < 3
puts count
count += 1
end
Matrices are stored in the same partitioned variable store as Forth state.
m.scale <source> <target> consumes an integer scalar from the stack, while
m.solve <left> <right> <target> solves $A X = B$ only when $A$ is invertible
and every result is an exact integer. The runtime rejects fractional or
overflowing results.
2 2 matrix A
2 0 0 mset A 1 1 1 mset A
2 1 matrix B
4 0 0 mset B 6 1 0 mset B
m.solve A B X
m.show X
GET /chatlog renders the full moderation dashboard: parses the raw Second Life
chat log, scores each message against hostile/positive/drug/slang word
dictionaries, and reports integrity, timing, and capture-source statistics.GET /chatlog?format=summary returns the same statistics as JSON instead of HTML.GET /chatlog?format=recent renders only the most recent messages.GET /chatlog/summary, GET /chatlog/recent, and GET /chatlog/admin are
convenience redirects to the format variants above.GET /chatlog/markov and GET /chatlog/markov.json generate Markov-chain
sentence samples from the logged chat; GET /chatlog/markov/transitions.json
exposes the underlying state-transition table.GET /avatarencounter opens the timer-driven nearby-avatar frequency HTML
dashboard; GET /avatarencounter?format=json exposes the underlying table,
including UUID/name pairs, probabilities, encounter rates, count deltas, and
per-source scan statistics. The /chatlog dashboard links out to this view;
LSL scanners upload snapshots with POST /avatarfrequency.The built-in hostile, positive, drug, and slang dictionaries can be
extended at runtime, without a rebuild, from /chatlog:
GET /chatlog/words is a browser console for adding, scoring, and removing words.GET /chatlog/words/list returns the current custom words as JSON.POST /chatlog/words/add with {"category":"hostile","word":"example","score":2}
upserts a word (score defaults to 2, clamped to -10..=10).POST /chatlog/words/delete with {"category":"hostile","word":"example"} removes it.Words are limited to 1-32 ASCII letters/numbers, persisted through
partitioned_array_rust alongside the other memory stores, and merged into the
matching dictionary every time /chatlog scores a message.
This server uses the partitioned_array_rust crate with LineDb as top-level manager.
Default LineDB root:
/root/midscore_io/logs/tiade_roda_compat/line_dbFallback LineDB root:
/tmp/tiade_line_dbState table mapping uses file stem naming:
blog_storegallery_storeThe Ollama 2.0 relay uses LineDb for team conversation history, team prompts, and isolated game-player histories:
/root/midscore_io/logs/ollama_teams/line_dbGET /admin/login?password=...POST /admin/add with form field db_nameGET /admin/remove/:db_namePOST /admin/delete with form field db_nameGET /admin/reloadGET /admin/listPOST /admin/rehash/:db_nameIntegration note:
src/main.rs mounts roda_tide_rewrite::mount_roda_compat_routes(&mut app) last, so overlapping paths resolve to the Roda-compat handlers./ redirects to /gallery./tiade/*.| Method | Path | Source | Auth | Notes |
|---|---|---|---|---|
| GET | / |
src/roda_tide_rewrite.rs |
public | Redirects to /gallery |
| GET | /assets/*path |
src/roda_tide_rewrite.rs |
public | Serves project assets |
| GET | /public/*path |
src/roda_tide_rewrite.rs |
public | Serves public files |
| GET | /card |
src/roda_tide_rewrite.rs |
public | JPEG banner |
| GET | /moon |
src/roda_tide_rewrite.rs |
public | Text response |
| GET | /sun |
src/roda_tide_rewrite.rs |
public | Text response |
| POST | /img/resize |
src/roda_tide_rewrite.rs |
public | Image bytes resize endpoint |
| Method | Path | Source | Auth | Notes |
|---|---|---|---|---|
| GET | /admin/login |
src/roda_tide_rewrite.rs |
public | ?password= sets admin cookie on success |
| GET | /admin |
src/roda_tide_rewrite.rs |
admin cookie | Admin HTML dashboard |
| POST | /admin/add |
src/roda_tide_rewrite.rs |
admin cookie | Form field db_name |
| GET | /admin/remove/:db_name |
src/roda_tide_rewrite.rs |
admin cookie | Redirects back to admin |
| POST | /admin/delete |
src/roda_tide_rewrite.rs |
admin cookie | Form field db_name |
| GET | /admin/reload |
src/roda_tide_rewrite.rs |
admin cookie | Reload LineDB state |
| GET | /admin/list |
src/roda_tide_rewrite.rs |
admin cookie | JSON database listing |
| POST | /admin/rehash/:db_name |
src/roda_tide_rewrite.rs |
admin cookie | JSON rehash result |
| Method | Path | Source | Auth | Notes |
|---|---|---|---|---|
| GET | /blog |
src/roda_tide_rewrite.rs |
public | Redirects to /blog/login |
| GET | /blog/login |
src/roda_tide_rewrite.rs |
public | HTML login form |
| POST | /blog/login |
src/roda_tide_rewrite.rs |
public | Requires blog_user_name, blog_password_name, super_password |
| GET | /blog/logout |
src/roda_tide_rewrite.rs |
session | Clears blog session cookie |
| GET | /blog/signup |
src/roda_tide_rewrite.rs |
public | HTML signup form |
| POST | /blog/signup |
src/roda_tide_rewrite.rs |
public | Creates user |
| GET | /blog/render |
src/roda_tide_rewrite.rs |
public | Render by query (user, id) |
| GET | /blog/:user |
src/roda_tide_rewrite.rs |
public | Redirects to /blog/:user/view |
| GET, POST | /blog/:user/pin |
src/roda_tide_rewrite.rs |
owner session | View or set pinned post |
| GET | /blog/:user/tag/:tag |
src/roda_tide_rewrite.rs |
public/private by profile | Tag filtered listing |
| GET, POST | /blog/:user/edit/:id |
src/roda_tide_rewrite.rs |
owner session | Edit post page and save |
| GET | /blog/:user/delete |
src/roda_tide_rewrite.rs |
public/private by profile | Delete/lock listing page |
| GET | /blog/:user/delete/:id |
src/roda_tide_rewrite.rs |
owner session | Toggle lock then redirect |
| GET | /blog/:user/list |
src/roda_tide_rewrite.rs |
public/private by profile | Post list |
| GET | /blog/:user/private_toggle |
src/roda_tide_rewrite.rs |
owner session | Toggle private view |
| GET | /blog/:user/view |
src/roda_tide_rewrite.rs |
public/private by profile | Main blog index |
| GET | /blog/:user/view/:id |
src/roda_tide_rewrite.rs |
public/private by profile | Post view (?format=json supported) |
| GET | /blog/:user/view/:month/:day/:year/:time |
src/roda_tide_rewrite.rs |
public/private by profile | Date-based post lookup |
| Method | Path | Source | Auth | Notes |
|---|---|---|---|---|
| GET | /gallery |
src/roda_tide_rewrite.rs |
public | Gallery home/user list |
| GET, POST | /gallery/upload/url |
src/roda_tide_rewrite.rs |
public | URL upload page and submit |
| GET, POST | /gallery/upload |
src/roda_tide_rewrite.rs |
public | Raw body or multipart file/url upload |
| GET | /gallery/view/:user/latest |
src/roda_tide_rewrite.rs |
public | Redirects to latest page index |
| GET | /gallery/reset_session/:user |
src/roda_tide_rewrite.rs |
public | Clears gallery preference cookies |
| GET | /gallery/view/:user |
src/roda_tide_rewrite.rs |
public/private by profile | Gallery index with cookie-backed prefs |
| GET | /gallery/view/:user/id/:id |
src/roda_tide_rewrite.rs |
public/private by profile | Single gallery item |
| GET | /gallery/view/:user/id/:id/attachments |
src/roda_tide_rewrite.rs |
public/private by profile | Attachment list |
| GET | /gallery/view/:user/id/:id/attachments/delete/:attachment_id |
src/roda_tide_rewrite.rs |
public/private by profile | Deletes attachment entry |
| GET, POST | /gallery/view/:user/id/:id/attachments/upload |
src/roda_tide_rewrite.rs |
public/private by profile | Form + multipart/url/value upload |
| GET | /gallery/delete/:user/id/:id |
src/roda_tide_rewrite.rs |
public/private by profile | Deletes post and files |
| GET | /gallery/view/:user/tags/search |
src/roda_tide_rewrite.rs |
public/private by profile | Include/exclude search (search_tags, --tag) |
| GET | /gallery/view/:user/tags |
src/roda_tide_rewrite.rs |
public/private by profile | Tag list page |
| GET, POST | /gallery/edit/:user/id/:id |
src/roda_tide_rewrite.rs |
public/private by profile | Edit gallery metadata |
| Method | Path | Source | Auth | Notes |
|---|---|---|---|---|
| GET | /gallery/uwu/view/:user |
src/roda_tide_rewrite.rs |
public/private by profile | List collections |
| GET | /gallery/uwu/view/:user/id/:id |
src/roda_tide_rewrite.rs |
public/private by profile | View collection |
| GET | /gallery/uwu/delete/id/:id |
src/roda_tide_rewrite.rs |
session-dependent | Delete collection |
| GET, POST | /gallery/uwu/new |
src/roda_tide_rewrite.rs |
session-dependent | Create collection |
| POST | /gallery/uwu/edit/id/:id |
src/roda_tide_rewrite.rs |
session-dependent | Replace collection items |
| GET | /gallery/uwu/delete_image/uwu_id/:uwu_id/gallery_id/:gallery_id |
src/roda_tide_rewrite.rs |
session-dependent | Remove image from collection |
| POST | /gallery/uwu/add_image/uwu_id/:uwu_id |
src/roda_tide_rewrite.rs |
session-dependent | Add image to collection |
| GET | /gallery/owo/add |
src/roda_tide_rewrite.rs |
public | Increment counter |
| GET | /gallery/owo/rem |
src/roda_tide_rewrite.rs |
public | Decrement counter |
| GET | /gallery/owo/sub |
src/roda_tide_rewrite.rs |
public | Read counter |
| Method | Path | Source | Notes |
|---|---|---|---|
| POST | /praexy-saerver |
src/main.rs |
Form relay utility |
| GET | /bridge/*rest |
src/main.rs |
iframe bridge page |
| GET | /time |
src/main.rs |
Embedded Ruby VM output |
| GET | /ae |
src/main.rs |
Embedded Ruby VM output |
| GET | /weather |
src/main.rs |
Embedded Ruby VM output |
| GET | /rneutrialg |
src/main.rs |
Text file read |
| GET | /rneutri |
src/main.rs |
Text file write |
| GET | /tiade/moon |
src/main.rs |
Embedded Ruby VM moon phase |
| GET | /tiade/sun |
src/main.rs |
Embedded Ruby VM sun phase |
| POST | /sl/ruby/eval |
src/sl_ruby.rs |
Token-protected Ruby evaluation for Second Life |
| POST | /sl/ruby/reset |
src/sl_ruby.rs |
Clears a Ruby evaluation session |
| GET | /tiade-maepers/*rest |
src/main.rs |
iframe bridge page |
| GET | /parse_plink |
src/main.rs |
URL parser redirect |
| POST | /tiade/img/resize |
src/main.rs |
Namespaced legacy placeholder resize |
| GET | / |
src/main.rs |
Redirects to /gallery |
| POST | /echo |
src/main.rs |
Echo body |
| POST | /restart-servers |
src/main.rs |
Process HUP command |
| POST | /file/add |
src/main.rs |
Writes /tmp/new_file.txt |
| DELETE | /file/delete |
src/main.rs |
Deletes /tmp/new_file.txt |
| Method | Path | Source | Notes |
|---|---|---|---|
| GET | /chatlog |
src/main.rs |
Dashboard HTML; ?format=summary for JSON stats, ?format=recent for latest-only |
| GET | /chatlog/summary |
src/main.rs |
Redirects to /chatlog?format=summary |
| GET | /chatlog/recent |
src/main.rs |
Redirects to /chatlog?format=recent |
| GET | /chatlog/admin |
src/main.rs |
Redirects to /chatlog?format=summary |
| GET | /chatlog/markov |
src/main.rs |
Markov sentence generator HTML console |
| GET | /chatlog/markov.json |
src/main.rs |
Markov sentence generator JSON |
| GET | /chatlog/markov/transitions.json |
src/main.rs |
Markov state-transition table JSON |
| GET | /chatlog/words |
src/main.rs |
Custom alert word console (HTML) |
| GET | /chatlog/words/list |
src/main.rs |
Lists custom alert words (JSON) |
| POST | /chatlog/words/add |
src/main.rs |
Upserts {category, word, score} |
| POST | /chatlog/words/delete |
src/main.rs |
Removes {category, word} |
These are mounted by mount_ollama_routes(&mut app, OllamaRelayConfig::default()):
GET /ollama (general route catalog)POST /ollama with Ollama-compatible {"model":"...","messages":[...],"stream":false}POST /chat/:team with {"message":"..."}POST /game/:team/:player with {"message":"...","game_prompt":"..."}POST /game/:team/:player/turn with {"action":"...","game_prompt":"...","state":{}}GET /game/:team/:player/statePOST /game/:team/:player/resetGET /history/:teamGET /ollama/healthPOST /sl/ask/:team with {"message":"...","speaker":"..."} (or plain text) → 202 {"job":"<id>"}GET /sl/job/:id → {"status":"pending"}, {"status":"done","reply":"..."} or {"status":"error","error":"..."}GET /teams/:team/promptPOST /teams/:team/prompt with {"prompt":"..."}Set OLLAMA_TEAM_PROMPT_TOKEN before starting the server to enable prompt management. A saved prompt is applied as a system message only to the matching team. An empty prompt clears the saved team prompt.
Read the prompt with GET /teams/:team/prompt and the same bearer token.
export OLLAMA_TEAM_PROMPT_TOKEN='set-a-long-random-token-here'
curl -X POST https://your-host/teams/research/prompt \
-H "Authorization: Bearer $OLLAMA_TEAM_PROMPT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"prompt":"Answer as a concise research assistant."}'
The relay sends permissive CORS headers and accepts OPTIONS preflight requests for chat, game, history, health, and team-prompt routes.
Call GET /ollama to discover the generic Ollama route catalog, including methods and authorization requirements.
For conversational game I/O, POST /game/:team/:player isolates history by player. For model-directed play, POST /game/:team/:player/turn persists a JSON state object per player and requires Ollama to return a JSON directive containing narrative, the complete next state, choices, and game_over. The saved team prompt is applied first; the optional game_prompt is applied only to that request, letting the game provide current rules or scene context.
const response = await fetch('https://your-host/chat/research', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Summarize today\'s findings.' }),
});
const { response: reply } = await response.json();
wasm_client compiles to a browser ES module and is served by the existing static-assets route. Run ./start-with-wasm.sh to install the Rust WebAssembly target and wasm-bindgen-cli on first use, build both artifacts, and start the release server.
import init, { chat, game_turn } from '/assets/wasm/ollama_game_client.js';
await init();
const reply = await game_turn(
window.location.origin,
'arcade',
'player-42',
'I open the north door.',
'Inventory: lantern, silver key.'
);
console.log(reply);
The generated module exports routes, health, chat, game_turn, and history. The relay route list is also available in OLLAMA_ROUTES.txt.
Ruby runs in-process: src/ruby_vm.rs initialises a Magnus-embedded VM on a dedicated ruby-vm thread at startup, and every Ruby evaluation is queued to that thread. The external rustby-vm file watcher is no longer needed.
src/main.rs (Calendar, AECalendar, MoonPhaseDetails2, SolarDance2, ForecastByLongitude, formatted_pst_time). Definitions persist across evaluations./time, /ae, /weather, /tiade/moon and /tiade/sun call ruby_vm::text_response, which returns the result's to_s as text/plain. A Ruby exception returns 500 with Error: <message>; a run longer than 120 s returns 504.rustby <ruby code> on the server's stdin to evaluate code and print the result (rustby alone runs a demo snippet)..rb file.INT/TERM/HUP/QUIT/USR1/USR2/ALRM handlers are reset to the system defaults, so ctrlc, stop-server.sh and the killall -HUP restart behave as before..ruby-version (4.0.7). Rebuild after changing Ruby versions.Evaluation is serial: a slow script delays later Ruby requests. A timed-out script keeps running in the VM until it finishes.
Tiade.Ollama.Ruby.lsl is an owner-only LSL client for both features (/7 ask ..., /7 ruby ..., /7 help). The in-world help command prints extended command, setup, Ruby safety, and notecard-runner guidance.
It can also run notecards from the object's inventory:
/7 notecard <name> runs one /7 command per line, in order (ask ..., ruby ..., team ..., ruby-reset, ...). # and // lines are comments, a leading /7 is optional, ask lines wait for their Ollama reply, wait <seconds> pauses, and lines between ruby-begin and ruby-end are sent as one multi-line Ruby program. Requests throttled by the region are retried instead of skipped./7 ruby-notecard <name> sends the whole notecard as one Ruby program (up to 12,000 characters)./7 notecard-stop stops; editing or removing the notecard also stops it, and a line that cannot be read within 30 s (for example an empty, never-saved notecard) ends the run with a message.Example notecards: sl_notecards/Demo.txt (commands) and sl_notecards/RubyScript.txt (one Ruby program). /7 health checks GET /ollama/health.
POST /sl/ask/:team returns a job id at once and the script polls GET /sl/job/:id. Replies share the team history used by /chat/:team, are plain text, and are capped at OLLAMA_SL_REPLY_CHARS (1000) characters and OLLAMA_SL_NUM_PREDICT (200) tokens with a 2048-token context (OLLAMA_SL_NUM_CTX). The relay keeps the model loaded (OLLAMA_KEEP_ALIVE, default 60m), preloads it at startup (OLLAMA_PRELOAD=false disables this) and caches the installed-model lookup for 60 s.POST /sl/ruby/eval (src/sl_ruby.rs) evaluates code in the embedded VM. Send {"code":"...","session":"..."} JSON or plain-text code, with the token in X-Ruby-Token or Authorization: Bearer. The session defaults to the calling object's key, and local variables persist per session until POST /sl/ruby/reset. Captured $stdout plus => value.inspect come back as text/plain (200). Ruby exceptions, exit and the per-call timeout (TIADE_RUBY_EVAL_SECONDS, default 10) return 422.503 until TIADE_RUBY_EVAL_TOKEN is set. TIADE_RUBY_EVAL_SL_OWNERS (comma-separated avatar keys) also checks X-SecondLife-Owner-Key; that header can be forged outside Second Life, so the token stays mandatory.start.sh loads server.env (git-ignored, mode 600) when present; put the token and any of the variables above there.ruby_client/ollama_game_client.rb is dependency-free Ruby using Net::HTTP and JSON, so it can run in a raylib-ruby game loop or in Ruby evaluated through Magnus. ruby_client/raylib_game_loop_example.rb shows a GameDirector that retains rendering, input, physics, and validation in Ruby while consuming Ollama's structured turn directives as data.
At server startup, src/main.rs loads this same client into the embedded Magnus VM. Ruby evaluated through that VM can instantiate OllamaGameClient::Client directly; no Ruby load-path setup or duplicated HTTP bridge is required.
require_relative "ruby_client/ollama_game_client"
client = OllamaGameClient::Client.new(
server_url: "https://your-host",
team: "arcade",
player: "player-42"
)
turn = client.turn(action: "open the north door", game_prompt: "Fantasy dungeon. Keep choices concise.")
puts turn.fetch("directive").fetch("narrative")
const response = await fetch('https://your-host/game/arcade/player-42', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: 'I open the north door.',
game_prompt: 'Player inventory: lantern, silver key. Return concise JSON-ready prose.',
}),
});
const { response: reply } = await response.json();
cargo run
For the TLS production server, build and start the release binary:
cargo build --release
./start.sh
To build and serve the browser WebAssembly client with the server instead:
./start-with-wasm.sh
Use ./stop-server.sh to send the managed process SIGTERM.
cargo check
src/main.rs currently contains pre-existing warnings unrelated to the LineDB migration layer in src/roda_tide_rewrite.rs.