zettcode 0.3.2__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zettcode-0.3.2 → zettcode-0.3.3}/PKG-INFO +2 -2
- {zettcode-0.3.2 → zettcode-0.3.3}/pyproject.toml +1 -1
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/__init__.py +1 -1
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/cli.py +48 -3
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/client.py +26 -7
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/finder.py +2 -2
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/hub.py +69 -5
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/models.py +3 -1
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/protocol.py +96 -8
- zettcode-0.3.3/src/zettcode/broker/reload.py +93 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/store.py +68 -13
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/web.py +8 -6
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/cli.py +11 -2
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/daemon/run.py +18 -2
- zettcode-0.3.3/src/zettcode/engine/extensions/guidance.py +17 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/subagents.py +4 -4
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/messenger.py +13 -4
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/builtin_agent.py +4 -4
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/app.py +20 -5
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/backend.py +7 -6
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/chat.py +18 -4
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/service.py +167 -8
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/app.css +36 -0
- zettcode-0.3.3/src/zettcode/gateway/static/app.js +3 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/relay.py +3 -1
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/client.py +18 -4
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/types.py +8 -0
- zettcode-0.3.2/src/zettcode/gateway/static/app.js +0 -3
- {zettcode-0.3.2 → zettcode-0.3.3}/.gitignore +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/README.md +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/_compat.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/agent/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/agent/commands.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/agent/settings.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/changes.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/effort_gauge.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/entries.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/presenters.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/projection.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/pulse.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/rendering.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/replay.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/shimmer.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/stops.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/text.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/tips.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/transcript/fold.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/transcript/stream.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/agent/conversation/transcript/turn.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/brand.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/commandline/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/commandline/arguments.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/commandline/completion.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/commandline/history.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/commandline/runner.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/commands.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/cat.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/chibi.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/dog.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/kangaroo.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/model.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/penguin.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/polarbear.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/scarfy.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/seagull.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/slime.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/snowbear.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/sprout.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/tiger.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/pets/whale.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/plugins/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/plugins/builtin_commands.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/plugins/builtins.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/plugins/groups.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/plugins/host.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/plugins/loader.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/plugins/mixins.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/plugins/plugin.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/plugins/state.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/app.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/bars.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/boot.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/clipboard.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/commands.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/config_form.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/demo.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/followups.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/gc_policy.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/keys.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/labels.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/messaging.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/notices.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/providers.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/sessions.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/settings.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/shell.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/turns.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/updating.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/approval_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/bash_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/completer.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/composer.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/context_charts.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/context_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/document_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/effort_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/follow_up.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/form_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/harness_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/input_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/model_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/model_settings_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/panel.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/pet.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/pet_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/plugins_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/root.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/self_compact_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/statusline_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/steering.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/switch.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/transcript.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/update_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/usage_charts.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/usage_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/app/ui/widgets/welcome.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/__main__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/files.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/log.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/process.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/roster.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/runner.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/server.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/broker/text.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/budget.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/collector.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/config.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/config_edit.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/daemon/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/daemon/__main__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/daemon/cli.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/daemon/companions.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/daemon/process.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/daemon/state.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/doctor.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/agent/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/agent/base.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/agent/context.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/agent/exports.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/agent/interaction.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/agent/sessions.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/agent/settings.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/agent/turns.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/core.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/dialogue/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/dialogue/ask.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/dialogue/side.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/dialogue/title.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/export/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/export/trace/script.js +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/export/trace/style.css +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/export/trace/template.html +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/capabilities.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/compaction.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/context.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/files.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/harness.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/harness_switch.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/mcp.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/mcp_notice.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/messaging.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/relay.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/tool_output.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/truncation.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/extensions/usage.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/harness_paths.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/permission/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/permission/approval.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/permission/readonly.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/permission/sandbox.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/pipeline.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/commands.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/context.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/extension.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/groups.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/loader.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/mixins.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/plugin.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/services.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/settings.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/state.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/plugins/switches.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/server_tools.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/storage/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/storage/layout.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/storage/ledger.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/storage/locks.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/storage/metadata.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/storage/records.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/storage/store.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/tools/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/tools/boundary.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/tools/files.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/tools/images.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/tools/output.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/tools/shell.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/engine/tools/workspace.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/error_text.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/filelock.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/cli.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/runfile.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/settings.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/assets/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/chunks/chunk-Y6SLVHK3.js +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/chunks/render-G6G3TSB3.js +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/icon.png +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/index.html +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/logo.svg +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/gateway/static/manifest.webmanifest +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/logging_setup.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/mentions.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/modes.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/oneshot.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/paths.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/plugin_cli.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/plugin_tui.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/processes.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/prompt.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/providers.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/py.typed +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/registry.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/commands.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/daemons.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/errors.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/events.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/handlers.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/models.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/session.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/sdk/translation.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/server/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/server/app.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/server/cli.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/server/runner.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/server/schemas.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/server/serialize.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/server/settings.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/statusline.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/table.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/capabilities.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/clipboard.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/console.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/app.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/events.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/focus.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/geometry.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/host.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/keymap.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/scheduler.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/screen.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/theme.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/core/widget.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/gallery.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/glyphs.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/input/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/input/bridge.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/input/decoder.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/input/events.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/input/keys.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/input/reader.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/layout/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/layout/box.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/layout/overlay.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/layout/scroll.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/layout/solver.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/layout/spacing.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/render/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/render/ansi.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/render/canvas.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/render/code.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/render/color.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/render/renderer.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/render/rich_text.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/render/style.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/render/sweep.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/render/text.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/runner.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/terminal.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/testing/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/testing/harness.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/testing/snapshot.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/theme_file.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/ayu-mirage.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/catppuccin-latte.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/catppuccin-mocha.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/dracula.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/github-dimmed.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/github-light.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/gruvbox-dark.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/gruvbox-light.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/kanagawa.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/nord.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/one-dark.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/rose-pine-dawn.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/rose-pine.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/solarized-dark.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/solarized-light.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/themes/tokyo-night.toml +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/__init__.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/collapsible.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/completion.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/console.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/dialog.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/diff.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/list.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/list_page.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/markdown.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/progress.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/rich_text.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/status.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/table.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/tasks.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/text.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/textarea.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/tui/widgets/toast.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/update.py +0 -0
- {zettcode-0.3.2 → zettcode-0.3.3}/src/zettcode/wording.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: zettcode
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: A focused terminal coding agent powered by zett-agent
|
|
5
5
|
Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
|
|
6
6
|
Project-URL: Issues, https://github.com/Chang-LeHung/zettcode/issues
|
|
@@ -35,7 +35,7 @@ Requires-Dist: truststore<1,>=0.10
|
|
|
35
35
|
Requires-Dist: uvicorn<1,>=0.35
|
|
36
36
|
Requires-Dist: wcwidth<1,>=0.2
|
|
37
37
|
Requires-Dist: websockets<17,>=15
|
|
38
|
-
Requires-Dist: zett-agent<0.3,>=0.2.
|
|
38
|
+
Requires-Dist: zett-agent<0.3,>=0.2.6
|
|
39
39
|
Provides-Extra: all
|
|
40
40
|
Requires-Dist: zettcode-monitor<0.3,>=0.2; extra == 'all'
|
|
41
41
|
Requires-Dist: zettcode-recap<0.2,>=0.1; extra == 'all'
|
|
@@ -61,7 +61,7 @@ dependencies = [
|
|
|
61
61
|
# now leans on: a hosted tool the provider left open is closed with
|
|
62
62
|
# ``output={"status": "not_reported"}`` instead of leaving the step waiting, and
|
|
63
63
|
# `engine/server_tools.describe_output` reads that to say so.
|
|
64
|
-
"zett-agent>=0.2.
|
|
64
|
+
"zett-agent>=0.2.6,<0.3",
|
|
65
65
|
]
|
|
66
66
|
|
|
67
67
|
[project.optional-dependencies]
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
#: The one place the version is written. ``pyproject.toml`` reads it from here
|
|
9
9
|
#: and the release workflow checks the tag against it, so a bump is one edit.
|
|
10
|
-
__version__ = "0.3.
|
|
10
|
+
__version__ = "0.3.3"
|
|
11
11
|
|
|
12
12
|
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
13
13
|
_EXPORTS = {
|
|
@@ -22,6 +22,8 @@ from ..filelock import is_held
|
|
|
22
22
|
from . import process
|
|
23
23
|
from .files import MODULE, Note, broker_dir, lock_path, log_path, read_note, remove_note
|
|
24
24
|
from .finder import spawn_broker
|
|
25
|
+
from .protocol import PROTOCOL_VERSION
|
|
26
|
+
from .reload import outdated
|
|
25
27
|
|
|
26
28
|
EXIT_OK = 0
|
|
27
29
|
EXIT_FAILED = 1
|
|
@@ -65,8 +67,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
65
67
|
)
|
|
66
68
|
parser.add_argument(
|
|
67
69
|
"action",
|
|
68
|
-
choices=("start", "stop", "restart"),
|
|
69
|
-
help=
|
|
70
|
+
choices=("start", "stop", "restart", "reload"),
|
|
71
|
+
help=(
|
|
72
|
+
"start a broker if there is none, stop the one there is, or do both; reload restarts it only when it is "
|
|
73
|
+
"older than this version and no background agent is on it (zettcode runs this by itself after an upgrade)"
|
|
74
|
+
),
|
|
70
75
|
)
|
|
71
76
|
parser.add_argument(
|
|
72
77
|
"-y",
|
|
@@ -88,10 +93,44 @@ def broker_program(argv: list[str] | None = None, companions: Companions | None
|
|
|
88
93
|
directory = broker_dir()
|
|
89
94
|
if args.action == "restart":
|
|
90
95
|
return restart(directory, companions, assume_yes=args.yes)
|
|
96
|
+
if args.action == "reload":
|
|
97
|
+
return reload(directory, companions)
|
|
91
98
|
action = {"start": start, "stop": stop}[args.action]
|
|
92
99
|
return action(directory)
|
|
93
100
|
|
|
94
101
|
|
|
102
|
+
def reload(directory: Path, companions: Companions | None = None) -> int:
|
|
103
|
+
"""Replace the running broker when it is older than this version, and say nothing when there is nothing to do.
|
|
104
|
+
|
|
105
|
+
This is what runs behind every start of ``zettcode`` after an upgrade (:mod:`.reload`), so it never asks and never
|
|
106
|
+
stops what it should not: a broker with background agents on it is left as it is, with a line that says how to
|
|
107
|
+
restart it knowingly, because stopping them is something a person agrees to (``restart`` asks). A broker that is
|
|
108
|
+
current, newer, or not running is not touched. The status is 0 unless a replacement was tried and failed.
|
|
109
|
+
"""
|
|
110
|
+
note = outdated(directory)
|
|
111
|
+
if note is None:
|
|
112
|
+
return EXIT_OK
|
|
113
|
+
if companions is not None and (on_it := companions.listing()):
|
|
114
|
+
count = len(on_it)
|
|
115
|
+
print(
|
|
116
|
+
f"the broker (pid {note.pid}) speaks an older protocol, but {count} background "
|
|
117
|
+
f"agent{'s are' if count != 1 else ' is'} running on it, so it was left alone: "
|
|
118
|
+
"run `zettcode broker restart` when that is fine"
|
|
119
|
+
)
|
|
120
|
+
return EXIT_OK
|
|
121
|
+
# `stop` compares the note itself, in the same step that ends the broker, so what it ends is the broker that was
|
|
122
|
+
# found and never one another start of zettcode has just put in its place.
|
|
123
|
+
if stop(directory, expect=note.pid) != EXIT_OK:
|
|
124
|
+
return EXIT_FAILED
|
|
125
|
+
if running(directory):
|
|
126
|
+
# `stop` left a broker that is not the one that was found: another start replaced it first, and it is theirs.
|
|
127
|
+
return EXIT_OK
|
|
128
|
+
status = start(directory)
|
|
129
|
+
if status == EXIT_OK:
|
|
130
|
+
print(f"replaced broker {note.pid} (protocol {note.version}) with one that speaks {PROTOCOL_VERSION}")
|
|
131
|
+
return status
|
|
132
|
+
|
|
133
|
+
|
|
95
134
|
def running(directory: Path) -> bool:
|
|
96
135
|
"""Return whether a broker holds its lock in ``directory``, which is the only reliable sign that one is there."""
|
|
97
136
|
return is_held(lock_path(directory))
|
|
@@ -118,7 +157,7 @@ def start(directory: Path) -> int:
|
|
|
118
157
|
return EXIT_OK
|
|
119
158
|
|
|
120
159
|
|
|
121
|
-
def stop(directory: Path) -> int:
|
|
160
|
+
def stop(directory: Path, expect: int | None = None) -> int:
|
|
122
161
|
"""Stop the broker that is running, and say what happened.
|
|
123
162
|
|
|
124
163
|
A note whose broker has gone is removed rather than reported: it is what the next session would read, and a
|
|
@@ -127,8 +166,14 @@ def stop(directory: Path) -> int:
|
|
|
127
166
|
An explicit ``stop`` does not compare the broker's protocol version: that rule (never end a newer broker) is for
|
|
128
167
|
a session replacing one automatically, so two versions do not end each other's in turn; a person who asks for
|
|
129
168
|
this wants it stopped, whatever it says it speaks.
|
|
169
|
+
|
|
170
|
+
With ``expect``, only the broker with that process id is ended: the note is read once, here, and a broker it does
|
|
171
|
+
not name (another start of zettcode replaced the one that was meant) is left running. That is a compare and a stop
|
|
172
|
+
in one step, which a check made before calling this cannot be.
|
|
130
173
|
"""
|
|
131
174
|
note = read_note(directory)
|
|
175
|
+
if expect is not None and (note is None or note.pid != expect):
|
|
176
|
+
return EXIT_OK
|
|
132
177
|
if not running(directory):
|
|
133
178
|
if note is not None:
|
|
134
179
|
remove_note(directory, note.token)
|
|
@@ -25,7 +25,7 @@ import itertools
|
|
|
25
25
|
import json
|
|
26
26
|
import time
|
|
27
27
|
from collections import deque
|
|
28
|
-
from collections.abc import Callable
|
|
28
|
+
from collections.abc import Callable, Sequence
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
from typing import Any
|
|
31
31
|
|
|
@@ -38,9 +38,11 @@ from .protocol import (
|
|
|
38
38
|
VIA_AGENT,
|
|
39
39
|
VIA_USER,
|
|
40
40
|
VIAS,
|
|
41
|
+
ImageData,
|
|
41
42
|
Limits,
|
|
42
43
|
PeerInfo,
|
|
43
44
|
Refused,
|
|
45
|
+
parse_images,
|
|
44
46
|
roster_from_wire,
|
|
45
47
|
)
|
|
46
48
|
from .text import clean_text
|
|
@@ -346,18 +348,28 @@ class BrokerClient:
|
|
|
346
348
|
|
|
347
349
|
# -- sending ----------------------------------------------------------------------------------
|
|
348
350
|
async def send(
|
|
349
|
-
self,
|
|
351
|
+
self,
|
|
352
|
+
to: str,
|
|
353
|
+
text: str,
|
|
354
|
+
*,
|
|
355
|
+
hops: int = 0,
|
|
356
|
+
via: str = VIA_AGENT,
|
|
357
|
+
direct: bool = False,
|
|
358
|
+
mode: str = MODE_STEER,
|
|
359
|
+
images: Sequence[ImageData] = (),
|
|
350
360
|
) -> Delivered:
|
|
351
361
|
"""Send a message to another session and return once it has taken it.
|
|
352
362
|
|
|
353
363
|
Args:
|
|
354
364
|
to: The session's handle or id.
|
|
355
|
-
text: What to say.
|
|
365
|
+
text: What to say; it may be empty when ``images`` is not.
|
|
356
366
|
hops: How many sessions it has passed through already.
|
|
357
367
|
via: How it came: the reader, the agent, or an external program.
|
|
358
368
|
direct: Ask a session that can to run it as its own reader's prompt and answer the sender.
|
|
359
369
|
mode: What a busy recipient does with it: ``"steer"`` (the default) hands it to the running request,
|
|
360
370
|
``"followup"`` waits until that request ends.
|
|
371
|
+
images: Pictures to send with it. Only a ``direct`` message from a reader may carry them, and the broker
|
|
372
|
+
refuses them for any other.
|
|
361
373
|
|
|
362
374
|
Raises:
|
|
363
375
|
SendError: There is no broker, or the broker or the recipient refused, or nobody answered in time, or
|
|
@@ -365,10 +377,12 @@ class BrokerClient:
|
|
|
365
377
|
"""
|
|
366
378
|
if mode not in MODES:
|
|
367
379
|
raise SendError("invalid", "mode is 'steer' or 'followup'")
|
|
368
|
-
cleaned = self._checked(text)
|
|
369
|
-
body = {"to": to, "text": cleaned, "via": via, "hops": hops, "mode": mode}
|
|
380
|
+
cleaned = self._checked(text, silent=bool(images))
|
|
381
|
+
body: dict[str, Any] = {"to": to, "text": cleaned, "via": via, "hops": hops, "mode": mode}
|
|
370
382
|
if direct:
|
|
371
383
|
body["direct"] = True
|
|
384
|
+
if images:
|
|
385
|
+
body["images"] = [picture.to_wire() for picture in images]
|
|
372
386
|
return self._delivered(await self._request("POST", "/messages", json=body, send=True))
|
|
373
387
|
|
|
374
388
|
async def send_group(
|
|
@@ -398,9 +412,9 @@ class BrokerClient:
|
|
|
398
412
|
)
|
|
399
413
|
return GroupDelivered(name if isinstance(name, str) else group, tuple(delivered), failed)
|
|
400
414
|
|
|
401
|
-
def _checked(self, text: str) -> str:
|
|
415
|
+
def _checked(self, text: str, *, silent: bool = False) -> str:
|
|
402
416
|
cleaned = clean_text(text)
|
|
403
|
-
if not cleaned.strip():
|
|
417
|
+
if not cleaned.strip() and not silent:
|
|
404
418
|
raise SendError("empty", "a message needs some text")
|
|
405
419
|
if len(cleaned) > self.limits.max_text:
|
|
406
420
|
raise SendError("too_long", f"a message is limited to {self.limits.max_text} characters")
|
|
@@ -679,6 +693,10 @@ class BrokerClient:
|
|
|
679
693
|
mode = MODE_STEER # a peer that does not say: the protocol's default
|
|
680
694
|
elif mode not in MODES:
|
|
681
695
|
mode = MODE_FOLLOWUP # one this version does not know: the safe half, not the disruptive one
|
|
696
|
+
try:
|
|
697
|
+
pictures = parse_images(line.get("images")) if line.get("direct") is True and via == VIA_USER else ()
|
|
698
|
+
except Refused:
|
|
699
|
+
return _ack(Receipt(False, "the message's pictures were malformed"))
|
|
682
700
|
incoming = Incoming(
|
|
683
701
|
sender,
|
|
684
702
|
clean_text(text),
|
|
@@ -687,6 +705,7 @@ class BrokerClient:
|
|
|
687
705
|
line.get("direct") is True and via == VIA_USER,
|
|
688
706
|
mode,
|
|
689
707
|
line["group"] if isinstance(line.get("group"), str) else "",
|
|
708
|
+
pictures,
|
|
690
709
|
)
|
|
691
710
|
try:
|
|
692
711
|
return _ack(self.on_message(incoming))
|
|
@@ -30,7 +30,7 @@ from .. import logging_setup
|
|
|
30
30
|
from ..filelock import is_held
|
|
31
31
|
from . import process
|
|
32
32
|
from .files import MODULE, Note, broker_dir, ensure_directory, lock_path, log_path, read_note
|
|
33
|
-
from .protocol import
|
|
33
|
+
from .protocol import MAX_MESSAGE_BODY, PROTOCOL_VERSION
|
|
34
34
|
|
|
35
35
|
#: A broker is started at most this often, so one that dies at once is not restarted in a loop.
|
|
36
36
|
SPAWN_INTERVAL = 3.0
|
|
@@ -152,7 +152,7 @@ class Finder:
|
|
|
152
152
|
additional_headers={"Authorization": f"Bearer {note.token}"},
|
|
153
153
|
proxy=None, # never through a proxy named in the environment: the broker is on this machine
|
|
154
154
|
compression=None,
|
|
155
|
-
max_size=
|
|
155
|
+
max_size=MAX_MESSAGE_BODY,
|
|
156
156
|
open_timeout=self.connect_timeout,
|
|
157
157
|
)
|
|
158
158
|
except InvalidStatus as refused:
|
|
@@ -39,20 +39,27 @@ from .protocol import (
|
|
|
39
39
|
STREAM_TYPES,
|
|
40
40
|
VIA_EXTERNAL,
|
|
41
41
|
VIA_USER,
|
|
42
|
+
ImageData,
|
|
42
43
|
Limits,
|
|
43
44
|
PeerInfo,
|
|
44
45
|
Refused,
|
|
45
46
|
SessionUpdate,
|
|
46
47
|
group_name,
|
|
48
|
+
parse_images,
|
|
47
49
|
parse_send,
|
|
48
50
|
)
|
|
49
51
|
from .roster import Entry, Roster
|
|
50
|
-
from .store import KIND_GROUP, KIND_SESSION, Store
|
|
52
|
+
from .store import KIND_GROUP, KIND_SESSION, MAX_WINDOW, Store
|
|
51
53
|
from .text import clean_text, clean_title, slug
|
|
52
54
|
|
|
53
55
|
#: Messages waiting for one session to say it took them. More are told the session is busy.
|
|
54
56
|
MAX_WAITING = 32
|
|
55
57
|
|
|
58
|
+
#: The pictures, as base64, that one session may have waiting for it at once: in messages it has not answered, and in
|
|
59
|
+
#: lines it has not read. A message may carry about 11 MiB of them, and a session that has stopped answering would
|
|
60
|
+
#: otherwise hold hundreds of those in the broker.
|
|
61
|
+
MAX_WAITING_PICTURES = 96 << 20
|
|
62
|
+
|
|
56
63
|
#: Lines queued for one session to read. A session that is this far behind has stopped reading and is dropped.
|
|
57
64
|
MAX_QUEUED = 256
|
|
58
65
|
|
|
@@ -103,6 +110,11 @@ class Session:
|
|
|
103
110
|
#: Whether this connection asked to be told of every message that passes (the gateway's graph does).
|
|
104
111
|
self.observing = False
|
|
105
112
|
self._waiting: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
113
|
+
#: The size, as base64, of the pictures in the messages in ``_waiting``.
|
|
114
|
+
self._weight = 0
|
|
115
|
+
#: The size, as base64, of the pictures in the lines still in ``outbox``, which a message whose ack timed out
|
|
116
|
+
#: leaves behind: a socket that never drains must not be able to hold more than the budget.
|
|
117
|
+
self._queued = 0
|
|
106
118
|
self._deliveries = itertools.count(1)
|
|
107
119
|
|
|
108
120
|
def push(self, line: dict[str, Any]) -> None:
|
|
@@ -113,6 +125,14 @@ class Session:
|
|
|
113
125
|
if not self._dropped:
|
|
114
126
|
self._dropped = True
|
|
115
127
|
self.close(STOPPED_READING)
|
|
128
|
+
else:
|
|
129
|
+
self._queued += _picture_weight(line)
|
|
130
|
+
|
|
131
|
+
async def next_line(self) -> dict[str, Any]:
|
|
132
|
+
"""Take the next queued line, for the writer to send: its pictures stop counting against this session's budget."""
|
|
133
|
+
line = await self.outbox.get()
|
|
134
|
+
self._queued -= _picture_weight(line)
|
|
135
|
+
return line
|
|
116
136
|
|
|
117
137
|
def push_lossy(self, line: dict[str, Any]) -> None:
|
|
118
138
|
"""Queue a line that may be lost: when this session is half a queue behind it is skipped, not dropped for it.
|
|
@@ -138,14 +158,19 @@ class Session:
|
|
|
138
158
|
"""
|
|
139
159
|
if self.waiting >= MAX_WAITING:
|
|
140
160
|
raise Refused("busy", "that session has too many messages waiting")
|
|
161
|
+
weight = sum(len(picture.get("data", "")) for picture in message.get("images", ()))
|
|
162
|
+
if weight and self._weight + self._queued + weight > MAX_WAITING_PICTURES:
|
|
163
|
+
raise Refused("busy", "that session has too many pictures waiting")
|
|
141
164
|
number = next(self._deliveries)
|
|
142
165
|
ack: asyncio.Future[dict[str, Any]] = asyncio.get_running_loop().create_future()
|
|
143
166
|
self._waiting[number] = ack
|
|
167
|
+
self._weight += weight
|
|
144
168
|
try:
|
|
145
169
|
self.push({"op": "deliver", "id": number, **message})
|
|
146
170
|
return await asyncio.wait_for(ack, timeout)
|
|
147
171
|
finally:
|
|
148
172
|
self._waiting.pop(number, None)
|
|
173
|
+
self._weight -= weight
|
|
149
174
|
|
|
150
175
|
def acked(self, line: dict[str, Any]) -> None:
|
|
151
176
|
"""Take an ack; one for something that was never asked, or already answered, is ignored."""
|
|
@@ -307,7 +332,11 @@ class Hub:
|
|
|
307
332
|
if sender.external:
|
|
308
333
|
body = {**body, "via": VIA_EXTERNAL}
|
|
309
334
|
group = body.get("group")
|
|
335
|
+
# Looked at, not yet decoded: a message that may not carry pictures is turned away before they cost anything.
|
|
336
|
+
offered = body.get("images")
|
|
310
337
|
if group is not None:
|
|
338
|
+
if offered:
|
|
339
|
+
raise Refused("invalid", "pictures go to one session, not to a group")
|
|
311
340
|
if body.get("to") not in (None, ""):
|
|
312
341
|
raise Refused("invalid", "name a session in 'to' or a group in 'group', not both")
|
|
313
342
|
# The group's name stands where a session's would while the text, the hops and the mode are checked.
|
|
@@ -315,7 +344,7 @@ class Hub:
|
|
|
315
344
|
if via == VIA_EXTERNAL and not sender.external:
|
|
316
345
|
raise Refused("invalid", "via is 'user' or 'agent'")
|
|
317
346
|
return await self._send_group(sender, group_name(group), text, via, hops, mode)
|
|
318
|
-
to, text, via, hops, mode = parse_send(body, self.limits)
|
|
347
|
+
to, text, via, hops, mode = parse_send(body, self.limits, may_be_silent=bool(offered))
|
|
319
348
|
if via == VIA_EXTERNAL and not sender.external:
|
|
320
349
|
raise Refused("invalid", "via is 'user' or 'agent'")
|
|
321
350
|
target = self.roster.resolve(to, asker=sender.connection)
|
|
@@ -327,13 +356,24 @@ class Hub:
|
|
|
327
356
|
# that said it can; for any other the flag is dropped and the message is delivered the ordinary way.
|
|
328
357
|
if body.get("direct") is True and via == VIA_USER and not sender.external and target.direct:
|
|
329
358
|
message["direct"] = True
|
|
359
|
+
pictures: tuple[ImageData, ...] = ()
|
|
360
|
+
if offered:
|
|
361
|
+
# Only a session that runs the message as its reader's own prompt has anywhere to put a picture: for any
|
|
362
|
+
# other the pictures would be dropped on the way in, and the sender told it went through.
|
|
363
|
+
if message.get("direct") is not True:
|
|
364
|
+
raise Refused(
|
|
365
|
+
"invalid",
|
|
366
|
+
f"@{target.handle} does not take pictures: it does not run a chat's text as its own prompt",
|
|
367
|
+
)
|
|
368
|
+
pictures = parse_images(offered)
|
|
369
|
+
message["images"] = [picture.to_wire() for picture in pictures]
|
|
330
370
|
ack = await self._hand_over(recipient, message, target.handle)
|
|
331
371
|
stored = self._remember(
|
|
332
372
|
sender,
|
|
333
373
|
KIND_SESSION,
|
|
334
374
|
(target.session_id, target.handle, target.title),
|
|
335
375
|
[target.session_id],
|
|
336
|
-
text,
|
|
376
|
+
_with_pictures(text, len(pictures)),
|
|
337
377
|
via,
|
|
338
378
|
hops + 1,
|
|
339
379
|
mode,
|
|
@@ -624,11 +664,19 @@ class Hub:
|
|
|
624
664
|
def timeline(self, since: float, until: float, limit: int) -> dict[str, Any]:
|
|
625
665
|
"""Return the messages of a stretch of the past, oldest first, for a front end to replay on screen.
|
|
626
666
|
|
|
627
|
-
Nothing is sent again: this is only what was stored.
|
|
667
|
+
Nothing is sent again: this is only what was stored. When more than ``limit`` messages fall in the stretch the
|
|
668
|
+
newest are returned and ``truncated`` says so, so that a replay can say it is missing the start of the stretch
|
|
669
|
+
rather than draw a flow that is quietly incomplete.
|
|
628
670
|
"""
|
|
671
|
+
# One less than the most a read may return, so that the extra row asked for below is always really read: at the
|
|
672
|
+
# cap itself the count could never exceed the limit, and a stretch of fifty thousand would be called complete.
|
|
673
|
+
limit = max(1, min(limit, MAX_WINDOW - 1))
|
|
674
|
+
found = self.store.window(since, until, limit=limit + 1)
|
|
675
|
+
truncated = len(found) > limit
|
|
629
676
|
span = self.store.span()
|
|
630
677
|
return {
|
|
631
|
-
"messages":
|
|
678
|
+
"messages": found[-limit:] if truncated else found,
|
|
679
|
+
"truncated": truncated,
|
|
632
680
|
"span": list(span) if span else None,
|
|
633
681
|
"now": time.time(),
|
|
634
682
|
}
|
|
@@ -715,6 +763,22 @@ class Hub:
|
|
|
715
763
|
session.push({"op": "roster", "sessions": [entry.to_wire() for entry in self.roster.snapshot()]})
|
|
716
764
|
|
|
717
765
|
|
|
766
|
+
def _picture_weight(line: Mapping[str, Any]) -> int:
|
|
767
|
+
"""Return the size, as base64, of the pictures a line carries: a ``deliver`` is the only line that has any."""
|
|
768
|
+
images = line.get("images")
|
|
769
|
+
if not isinstance(images, list):
|
|
770
|
+
return 0
|
|
771
|
+
return sum(len(item.get("data", "")) for item in images if isinstance(item, dict))
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _with_pictures(text: str, count: int) -> str:
|
|
775
|
+
"""Return what the history keeps of a message: its words, and how many pictures went with it, never the pictures."""
|
|
776
|
+
if not count:
|
|
777
|
+
return text
|
|
778
|
+
note = f"[{count} picture{'s' if count != 1 else ''}]"
|
|
779
|
+
return f"{text}\n{note}" if text.strip() else note
|
|
780
|
+
|
|
781
|
+
|
|
718
782
|
def _count(raw: object, default: int) -> int:
|
|
719
783
|
"""Read a count from a query string; one that is not a number is the default."""
|
|
720
784
|
try:
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
|
|
7
7
|
from ..modes import MODE_STEER
|
|
8
|
-
from .protocol import KIND_ZETTCODE, PeerInfo
|
|
8
|
+
from .protocol import KIND_ZETTCODE, ImageData, PeerInfo
|
|
9
9
|
|
|
10
10
|
#: Another word for a registered session, as the rest of the application knows it.
|
|
11
11
|
RosterEntry = PeerInfo
|
|
@@ -38,6 +38,7 @@ class Incoming:
|
|
|
38
38
|
mode: What a busy recipient should do with it: ``"steer"`` (the default) hands it to the request that is
|
|
39
39
|
running, and ``"followup"`` waits until that request ends. An idle recipient starts a turn either way.
|
|
40
40
|
group: The group chat it was written in (without the ``#``), or an empty string for a one-to-one message.
|
|
41
|
+
images: The pictures it carries, already checked: only a ``direct`` message has any.
|
|
41
42
|
"""
|
|
42
43
|
|
|
43
44
|
sender: PeerInfo
|
|
@@ -47,6 +48,7 @@ class Incoming:
|
|
|
47
48
|
direct: bool = False
|
|
48
49
|
mode: str = MODE_STEER
|
|
49
50
|
group: str = ""
|
|
51
|
+
images: tuple[ImageData, ...] = ()
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
@dataclass(frozen=True, slots=True)
|
|
@@ -16,7 +16,8 @@ HTTP, for anyone who holds the token — a session, a script, a hook, ``curl``::
|
|
|
16
16
|
the owner's token alone: ?a=&b= or ?group=) -> {"messages": [...]}
|
|
17
17
|
GET /graph -> {"sessions", "groups", "edges", "recent"} (token only; for the gateway's graph)
|
|
18
18
|
GET /older ?before=<message id>&limit= -> {"messages", "more"} the stored messages before one
|
|
19
|
-
GET /timeline ?since=&until=&limit= -> {"messages", "span", "now"} stored messages, to replay on
|
|
19
|
+
GET /timeline ?since=&until=&limit= -> {"messages", "truncated", "span", "now"} stored messages, to replay on
|
|
20
|
+
screen (at most MAX_WINDOW; "truncated" says the stretch held more and its start is missing)
|
|
20
21
|
|
|
21
22
|
A failure is an HTTP status and ``{"error": {"reason", "message"}}``; the reason is what a program acts on. A
|
|
22
23
|
message's ``mode`` says what a busy recipient should do with it: ``steer`` (the default) hands it to the request
|
|
@@ -32,7 +33,7 @@ A *session* — ZettCode's, or any program that wants to be listed and reached
|
|
|
32
33
|
{"op": "traffic", "kind": "message"|"graph", "message"?} to an observer: what passed, what changed
|
|
33
34
|
{"op": "done", "n": 1, "handle": ...} the answer to a session or leave
|
|
34
35
|
{"op": "refused", "n": 1, "error": {"reason", "message"}} ... or its refusal
|
|
35
|
-
{"op": "deliver", "id": 9, "from": {...}, "text", "hops", "via", "mode"} a message; answer with an ack
|
|
36
|
+
{"op": "deliver", "id": 9, "from": {...}, "text", "hops", "via", "mode", "images"?} a message; answer with an ack
|
|
36
37
|
|
|
37
38
|
and the session says::
|
|
38
39
|
|
|
@@ -68,6 +69,7 @@ characters the composer can type, a chain of messages that keep triggering one a
|
|
|
68
69
|
|
|
69
70
|
from __future__ import annotations
|
|
70
71
|
|
|
72
|
+
import base64
|
|
71
73
|
import re
|
|
72
74
|
from collections.abc import Mapping
|
|
73
75
|
from dataclasses import dataclass
|
|
@@ -76,15 +78,24 @@ from typing import Any
|
|
|
76
78
|
from ..modes import MODE_STEER, MODES
|
|
77
79
|
from .text import clean_text, clean_title
|
|
78
80
|
|
|
79
|
-
#: Bumped whenever this protocol changes meaning.
|
|
81
|
+
#: Bumped whenever this protocol changes meaning. 9: a one-to-one message may carry pictures (``images`` on a send and
|
|
82
|
+
#: on a ``deliver``, with a longer line for the ``deliver``); 8: group chats (``group`` on a send, the ``groups`` line,
|
|
83
|
+
#: ``/groups``),
|
|
80
84
|
#: the persistent ``/history``, and observers (``observe``, the ``traffic`` line); 7: a message says whether a busy recipient should steer its running
|
|
81
85
|
#: request with it or wait behind that request (``mode``, default steer); 6 was the HTTP server and its WebSocket per
|
|
82
86
|
#: session, 5 lines of JSON on a TCP socket.
|
|
83
|
-
PROTOCOL_VERSION =
|
|
87
|
+
PROTOCOL_VERSION = 9
|
|
84
88
|
|
|
85
89
|
#: The longest message, in characters. Longer than anyone types, shorter than a paste of a file.
|
|
86
90
|
MAX_TEXT = 32_000
|
|
87
91
|
|
|
92
|
+
#: The pictures a one-to-one chat message may carry: how many, how large each is once decoded, and how large they are
|
|
93
|
+
#: together. The broker keeps none of them: it hands them over and writes down only how many there were.
|
|
94
|
+
IMAGE_TYPES = ("image/png", "image/jpeg", "image/gif", "image/webp")
|
|
95
|
+
MAX_IMAGES = 4
|
|
96
|
+
MAX_IMAGE_BYTES = 4 << 20
|
|
97
|
+
MAX_IMAGES_BYTES = 8 << 20
|
|
98
|
+
|
|
88
99
|
#: The longest title the roster keeps; a longer one is cut.
|
|
89
100
|
MAX_TITLE = 120
|
|
90
101
|
|
|
@@ -95,6 +106,11 @@ MAX_WORKSPACE = 512
|
|
|
95
106
|
#: character escaped) and a roster of a few hundred sessions both fit. A longer one is refused or ends the connection.
|
|
96
107
|
MAX_BODY = 1 << 20
|
|
97
108
|
|
|
109
|
+
#: The longest ``POST /messages`` body, and the longest line a session reads from the broker: :data:`MAX_BODY` for the
|
|
110
|
+
#: words, plus the pictures a message may carry as base64 (a third larger than they are). Everything else a session sends
|
|
111
|
+
#: stays within :data:`MAX_BODY`.
|
|
112
|
+
MAX_MESSAGE_BODY = MAX_BODY + (MAX_IMAGES_BYTES + 2) // 3 * 4
|
|
113
|
+
|
|
98
114
|
#: How many sessions a message may pass through. A reader who typed it is hop zero; each session whose agent
|
|
99
115
|
#: forwards it adds one. Two agents that answer each other would otherwise spend tokens for ever, so the broker
|
|
100
116
|
#: refuses the message that would be hop :data:`MAX_HOPS` + 1.
|
|
@@ -177,6 +193,75 @@ class Refused(Exception):
|
|
|
177
193
|
return cls("invalid", "the other end refused without saying why")
|
|
178
194
|
|
|
179
195
|
|
|
196
|
+
@dataclass(frozen=True, slots=True)
|
|
197
|
+
class ImageData:
|
|
198
|
+
"""One picture a message carries: its media type and the encoded file (not decoded pixels)."""
|
|
199
|
+
|
|
200
|
+
media_type: str
|
|
201
|
+
data: bytes
|
|
202
|
+
|
|
203
|
+
def to_wire(self) -> dict[str, str]:
|
|
204
|
+
"""Return the picture as a line carries it: the media type and the file in base64."""
|
|
205
|
+
return {"media_type": self.media_type, "data": base64.b64encode(self.data).decode("ascii")}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def sniff_image(data: bytes) -> str | None:
|
|
209
|
+
"""Return the media type a file's first bytes say it is, or ``None`` when it is not a picture a model takes."""
|
|
210
|
+
if data.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
211
|
+
return "image/png"
|
|
212
|
+
if data.startswith(b"\xff\xd8\xff"):
|
|
213
|
+
return "image/jpeg"
|
|
214
|
+
if data.startswith((b"GIF87a", b"GIF89a")):
|
|
215
|
+
return "image/gif"
|
|
216
|
+
if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
|
|
217
|
+
return "image/webp"
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def parse_images(raw: object) -> tuple[ImageData, ...]:
|
|
222
|
+
"""Read the pictures a send carries, refusing what is not one.
|
|
223
|
+
|
|
224
|
+
A picture is ``{"media_type", "data"}`` with the file in base64. What it says it is must be what its first bytes say,
|
|
225
|
+
so a program cannot hand a model something else under an image's name, and each picture and all of them together
|
|
226
|
+
are bounded.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
Refused: ``invalid`` for a malformed or mislabelled picture, ``too_long`` for too many or too large.
|
|
230
|
+
"""
|
|
231
|
+
if raw is None:
|
|
232
|
+
return ()
|
|
233
|
+
if not isinstance(raw, list):
|
|
234
|
+
raise Refused("invalid", "images is a list")
|
|
235
|
+
if len(raw) > MAX_IMAGES:
|
|
236
|
+
raise Refused("too_long", f"a message may carry at most {MAX_IMAGES} pictures")
|
|
237
|
+
pictures: list[ImageData] = []
|
|
238
|
+
total = 0
|
|
239
|
+
for item in raw:
|
|
240
|
+
if not isinstance(item, Mapping) or not isinstance(item.get("data"), str):
|
|
241
|
+
raise Refused("invalid", "a picture is {media_type, data}")
|
|
242
|
+
kind = item.get("media_type")
|
|
243
|
+
if kind not in IMAGE_TYPES:
|
|
244
|
+
raise Refused("invalid", f"a picture is one of: {', '.join(IMAGE_TYPES)}")
|
|
245
|
+
encoded: str = item["data"]
|
|
246
|
+
if len(encoded) > (MAX_IMAGE_BYTES + 2) // 3 * 4:
|
|
247
|
+
raise Refused("too_long", f"a picture is limited to {MAX_IMAGE_BYTES >> 20} MiB")
|
|
248
|
+
try:
|
|
249
|
+
data = base64.b64decode(encoded, validate=True)
|
|
250
|
+
except ValueError:
|
|
251
|
+
raise Refused("invalid", "a picture's data is not base64") from None
|
|
252
|
+
if not data:
|
|
253
|
+
raise Refused("invalid", "a picture is empty")
|
|
254
|
+
if len(data) > MAX_IMAGE_BYTES:
|
|
255
|
+
raise Refused("too_long", f"a picture is limited to {MAX_IMAGE_BYTES >> 20} MiB")
|
|
256
|
+
if sniff_image(data) != kind:
|
|
257
|
+
raise Refused("invalid", f"a picture said to be {kind} is not")
|
|
258
|
+
total += len(data)
|
|
259
|
+
if total > MAX_IMAGES_BYTES:
|
|
260
|
+
raise Refused("too_long", f"the pictures of a message are limited to {MAX_IMAGES_BYTES >> 20} MiB together")
|
|
261
|
+
pictures.append(ImageData(str(kind), data))
|
|
262
|
+
return tuple(pictures)
|
|
263
|
+
|
|
264
|
+
|
|
180
265
|
def field(raw: Mapping[str, Any], name: str, kind: type, default: Any = None) -> Any:
|
|
181
266
|
"""Return a field of a line, or ``default`` when it is missing; a field of the wrong kind is ``invalid``."""
|
|
182
267
|
value = raw.get(name, default)
|
|
@@ -319,19 +404,22 @@ class SessionUpdate:
|
|
|
319
404
|
return cls(session_id, *_description(raw), _kind(raw), raw.get("direct") is True)
|
|
320
405
|
|
|
321
406
|
|
|
322
|
-
def parse_send(
|
|
407
|
+
def parse_send(
|
|
408
|
+
raw: Mapping[str, Any], limits: Limits, *, may_be_silent: bool = False
|
|
409
|
+
) -> tuple[str, str, str, int, str]:
|
|
323
410
|
"""Read and check a ``send``: who it is for, its cleaned text, how it came, its hops, and its mode.
|
|
324
411
|
|
|
325
412
|
The text is cleaned (see :func:`~.text.clean_text`) before it is measured, so the limit is on what would be
|
|
326
413
|
delivered. A message a reader typed, or one from outside, starts a chain whatever hop count it claims. The mode
|
|
327
|
-
is :data:`MODE_STEER` unless the sender asked for :data:`MODE_FOLLOWUP`.
|
|
414
|
+
is :data:`MODE_STEER` unless the sender asked for :data:`MODE_FOLLOWUP`. ``may_be_silent`` allows no words at all,
|
|
415
|
+
for a message that carries pictures.
|
|
328
416
|
|
|
329
417
|
Raises:
|
|
330
418
|
Refused: ``invalid`` for a field of the wrong kind or a mode the recipient does not know, ``empty`` for
|
|
331
419
|
nothing to say, ``too_long`` past the limit, ``loop`` when the message would pass the hop limit.
|
|
332
420
|
"""
|
|
333
421
|
to = field(raw, "to", str)
|
|
334
|
-
text = clean_text(field(raw, "text", str))
|
|
422
|
+
text = clean_text(field(raw, "text", str, "" if may_be_silent else None))
|
|
335
423
|
via = field(raw, "via", str, VIA_AGENT)
|
|
336
424
|
hops = field(raw, "hops", int, 0)
|
|
337
425
|
mode = field(raw, "mode", str, MODE_STEER)
|
|
@@ -341,7 +429,7 @@ def parse_send(raw: Mapping[str, Any], limits: Limits) -> tuple[str, str, str, i
|
|
|
341
429
|
raise Refused("invalid", "mode is 'steer' or 'followup'")
|
|
342
430
|
if hops < 0:
|
|
343
431
|
raise Refused("invalid", "hops is a count")
|
|
344
|
-
if not text.strip():
|
|
432
|
+
if not text.strip() and not may_be_silent:
|
|
345
433
|
raise Refused("empty", "a message needs some text")
|
|
346
434
|
if len(text) > limits.max_text:
|
|
347
435
|
raise Refused("too_long", f"a message is limited to {limits.max_text} characters")
|