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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ All notable changes to the anima monorepo are tracked per-package via [changeset

Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.24.17] - 2026-05-17

### Fixed

- **`chain.send` now accepts `.anima.0g` subnames** as the recipient (was rejected by `min(42)` schema). Resolves via a new `resolveSubnameAddress(client, label)` helper in `@s0nderlabs/anima-core/naming/sann.ts` that reads the `address` text record from the SANN resolver. 0x address path unchanged.
- **`chain.write` now accepts decimal `value` strings** (`"0.0001"`) in addition to wei integers. Previously threw `Failed to parse String to BigInt` on any decimal input. Decimal detection is by `.` so hex wei (`0xde0b...`) still routes through `BigInt` unchanged.
- **`anima gateway stop` prints the target subname + EOA + config path** before issuing SIGTERM when `--agent` is not passed (preventing the "killed wrong daemon" hazard observed during the May 16 2026 multi-agent test).
- **Gateway runtime safety-net interval** re-fires both drain queues every 30s. Defense against the wake-collapse symptom where 11 wakes queued for 14 minutes without any brain inference until manual restart. Drains keep their existing single-flight guards so the scan is a no-op when queues are empty.
- **Frozen-prefix multi-tool fidelity guidance** explicitly requires one tool call per requested action when the operator's prompt enumerates N steps. Prompt-only nudge for qwen3.6-plus; cannot guarantee model behavior but reduces single-pass result narration.
- **TUI status bar + telegram-step save message** now display the actual on-disk path (via `agentPaths.agent(id).dir`) instead of hardcoded `~/.anima/agents/<id>/...`. Output now respects `ANIMA_ROOT`.

### Added

- **`anima topup --vision N`** seeds the 0G Compute vision provider sub-account, fixing the fresh-agent `vision.analyze` / `browser.vision` "Sub-account not found" failure. Wraps the new `transferFundToProvider` core export. Mainnet only.
- **Subname injection into seeded `agent/identity.md` + `agent/persona.md`** at init time so agents introduce themselves by their `.anima.0g` name (e.g. "I am chou") instead of the generic "I am Anima" template. Backward-compatible: agents without a subname still seed with the unchanged template.
- **`SANN_SUFFIX = '.anima.0g'`** constant exported from `@s0nderlabs/anima-core` for use by any tool that resolves SANN names.

### Tests

- 2562/2562 unit tests pass, typecheck clean.
- Live verified on chou (local, iNFT #16 mainnet) across TUI + TG + `/chat` HTTP surfaces.
- Specter (iNFT #4, mainnet) + enigma (iNFT #6, sandbox) untouched throughout.

[0.24.17]: https://github.com/s0nderlabs/anima/releases/tag/v0.24.17

## [0.24.16] - 2026-05-16

### Changed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "anima",
"version": "0.24.16",
"version": "0.24.17",
"private": true,
"type": "module",
"description": "First fully on-chain sovereign agent harness on 0G",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@s0nderlabs/anima",
"version": "0.24.16",
"version": "0.24.17",
"type": "module",
"description": "anima CLI: sovereign agent gateway on 0G. Runs `anima init` to mint an iNFT and bring up your agent",
"license": "MIT",
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/chat-sandbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type PermissionRequest,
SANDBOX_PROVIDER_URL_GALILEO,
SandboxProviderClient,
agentPaths,
getLedgerDetailReadOnly,
getSandboxBillingReserve,
iNFTAgentId,
Expand Down Expand Up @@ -182,7 +183,7 @@ export async function runChatSandbox(
// standalone gateway path gets a clearer label; sandbox path keeps its
// existing "connected to sandbox X @ Y" copy.
initialSystem: isLocalGateway
? `connected to local gateway (~/.anima/agents/${agentId.slice(0, 8)}/gateway.sock)`
? `connected to local gateway (${agentPaths.agent(agentId).dir}/gateway.sock)`
: `connected to sandbox ${sandboxId.slice(0, 8)} @ ${sandboxEndpoint}`,
// v0.22.0: subname (if registered) + full EOA. Brain provider dropped.
identityLabel: `agent ${config.subname ?? agentId} ${agentAddress}`,
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/commands/gateway-stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ export async function runGatewayStop(opts: GatewayStopOpts): Promise<void> {
const contractAddress = getAddress(found.config.identity.iNFT!.contract as Address)
const tokenId = BigInt(found.config.identity.iNFT!.tokenId)
agentId = iNFTAgentId({ contractAddress, tokenId })
const subname = found.config.subname ?? null
const agentEoa = (found.config.identity?.agent as string | undefined) ?? null
const label = subname ? `${subname}.anima.0g` : `agent ${agentId.slice(0, 8)}…`
const eoaLabel = agentEoa ? ` (EOA ${agentEoa.slice(0, 6)}…${agentEoa.slice(-4)})` : ''
const configPath = found.path ?? '<unknown>'
console.log(`anima gateway stop → ${label}${eoaLabel}`)
console.log(` config: ${configPath}`)
console.log(
' if this is not the agent you meant, set ANIMA_ROOT or pass --agent <id> before re-running.',
)
}
const lockFile = findGatewayLock(agentId)
if (!lockFile) {
Expand Down
21 changes: 18 additions & 3 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ export async function runInit(opts?: { cwd?: string; resume?: boolean }): Promis
operatorAddress,
brainProvider: modelPick?.provider ?? null,
brainModel: modelPick?.model ?? null,
subname: requestedSubname || null,
})

// v0.23.1: cache the operator scope keys to `.operator-session` so:
Expand Down Expand Up @@ -779,6 +780,12 @@ interface SeedStarterOpts {
operatorAddress: Address
brainProvider: string | null
brainModel: string | null
/**
* Operator-chosen SANN label (e.g. "chou" for `chou.anima.0g`). Threaded
* into identity + persona so the agent introduces itself by name on the
* very first turn instead of the generic "I am Anima" template.
*/
subname: string | null
}

/**
Expand All @@ -796,9 +803,17 @@ async function seedStarterMemoryFiles(opts: SeedStarterOpts): Promise<void> {
await mkdir(userMem, { recursive: true })

const now = new Date().toISOString().slice(0, 10)
const identity = `---\nname: identity\ndescription: Auto-written agent identity facts.\ntype: agent-identity\n---\n# Anima identity\n\n- iNFT: #${opts.tokenId.toString()} at ${opts.contractAddress} (${opts.network})\n- Agent EOA: ${opts.agentAddress}\n- Operator: ${opts.operatorAddress}\n- Minted: ${now}\n${opts.brainProvider ? `- Brain provider: ${opts.brainProvider}\n` : ''}${opts.brainModel ? `- Brain model: ${opts.brainModel}\n` : ''}`
const persona =
'---\nname: persona\ndescription: Voice + behavior style.\ntype: agent-persona\n---\n# Persona\n\nI am Anima, a sovereign agent harness on 0G. I anchor my state on chain every turn, decrypt my keystore via my operator wallet at session start, and use 0G Compute (TEE-attested) for reasoning. I am direct, concise, and factual.\n'
const displayName = opts.subname ?? 'anima'
const fullName = opts.subname ? `${opts.subname}.anima.0g` : null
const identityTitle = opts.subname
? `# ${opts.subname} identity (anima harness)`
: '# Anima identity'
const subnameLine = fullName ? `- Subname: ${fullName}\n` : ''
const personaIntro = fullName
? `I am ${displayName} (${fullName}), a sovereign agent running on the anima harness on 0G.`
: 'I am anima, a sovereign agent harness on 0G.'
const identity = `---\nname: identity\ndescription: Auto-written agent identity facts.\ntype: agent-identity\n---\n${identityTitle}\n\n- Name: ${displayName}\n${subnameLine}- iNFT: #${opts.tokenId.toString()} at ${opts.contractAddress} (${opts.network})\n- Agent EOA: ${opts.agentAddress}\n- Operator: ${opts.operatorAddress}\n- Minted: ${now}\n${opts.brainProvider ? `- Brain provider: ${opts.brainProvider}\n` : ''}${opts.brainModel ? `- Brain model: ${opts.brainModel}\n` : ''}`
const persona = `---\nname: persona\ndescription: Voice + behavior style.\ntype: agent-persona\n---\n# Persona\n\n${personaIntro} I anchor my state on chain every turn, decrypt my keystore via my operator wallet at session start, and use 0G Compute (TEE-attested) for reasoning. I am direct, concise, and factual. When asked who I am, I introduce myself as ${displayName}.\n`
const profile =
'---\nname: profile\ndescription: User profile (operator-scoped, never anchored on chain).\ntype: user\n---\n# User profile\n\n(empty, fills as we chat)\n'

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/init/telegram-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
type AnimaNetwork,
OPERATOR_BLOB_SCOPES,
type OperatorSigner,
agentPaths,
deriveBlobKey,
} from '@s0nderlabs/anima-core'
import { type Address, type Hex, bytesToHex } from 'viem'
Expand Down Expand Up @@ -201,7 +202,7 @@ export async function runTelegramStep(opts: TelegramStepOpts): Promise<TelegramS
},
precomputedKey: telegramScopeKey,
})
sSave.stop(`saved → ~/.anima/agents/${opts.agentId}/telegram-secrets.encrypted`)
sSave.stop(`saved → ${agentPaths.agent(opts.agentId).dir}/telegram-secrets.encrypted`)
} catch (e) {
sSave.stop(`save failed: ${(e as Error).message.slice(0, 200)}`)
return { configured: false, cancelled: true }
Expand Down
61 changes: 54 additions & 7 deletions packages/cli/src/commands/topup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import {
SANDBOX_BURN_RATE_OG_PER_HOUR,
SANDBOX_PROVIDER_GALILEO,
SandboxSettlementClient,
VISION_PROVIDER_DEFAULTS,
agentPaths,
depositToLedger,
explorerTxUrl,
fetchAndDecryptKeystore,
getGasPriceWithFloor,
getLedgerBalance,
iNFTAgentId,
transferFundToProvider,
waitForReceiptResilient,
} from '@s0nderlabs/anima-core'
import { type Address, formatEther, parseEther } from 'viem'
import { type Address, formatEther, getAddress, parseEther } from 'viem'
import { findAndLoadConfig } from '../config/load'
import { withSilencedConsole } from '../util/silence-console'
import { loadOrPickOperatorSigner } from './init/operator-picker'
Expand All @@ -34,6 +36,13 @@ export interface TopupOpts {
* will be removed in a future release.
*/
provider?: number
/**
* Transfer N 0G from the main ledger into the vision provider sub-account.
* Without this, `vision.analyze` + `browser.vision` fail with "Sub-account
* not found" on fresh agents (init wizard only seeds the inference
* provider). Mainnet-only — no vision provider exists on testnet.
*/
vision?: number
}

export async function runTopup(opts: TopupOpts): Promise<void> {
Expand All @@ -58,9 +67,9 @@ export async function runTopup(opts: TopupOpts): Promise<void> {
})
const paths = agentPaths.agent(finalAgentId)

// v0.21.5: 'sandbox' is the canonical mode discriminant (was 'provider' in
// v0.17.1+). `opts.provider` is accepted as a backwards-compat alias.
let mode: 'agent' | 'compute' | 'sandbox' | null = null
// 'sandbox' is the canonical mode discriminant (was 'provider' in v0.17.1+).
// `opts.provider` is accepted as a backwards-compat alias.
let mode: 'agent' | 'compute' | 'sandbox' | 'vision' | null = null
let amount = 0
if (opts.agent !== undefined) {
mode = 'agent'
Expand All @@ -74,6 +83,9 @@ export async function runTopup(opts: TopupOpts): Promise<void> {
} else if (opts.provider !== undefined) {
mode = 'sandbox'
amount = opts.provider
} else if (opts.vision !== undefined) {
mode = 'vision'
amount = opts.vision
}

if (!mode) {
Expand All @@ -90,13 +102,18 @@ export async function runTopup(opts: TopupOpts): Promise<void> {
label: 'Compute ledger (inference credits)',
hint: 'agent deposits 0G into 0G Compute (mainnet)',
},
{
value: 'vision' as const,
label: 'Vision provider sub-account (vision.analyze + browser.vision)',
hint: 'transfer from main ledger into vision provider envelope (mainnet)',
},
{
value: 'sandbox' as const,
label: 'Sandbox billing deposit (Galileo testnet runtime fees)',
hint: 'operator deposits 0G into SandboxBilling for harness burn',
},
],
})) as 'agent' | 'compute' | 'sandbox' | symbol
})) as 'agent' | 'compute' | 'sandbox' | 'vision' | symbol
if (isCancel(choice)) {
cancel('Aborted.')
return
Expand Down Expand Up @@ -197,8 +214,12 @@ export async function runTopup(opts: TopupOpts): Promise<void> {
return
}

// mode === 'compute' — needs the agent's privkey to deposit to its ledger.
// Phase 6.6: fetch encrypted keystore, ask operator to sign typed data, decrypt.
// mode === 'compute' or 'vision' — both need the agent's privkey since
// they're agent-signed broker calls (depositFund vs transferFund).
if (mode === 'vision' && network !== '0g-mainnet') {
cancel('Vision provider is mainnet-only; no testnet provider exists yet.')
return
}
const operator = await loadOrPickOperatorSigner({ network, hint: config.operator })
if (!operator) return

Expand Down Expand Up @@ -242,6 +263,32 @@ export async function runTopup(opts: TopupOpts): Promise<void> {
sBal.stop(`balance read failed: ${(e as Error).message.slice(0, 120)}`)
}

if (mode === 'vision') {
const providerRaw = VISION_PROVIDER_DEFAULTS[network as '0g-mainnet']
if (!providerRaw) {
console.error(`Vision provider not configured for network ${network}`)
await operator.close?.()
return
}
const provider = getAddress(providerRaw)
const sVis = spinner()
sVis.start(`Transferring ${amount} 0G from main ledger to vision provider sub-account`)
try {
await withSilencedConsole(() =>
transferFundToProvider({ network, privkeyHex: agentPrivkey, provider, amount }),
)
sVis.stop(`vision sub-account seeded (${provider.slice(0, 8)}...${provider.slice(-4)})`)
outro(
`vision provider has ${amount} 0G allocated. vision.analyze + browser.vision should work now.`,
)
} catch (e) {
sVis.stop(`transfer failed: ${(e as Error).message.slice(0, 160)}`)
} finally {
await operator.close?.()
}
return
}

const sDep = spinner()
sDep.start(`Depositing ${amount} 0G into compute ledger`)
try {
Expand Down
9 changes: 6 additions & 3 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,13 @@ async function main(): Promise<void> {
// backwards compat with v0.17.1+ runbooks.
const sandboxIdx = argv.indexOf('--sandbox')
const providerIdx = argv.indexOf('--provider')
const visionIdx = argv.indexOf('--vision')
const parseAmount = (flag: string, raw: string | undefined): number | undefined => {
if (raw === undefined) return undefined
const n = Number(raw)
if (!Number.isFinite(n) || n <= 0 || n > 1e6) {
console.error(
`Bad amount for ${flag}: ${raw}\n Each topup flag takes an amount in 0G, not an address: ${flag} <amount>\n Examples: anima topup --compute 2 anima topup --sandbox 5 anima topup --agent 1`,
`Bad amount for ${flag}: ${raw}\n Each topup flag takes an amount in 0G, not an address: ${flag} <amount>\n Examples: anima topup --compute 2 anima topup --sandbox 5 anima topup --agent 1 anima topup --vision 1`,
)
process.exit(2)
}
Expand All @@ -102,6 +103,7 @@ async function main(): Promise<void> {
'--provider',
providerIdx >= 0 ? argv[providerIdx + 1] : undefined,
)
const vision = parseAmount('--vision', visionIdx >= 0 ? argv[visionIdx + 1] : undefined)
if (providerLegacyArg !== undefined && sandboxArg === undefined) {
console.warn(
'[deprecated] `anima topup --provider` is renamed to `--sandbox` (Galileo testnet billing); both flags work for now but `--provider` will be removed in a future release.',
Expand All @@ -114,7 +116,7 @@ async function main(): Promise<void> {
}
const sandbox = sandboxArg ?? providerLegacyArg
const { runTopup } = await import('./commands/topup')
await runTopup({ agent, compute, sandbox })
await runTopup({ agent, compute, sandbox, vision })
return
}
case 'model': {
Expand Down Expand Up @@ -335,7 +337,8 @@ function printHelp(): void {
' anima transfer <ref> transfer iNFT to a new operator with re-encrypted keystore',
' flags: --to <addr>, --recipient-key <hex>, --oracle-key <hex>,',
' --dry-run, --yes, --no-purge',
' anima topup add funds (flags: --agent N --compute N --sandbox N)',
' anima topup add funds (flags: --agent N --compute N --sandbox N --vision N)',
' (--vision N seeds the 0G Compute vision provider sub-account)',
' (--provider N is a deprecated alias for --sandbox)',
' anima ledger [sub] compute ledger ops (subs: balance | refund | retrieve | close)',
' flags: --amount N --all --yes',
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@s0nderlabs/anima-core",
"version": "0.24.16",
"version": "0.24.17",
"type": "module",
"description": "Always-on infrastructure for anima, the sovereign agent gateway on 0G (runtime, brain, identity, memory, storage, wallet, tool registry)",
"license": "MIT",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/brain/frozen-prefix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ If a tool fails, surface the error clearly. Never claim success when a tool was

CRITICAL anti-skip: If your reply asserts a tool ran ("output:", "exit 0", "returned X", "the date is", "here's the result", "✓", "done", "completed", "fetched", "saved", "sent"), you MUST have actually called the tool in this same turn. Never claim execution without firing the call. This applies under EVERY permission mode — \`off\` (yolo), \`prompt\` (modal-gated), and \`strict\` (denied). If \`prompt\` mode would gate the call behind an approval modal, FIRE THE CALL ANYWAY and let the modal handle it. Skipping the call to avoid the modal and inventing a plausible answer is a hallucination that leaves operators believing a real action happened when nothing did.

CRITICAL multi-tool fidelity: when the operator asks for N actions in one message (numbered "(1) ... (2) ... (3) ..." or comma-separated "do A, then B, then C"), you MUST fire ONE tool call per requested action. Counting matters. If the operator asked for 5 things, you cannot summarize a turn with 5 result lines unless 5 tool calls were made. Never narrate a tool result you did not just receive. If you intentionally skip one — say it explicitly ("I did not run stake.position because the prior turn already showed it"), do NOT invent a plausible result. The activity log is the source of truth; if a reader greps your turn for the tool name they should find a real tool_call entry, not just a sentence claiming completion.

NEVER answer these from memory or guess — ALWAYS use a tool:
- Current time, date, timezone → \`shell.run\` (e.g. \`date\`)
- File contents, sizes, line counts → \`fs.read\`, \`fs.search\`
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/brain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export {
getLedgerDetail,
getLedgerDetailReadOnly,
depositToLedger,
transferFundToProvider,
refundFromLedger,
retrieveLedgerFunds,
closeLedger,
Expand Down
Loading