talon-agent 4.6.1 → 5.0.1
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.
- package/README.md +3 -3
- package/package.json +2 -1
- package/prompts/README.md +2 -2
- package/prompts/system/memory-recall.md +16 -0
- package/src/app.ts +22 -7
- package/src/backend/claude-sdk/factory.ts +21 -41
- package/src/backend/claude-sdk/handler.ts +28 -7
- package/src/backend/claude-sdk/host/in-process.ts +147 -0
- package/src/backend/claude-sdk/one-shot.ts +1 -1
- package/src/backend/claude-sdk/options.ts +7 -6
- package/src/backend/claude-sdk/stream.ts +30 -3
- package/src/backend/claude-sdk/warm.ts +1 -1
- package/src/backend/codex/constants.ts +1 -1
- package/src/backend/codex/factory.ts +2 -2
- package/src/backend/codex/handler/events.ts +1 -1
- package/src/backend/codex/handler/message.ts +26 -12
- package/src/backend/codex/handler/rollout-accounting.ts +1 -1
- package/src/backend/codex/init.ts +1 -1
- package/src/backend/codex/mcp-config.ts +2 -2
- package/src/backend/codex/one-shot.ts +1 -1
- package/src/backend/kilo/handler/message.ts +4 -1
- package/src/backend/openai-agents/constants.ts +1 -1
- package/src/backend/openai-agents/factory.ts +2 -2
- package/src/backend/openai-agents/handler/events.ts +1 -1
- package/src/backend/openai-agents/handler/message.ts +8 -4
- package/src/backend/openai-agents/init.ts +1 -1
- package/src/backend/openai-agents/mcp-pool.ts +2 -2
- package/src/backend/opencode/handler/message.ts +4 -1
- package/src/backend/remote-server/chat-turn.ts +31 -23
- package/src/backend/remote-server/events.ts +3 -3
- package/src/backend/remote-server/factory.ts +6 -3
- package/src/backend/remote-server/index.ts +1 -1
- package/src/backend/remote-server/mcp.ts +1 -1
- package/src/backend/remote-server/one-shot.ts +1 -1
- package/src/backend/remote-server/server-bindings.ts +1 -1
- package/src/backend/remote-server/turn.ts +1 -1
- package/src/backend/runtime/cache/cache-metrics.ts +126 -0
- package/src/backend/{shared → runtime/cache}/cache-telemetry.ts +22 -2
- package/src/backend/{shared → runtime}/index.ts +34 -25
- package/src/backend/{shared → runtime/prompt}/delivery-contract.ts +1 -1
- package/src/backend/{shared → runtime/prompt}/prompt-format.ts +46 -2
- package/src/backend/{shared → runtime/prompt}/system-prompt.ts +4 -4
- package/src/backend/{shared → runtime/turn}/delivered-text.ts +1 -1
- package/src/backend/{shared → runtime/turn}/delivery.ts +3 -3
- package/src/backend/{shared → runtime/turn}/handle-retry.ts +5 -5
- package/src/backend/{shared → runtime/turn}/handler-to-events.ts +24 -10
- package/src/backend/{shared → runtime/turn}/handler-types.ts +7 -1
- package/src/backend/{shared → runtime/turn}/model-retry.ts +2 -2
- package/src/backend/{shared → runtime/turn}/result-events.ts +2 -2
- package/src/backend/{shared → runtime/turn}/stream-state.ts +2 -2
- package/src/backend/{shared → runtime/turn}/turn-interrupt.ts +2 -2
- package/src/backend/{shared → runtime/turn}/turn-phases.ts +6 -6
- package/src/bootstrap.ts +6 -21
- package/src/cli.ts +6 -3
- package/src/core/agent-runtime/README.md +15 -0
- package/src/core/agent-runtime/agent-host.ts +387 -0
- package/src/core/agent-runtime/capabilities.ts +11 -0
- package/src/core/agent-runtime/contract-tests.ts +92 -1
- package/src/core/agent-runtime/events.ts +1 -1
- package/src/core/background/{isolated-agent.ts → cron/isolated-agent.ts} +3 -3
- package/src/core/background/{job-health.ts → cron/job-health.ts} +1 -1
- package/src/core/background/{job-oneshot.ts → cron/job-oneshot.ts} +5 -5
- package/src/core/background/{job-prompt.ts → cron/job-prompt.ts} +1 -1
- package/src/core/background/{cron.ts → cron/scheduler.ts} +6 -6
- package/src/core/background/{cron-spec.ts → cron/spec.ts} +1 -1
- package/src/core/background/{dream.ts → dream/index.ts} +10 -15
- package/src/core/background/{plan-alerts.ts → pulse/plan-alerts.ts} +3 -3
- package/src/core/background/{pulse.ts → pulse/pulse.ts} +8 -5
- package/src/core/background/triggers/command.ts +2 -2
- package/src/{util → core/config}/harden.ts +1 -1
- package/src/core/config/index.ts +16 -12
- package/src/{util → core/daemon}/boot-timer.ts +15 -1
- package/src/core/daemon/pidfile.ts +1 -1
- package/src/core/daemon/resource-sampler.ts +121 -0
- package/src/{util → core/daemon}/respawn.ts +1 -1
- package/src/core/doctor/index.ts +1 -1
- package/src/core/engine/dispatcher.ts +16 -0
- package/src/core/engine/gateway-actions/cron.ts +2 -2
- package/src/core/engine/gateway-actions/fetch-url.ts +1 -1
- package/src/core/engine/gateway-actions/index.ts +4 -1
- package/src/core/engine/gateway-actions/memory.ts +335 -0
- package/src/core/engine/gateway-actions/native/exec-background.ts +129 -0
- package/src/core/engine/gateway-actions/native/exec-remote.ts +85 -0
- package/src/core/engine/gateway-actions/native/exec.ts +182 -0
- package/src/core/engine/gateway-actions/native/index.ts +46 -0
- package/src/core/engine/gateway-actions/native/params.ts +46 -0
- package/src/core/engine/gateway-actions/native/read.ts +182 -0
- package/src/core/engine/gateway-actions/native/results.ts +34 -0
- package/src/core/engine/gateway-actions/native/search.ts +212 -0
- package/src/core/engine/gateway-actions/native/shell.ts +39 -0
- package/src/core/engine/gateway-actions/native/teleport.ts +55 -0
- package/src/core/engine/gateway-actions/native/write.ts +170 -0
- package/src/core/engine/gateway.ts +0 -6
- package/src/core/frontend-runtime/builtins.ts +2 -2
- package/src/{util → core/frontend-runtime}/chat-id.ts +30 -0
- package/src/core/mcp-hub/index.ts +1 -1
- package/src/core/mcp-hub/talon-server.ts +1 -1
- package/src/core/memory/import.ts +3 -2
- package/src/core/memory/taps.ts +199 -0
- package/src/core/memory/turn-retrieval.ts +222 -0
- package/src/core/plugin/mcp.ts +1 -1
- package/src/core/prompt/assemble.ts +2 -12
- package/src/core/prompt/index.ts +2 -2
- package/src/core/prompt/invalidation.ts +1 -1
- package/src/core/{scripting/lua-runner.ts → scripts/lua.ts} +1 -1
- package/src/core/tasks/index.ts +1 -1
- package/src/core/tools/{chat.ts → chat/chat.ts} +1 -1
- package/src/core/tools/{cross-send.ts → chat/cross-send.ts} +1 -1
- package/src/core/tools/{history.ts → chat/history.ts} +2 -2
- package/src/core/tools/{media.ts → chat/media.ts} +1 -1
- package/src/core/tools/{members.ts → chat/members.ts} +2 -2
- package/src/core/tools/{messaging.ts → chat/messaging.ts} +2 -2
- package/src/core/tools/{moderation.ts → chat/moderation.ts} +2 -2
- package/src/core/tools/{stickers.ts → chat/stickers.ts} +2 -2
- package/src/core/tools/{whatsapp.ts → chat/whatsapp.ts} +1 -1
- package/src/core/tools/content/memory.ts +128 -0
- package/src/core/tools/{web.ts → content/web.ts} +1 -1
- package/src/core/tools/index.ts +21 -19
- package/src/core/tools/{admin.ts → ops/admin.ts} +1 -1
- package/src/core/tools/{bridge.ts → ops/bridge.ts} +2 -2
- package/src/core/tools/{goals.ts → ops/goals.ts} +2 -2
- package/src/core/tools/{mesh.ts → ops/mesh.ts} +1 -1
- package/src/core/tools/{models.ts → ops/models.ts} +1 -1
- package/src/core/tools/{native.ts → ops/native.ts} +1 -1
- package/src/core/tools/{scheduling.ts → ops/scheduling.ts} +1 -1
- package/src/core/tools/{scripts.ts → ops/scripts.ts} +1 -1
- package/src/core/tools/{skills.ts → ops/skills.ts} +1 -1
- package/src/core/tools/{triggers.ts → ops/triggers.ts} +1 -1
- package/src/core/tools/types.ts +1 -0
- package/src/{util → core/vfs}/workspace.ts +2 -2
- package/src/core/weaver/turn-cpu.ts +32 -0
- package/src/core/weaver/weaver.ts +36 -0
- package/src/frontend/discord/admin.ts +1 -1
- package/src/frontend/discord/callbacks/components/backend-select.ts +1 -1
- package/src/frontend/discord/callbacks/components/effort.ts +1 -1
- package/src/frontend/discord/callbacks/components/index.ts +1 -1
- package/src/frontend/discord/callbacks/components/pulse.ts +1 -1
- package/src/frontend/discord/callbacks/components/settings.ts +2 -2
- package/src/frontend/discord/callbacks/modals.ts +4 -1
- package/src/frontend/discord/commands/admin.ts +2 -36
- package/src/frontend/discord/commands/definitions.ts +0 -11
- package/src/frontend/discord/commands/info.ts +1 -1
- package/src/frontend/discord/commands/interaction.ts +1 -1
- package/src/frontend/discord/commands/router.ts +0 -3
- package/src/frontend/discord/commands/session.ts +8 -2
- package/src/frontend/discord/commands/settings.ts +2 -2
- package/src/frontend/discord/handlers/messages.ts +1 -1
- package/src/frontend/discord/handlers/state.ts +1 -1
- package/src/frontend/discord/middleware.ts +2 -26
- package/src/frontend/discord/ready.ts +1 -1
- package/src/frontend/discord/render.ts +41 -259
- package/src/frontend/discord/runtime.ts +1 -2
- package/src/frontend/native/{auth.ts → bridge/auth.ts} +2 -2
- package/src/frontend/native/{discovery.ts → bridge/discovery.ts} +3 -3
- package/src/frontend/native/{routes → bridge/routes}/chats.ts +1 -1
- package/src/frontend/native/{routes → bridge/routes}/daemon.ts +1 -1
- package/src/frontend/native/{routes → bridge/routes}/host.ts +6 -3
- package/src/frontend/native/{routes → bridge/routes}/pre-auth.ts +1 -1
- package/src/frontend/native/{server.ts → bridge/server.ts} +3 -3
- package/src/frontend/native/{tls.ts → bridge/tls.ts} +2 -2
- package/src/frontend/native/{chat-lifecycle.ts → chats/chat-lifecycle.ts} +3 -3
- package/src/frontend/native/{chat-wire.ts → chats/chat-wire.ts} +4 -4
- package/src/frontend/native/{chats.ts → chats/chats.ts} +4 -4
- package/src/frontend/native/{empty-chat-sweep.ts → chats/empty-chat-sweep.ts} +4 -4
- package/src/frontend/native/{history.ts → chats/history.ts} +7 -7
- package/src/frontend/native/{reset.ts → chats/reset.ts} +7 -7
- package/src/frontend/native/index.ts +13 -10
- package/src/frontend/native/{media.ts → media/media.ts} +3 -3
- package/src/frontend/native/runtime.ts +1 -1
- package/src/frontend/native/{control.ts → surface/control.ts} +4 -4
- package/src/frontend/native/{extensions.ts → surface/extensions.ts} +12 -9
- package/src/frontend/native/{handlers.ts → surface/handlers.ts} +19 -14
- package/src/frontend/native/{logs.ts → surface/logs.ts} +2 -2
- package/src/frontend/native/{memory.ts → surface/memory.ts} +2 -2
- package/src/frontend/native/{models.ts → surface/models.ts} +17 -10
- package/src/frontend/native/{settings.ts → surface/settings.ts} +9 -9
- package/src/frontend/native/{status.ts → surface/status.ts} +3 -3
- package/src/frontend/native/{actions.ts → turn/actions.ts} +7 -4
- package/src/frontend/native/{context.ts → turn/context.ts} +8 -8
- package/src/frontend/native/{emit.ts → turn/emit.ts} +7 -7
- package/src/frontend/native/{queue.ts → turn/queue.ts} +3 -3
- package/src/frontend/native/{turn-meta.ts → turn/turn-meta.ts} +2 -2
- package/src/frontend/native/{turn.ts → turn/turn.ts} +9 -9
- package/src/frontend/{shared → presentation}/format.ts +4 -3
- package/src/frontend/{shared → presentation}/model-commands.ts +1 -1
- package/src/frontend/{shared → presentation}/reasoning-levels.ts +12 -0
- package/src/frontend/presentation/reports.ts +493 -0
- package/src/frontend/{shared → presentation}/session-status.ts +36 -9
- package/src/frontend/{shared → presentation}/status-context.ts +101 -2
- package/src/frontend/teams/commands.ts +2 -2
- package/src/frontend/teams/turn.ts +1 -1
- package/src/frontend/telegram/admin/background.ts +1 -1
- package/src/frontend/telegram/admin/health.ts +1 -1
- package/src/frontend/telegram/admin/sessions.ts +1 -1
- package/src/frontend/telegram/callbacks/effort.ts +2 -2
- package/src/frontend/telegram/callbacks/metrics.ts +1 -1
- package/src/frontend/telegram/callbacks/model/backend.ts +1 -1
- package/src/frontend/telegram/callbacks/model/views.ts +1 -1
- package/src/frontend/telegram/callbacks/pulse.ts +1 -1
- package/src/frontend/telegram/callbacks/query.ts +1 -1
- package/src/frontend/telegram/callbacks/settings.ts +3 -3
- package/src/frontend/telegram/commands/admin.ts +6 -41
- package/src/frontend/telegram/commands/index.ts +1 -1
- package/src/frontend/telegram/commands/info.ts +2 -2
- package/src/frontend/telegram/commands/session.ts +9 -3
- package/src/frontend/telegram/commands/settings.ts +5 -3
- package/src/frontend/telegram/handlers/messages.ts +0 -10
- package/src/frontend/telegram/handlers/state.ts +1 -1
- package/src/frontend/telegram/index.ts +1 -5
- package/src/frontend/telegram/middleware.ts +1 -15
- package/src/frontend/telegram/model-menu.ts +1 -1
- package/src/frontend/telegram/render/html.ts +30 -0
- package/src/frontend/telegram/{helpers → render}/menu.ts +46 -19
- package/src/frontend/telegram/render/reports.ts +182 -0
- package/src/frontend/terminal/builtins/context.ts +2 -2
- package/src/frontend/terminal/builtins/session.ts +1 -1
- package/src/frontend/terminal/builtins/status.ts +2 -2
- package/src/frontend/terminal/index.ts +2 -2
- package/src/frontend/terminal/renderer.ts +1 -1
- package/src/frontend/whatsapp/access.ts +2 -2
- package/src/frontend/whatsapp/actions/history.ts +2 -2
- package/src/frontend/whatsapp/actions/messaging.ts +2 -2
- package/src/frontend/whatsapp/actions/moderation.ts +1 -1
- package/src/frontend/whatsapp/actions/send.ts +1 -1
- package/src/frontend/whatsapp/commands.ts +12 -6
- package/src/frontend/whatsapp/{connection.ts → connection/connection.ts} +5 -5
- package/src/frontend/whatsapp/{pairing-service.ts → connection/pairing-service.ts} +3 -3
- package/src/frontend/whatsapp/{wa-logger.ts → connection/wa-logger.ts} +2 -2
- package/src/frontend/whatsapp/index.ts +4 -4
- package/src/frontend/whatsapp/{inbound.ts → messages/inbound.ts} +15 -15
- package/src/frontend/whatsapp/{media-store.ts → messages/media-store.ts} +4 -4
- package/src/frontend/whatsapp/{message-store.ts → messages/message-store.ts} +1 -1
- package/src/frontend/whatsapp/{turn-recovery.ts → messages/turn-recovery.ts} +4 -4
- package/src/frontend/whatsapp/registry.ts +2 -2
- package/src/frontend/whatsapp/runtime.ts +1 -1
- package/src/index.ts +6 -3
- package/src/storage/db.ts +6 -1
- package/src/storage/memory.ts +59 -8
- package/src/storage/metrics.ts +38 -0
- package/src/storage/repositories/sessions-repo.ts +6 -0
- package/src/storage/session-record.ts +13 -2
- package/src/storage/sessions.ts +12 -0
- package/src/storage/sql/db.sql +5 -0
- package/src/storage/sql/schema.sql +4 -0
- package/src/storage/sql/sessions.sql +3 -3
- package/src/storage/sql/statements.generated.ts +10 -3
- package/src/storage/sql/turn-meta.sql +1 -1
- package/src/util/log.ts +1 -1
- package/src/util/paths.ts +0 -2
- package/src/util/runtime.ts +1 -1
- package/src/core/engine/gateway-actions/native.ts +0 -1035
- package/src/core/soul/README.md +0 -110
- package/src/core/soul/RESEARCH.md +0 -98
- package/src/core/soul/associative.ts +0 -98
- package/src/core/soul/centrality.ts +0 -98
- package/src/core/soul/cluster.ts +0 -83
- package/src/core/soul/compiler.ts +0 -207
- package/src/core/soul/consolidate.ts +0 -179
- package/src/core/soul/critic.ts +0 -162
- package/src/core/soul/dag.ts +0 -265
- package/src/core/soul/delta.ts +0 -123
- package/src/core/soul/drift.ts +0 -99
- package/src/core/soul/embedder.ts +0 -129
- package/src/core/soul/emergent-critic.ts +0 -96
- package/src/core/soul/forgetting.ts +0 -131
- package/src/core/soul/governance.ts +0 -93
- package/src/core/soul/hash.ts +0 -97
- package/src/core/soul/hdc.ts +0 -154
- package/src/core/soul/kernel.ts +0 -540
- package/src/core/soul/lattice.ts +0 -103
- package/src/core/soul/lens.ts +0 -110
- package/src/core/soul/projector.ts +0 -240
- package/src/core/soul/reflect.ts +0 -170
- package/src/core/soul/reflex.ts +0 -164
- package/src/core/soul/retrieve.ts +0 -146
- package/src/core/soul/salience.ts +0 -146
- package/src/core/soul/service.ts +0 -204
- package/src/core/soul/settings.ts +0 -47
- package/src/core/soul/signals.ts +0 -117
- package/src/core/soul/talon-embedder.ts +0 -80
- package/src/core/soul/taps.ts +0 -199
- package/src/core/soul/types.ts +0 -298
- package/src/core/soul/valence.ts +0 -83
- package/src/frontend/telegram/helpers/diagnostics.ts +0 -428
- package/src/frontend/telegram/helpers/format.ts +0 -42
- package/src/frontend/telegram/helpers/index.ts +0 -13
- package/src/util/cleanup-registry.ts +0 -36
- /package/src/backend/{shared → runtime}/frontends.ts +0 -0
- /package/src/backend/{shared → runtime}/metrics.ts +0 -0
- /package/src/backend/{shared → runtime}/sleep.ts +0 -0
- /package/src/backend/{shared → runtime/turn}/flow-violation.ts +0 -0
- /package/src/backend/{shared → runtime}/usage.ts +0 -0
- /package/src/{util/mcp-launcher.ts → core/mcp-hub/launcher.ts} +0 -0
- /package/src/{util → core/tools/content}/web-content.ts +0 -0
- /package/src/core/tools/{mcp-env.ts → ops/mcp-env.ts} +0 -0
- /package/src/{util → core/weaver}/session-name.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/index.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/memory.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/mesh.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/models.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/params.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/table.ts +0 -0
- /package/src/frontend/native/{tool-result.ts → turn/tool-result.ts} +0 -0
- /package/src/frontend/{shared → presentation}/access.ts +0 -0
- /package/src/frontend/{shared → presentation}/plan-usage-report.ts +0 -0
- /package/src/frontend/whatsapp/{auth-state.ts → connection/auth-state.ts} +0 -0
- /package/src/frontend/whatsapp/{identity.ts → connection/identity.ts} +0 -0
- /package/src/frontend/whatsapp/{pairing-lock.ts → connection/pairing-lock.ts} +0 -0
- /package/src/frontend/whatsapp/{pairing.ts → connection/pairing.ts} +0 -0
- /package/src/frontend/whatsapp/{pins.ts → messages/pins.ts} +0 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory — the in-band write path over the typed store: remember /
|
|
3
|
+
* recall / forget (docs/memory-persona-plan.md §3.2).
|
|
4
|
+
*
|
|
5
|
+
* Three rules shape this module, and none of them live in the store:
|
|
6
|
+
*
|
|
7
|
+
* - **Single claim, supersede-candidate.** A `remember` that restates
|
|
8
|
+
* something already on file does NOT insert a second row. The FTS
|
|
9
|
+
* near-duplicate probe runs first, and a hit comes back as a refusal
|
|
10
|
+
* naming the rows to supersede. The caller then either picks one
|
|
11
|
+
* (`replace_id`) or declares the claim genuinely separate (`force`).
|
|
12
|
+
* - **Trust comes from the chat, not the caller.** Anything learned on
|
|
13
|
+
* a multi-party surface is `group_chat` — never pinnable, never in
|
|
14
|
+
* the core view (plan §5) — and a `directive` cannot be planted from
|
|
15
|
+
* one at all. The tier also gates what a context may *overwrite*:
|
|
16
|
+
* a claim may only be superseded or forgotten from a context at its
|
|
17
|
+
* own tier or stronger. Without that, both `replace_id` and `forget`
|
|
18
|
+
* are escalations — the first because `supersedeMemory` copies the
|
|
19
|
+
* old row's trust onto the successor, the second because the store
|
|
20
|
+
* drops whatever id it is handed.
|
|
21
|
+
* - **Writes never invalidate the prompt cache.** This module must not
|
|
22
|
+
* import `core/prompt/invalidation.js`. A `remember` that dropped
|
|
23
|
+
* every live session's prompt snapshot would turn a ~50-token claim
|
|
24
|
+
* into a 60–90 k cache write (plan §3.6). A fact learned mid-session
|
|
25
|
+
* reaches the model through turn retrieval, not the frozen core view.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
assertMemory,
|
|
30
|
+
dropMemory,
|
|
31
|
+
findSimilarMemories,
|
|
32
|
+
formatMemory,
|
|
33
|
+
getMemory,
|
|
34
|
+
isMemoryKind,
|
|
35
|
+
replaceStateKey,
|
|
36
|
+
searchMemories,
|
|
37
|
+
supersedeMemory,
|
|
38
|
+
touchMemory,
|
|
39
|
+
MEMORY_KINDS,
|
|
40
|
+
type MemoryKind,
|
|
41
|
+
type MemoryRow,
|
|
42
|
+
type MemorySource,
|
|
43
|
+
type MemoryTrust,
|
|
44
|
+
} from "../../../storage/memory.js";
|
|
45
|
+
import { chatScope } from "../../frontend-runtime/chat-id.js";
|
|
46
|
+
import { log } from "../../../util/log.js";
|
|
47
|
+
import type { ActionResult } from "../../types.js";
|
|
48
|
+
import type { SharedActionHandlers } from "./types.js";
|
|
49
|
+
|
|
50
|
+
/** `source.actor` on every row this path writes — the ownership marker. */
|
|
51
|
+
const REMEMBER_ACTOR = "remember";
|
|
52
|
+
|
|
53
|
+
/** Kinds whose subject is the chat itself when the caller names none. */
|
|
54
|
+
const CHAT_SCOPED_KINDS: readonly MemoryKind[] = ["episode", "relationship"];
|
|
55
|
+
|
|
56
|
+
/** Hard ceiling on `recall`, regardless of what the caller asks for. */
|
|
57
|
+
const MAX_RECALL_LIMIT = 20;
|
|
58
|
+
|
|
59
|
+
const KIND_LIST = MEMORY_KINDS.join(", ");
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The trust tier for a claim learned in this chat.
|
|
63
|
+
*
|
|
64
|
+
* A group chat is a multi-party surface: any participant can assert
|
|
65
|
+
* anything, so what is learned there is `group_chat` — a tier the store
|
|
66
|
+
* refuses to pin and the core view never carries (plan §5). A DM with
|
|
67
|
+
* the operator, or a local terminal/native session, is the agent writing
|
|
68
|
+
* for its own principal, so that is `agent`.
|
|
69
|
+
*
|
|
70
|
+
* "Group" is read off the canonical chat-id grammar (`chatScope` in
|
|
71
|
+
* core/frontend-runtime/chat-id.ts), which is the only identity a gateway action holds.
|
|
72
|
+
* When the grammar cannot tell — Teams' `teams_chat_…` is 1:1 and group
|
|
73
|
+
* alike — this fails closed to `group_chat`: over-restricting a claim
|
|
74
|
+
* costs a pin, under-restricting one is a permanent prompt injection.
|
|
75
|
+
*/
|
|
76
|
+
function trustForChat(chatKey: string): MemoryTrust {
|
|
77
|
+
return chatScope(chatKey) === "dm" ? "agent" : "group_chat";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The trust tiers, weakest first. */
|
|
81
|
+
const TRUST_ORDER: readonly MemoryTrust[] = [
|
|
82
|
+
"group_chat",
|
|
83
|
+
"user_claim",
|
|
84
|
+
"agent",
|
|
85
|
+
"operator",
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
function trustRank(trust: MemoryTrust): number {
|
|
89
|
+
const rank = TRUST_ORDER.indexOf(trust);
|
|
90
|
+
// A tier this module does not know is treated as the strongest, so an
|
|
91
|
+
// unrecognised row is protected rather than freely overwritten.
|
|
92
|
+
return rank === -1 ? TRUST_ORDER.length : rank;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether a claim made in `context` may overwrite or retire a row
|
|
97
|
+
* recorded at `row` trust — its own tier or weaker, never stronger.
|
|
98
|
+
*
|
|
99
|
+
* This is what keeps `replace_id` from being an escalation:
|
|
100
|
+
* `supersedeMemory` inherits the old row's frame, trust included, so
|
|
101
|
+
* without this check a group participant could hand new text to an
|
|
102
|
+
* `agent` row and have it land live at `agent` trust. `forget` needs the
|
|
103
|
+
* same guard for the mirror reason — the store drops whatever id it is
|
|
104
|
+
* given, so the policy has to sit here.
|
|
105
|
+
*/
|
|
106
|
+
function outranks(context: MemoryTrust, row: MemoryTrust): boolean {
|
|
107
|
+
return trustRank(context) >= trustRank(row);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function fail(error: string): ActionResult {
|
|
111
|
+
return { ok: false, error };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Never let a store error escape as a throw — the gateway wants a result. */
|
|
115
|
+
function attempt(run: () => ActionResult): ActionResult {
|
|
116
|
+
try {
|
|
117
|
+
return run();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function storedLine(id: number): ActionResult {
|
|
124
|
+
const row = getMemory(id);
|
|
125
|
+
return { ok: true, id, line: row ? formatMemory(row) : `#${id}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Clamp a caller-supplied limit into 1..max, defaulting to max. */
|
|
129
|
+
function clampLimit(raw: unknown, max: number): number {
|
|
130
|
+
const n = Number(raw);
|
|
131
|
+
if (!Number.isFinite(n) || n < 1) return max;
|
|
132
|
+
return Math.min(Math.floor(n), max);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `episode` / `relationship` default to the chat; everything else must say. */
|
|
136
|
+
function resolveSubject(
|
|
137
|
+
kind: MemoryKind,
|
|
138
|
+
raw: unknown,
|
|
139
|
+
chatKey: string,
|
|
140
|
+
): string {
|
|
141
|
+
const given = raw === undefined ? "" : String(raw).trim();
|
|
142
|
+
if (given) return given;
|
|
143
|
+
return CHAT_SCOPED_KINDS.includes(kind) ? chatKey : "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The parts of a `remember` body that survive validation. */
|
|
147
|
+
type RememberClaim = {
|
|
148
|
+
kind: MemoryKind;
|
|
149
|
+
subject: string;
|
|
150
|
+
text: string;
|
|
151
|
+
trust: MemoryTrust;
|
|
152
|
+
source: MemorySource;
|
|
153
|
+
confidence?: number;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function parseClaim(
|
|
157
|
+
body: Record<string, unknown>,
|
|
158
|
+
chatKey: string,
|
|
159
|
+
): RememberClaim | ActionResult {
|
|
160
|
+
const kind = body.kind;
|
|
161
|
+
if (!isMemoryKind(kind))
|
|
162
|
+
return fail(
|
|
163
|
+
`Unknown kind "${String(kind ?? "")}" (expected one of ${KIND_LIST})`,
|
|
164
|
+
);
|
|
165
|
+
const text = String(body.text ?? "").trim();
|
|
166
|
+
if (!text) return fail("Missing text — a memory is one written claim");
|
|
167
|
+
const subject = resolveSubject(kind, body.subject, chatKey);
|
|
168
|
+
if (!subject)
|
|
169
|
+
return fail(`A ${kind} memory needs a subject (who or what it is about)`);
|
|
170
|
+
const trust = trustForChat(chatKey);
|
|
171
|
+
if (kind === "directive" && trust !== "agent")
|
|
172
|
+
return fail(
|
|
173
|
+
"A directive cannot be recorded from a group chat — durable standing " +
|
|
174
|
+
"intent only comes from a direct conversation with the operator. " +
|
|
175
|
+
"Record it as a fact instead, or ask the operator in a DM.",
|
|
176
|
+
);
|
|
177
|
+
const confidence =
|
|
178
|
+
body.confidence === undefined ? undefined : Number(body.confidence);
|
|
179
|
+
return {
|
|
180
|
+
kind,
|
|
181
|
+
subject,
|
|
182
|
+
text,
|
|
183
|
+
trust,
|
|
184
|
+
source: { chat: chatKey, actor: REMEMBER_ACTOR },
|
|
185
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The keyed-state path: a write replaces the live row for that key. */
|
|
190
|
+
function rememberState(
|
|
191
|
+
body: Record<string, unknown>,
|
|
192
|
+
claim: RememberClaim,
|
|
193
|
+
): ActionResult {
|
|
194
|
+
if (body.replace_id !== undefined)
|
|
195
|
+
return fail(
|
|
196
|
+
"A state memory replaces the live row for its key — omit replace_id",
|
|
197
|
+
);
|
|
198
|
+
const key = String(body.key ?? "").trim();
|
|
199
|
+
if (!key)
|
|
200
|
+
return fail("A state memory requires a key (e.g. heartbeat.health)");
|
|
201
|
+
const id = replaceStateKey(key, claim.text, claim.source, {
|
|
202
|
+
subject: claim.subject,
|
|
203
|
+
trust: claim.trust,
|
|
204
|
+
...(claim.confidence !== undefined ? { confidence: claim.confidence } : {}),
|
|
205
|
+
reason: "remember: state",
|
|
206
|
+
});
|
|
207
|
+
return storedLine(id);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The explicit supersede: fold the new text into an existing live row. */
|
|
211
|
+
function rememberReplacing(
|
|
212
|
+
replaceId: number,
|
|
213
|
+
claim: RememberClaim,
|
|
214
|
+
): ActionResult {
|
|
215
|
+
const target = getMemory(replaceId);
|
|
216
|
+
if (!target) return fail(`No memory with id ${replaceId}`);
|
|
217
|
+
if (target.droppedAt !== undefined)
|
|
218
|
+
return fail(`Memory #${replaceId} is dropped; it cannot be superseded`);
|
|
219
|
+
if (target.supersededBy !== undefined)
|
|
220
|
+
return fail(
|
|
221
|
+
`Memory #${replaceId} is already superseded by #${target.supersededBy}`,
|
|
222
|
+
);
|
|
223
|
+
if (!outranks(claim.trust, target.trust))
|
|
224
|
+
return fail(
|
|
225
|
+
`Memory #${replaceId} was recorded at ${target.trust} trust; a ${claim.trust} context cannot replace it — record a separate claim instead`,
|
|
226
|
+
);
|
|
227
|
+
if (target.kind !== claim.kind)
|
|
228
|
+
return fail(
|
|
229
|
+
`Memory #${replaceId} is a ${target.kind}, not a ${claim.kind} — a supersede keeps the row's kind`,
|
|
230
|
+
);
|
|
231
|
+
return storedLine(
|
|
232
|
+
supersedeMemory(replaceId, claim.text, "remember: replaced"),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The refusal that offers supersede-instead-of-append (plan §3.2). */
|
|
237
|
+
function nearDuplicate(similar: MemoryRow[]): ActionResult {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: `Near-duplicate of #${similar[0]!.id}`,
|
|
241
|
+
similar: similar.map((row) => formatMemory(row)),
|
|
242
|
+
hint: "pass replace_id to supersede it, or force: true to store a separate claim",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function rememberClaim(
|
|
247
|
+
body: Record<string, unknown>,
|
|
248
|
+
chatKey: string,
|
|
249
|
+
): ActionResult {
|
|
250
|
+
const parsed = parseClaim(body, chatKey);
|
|
251
|
+
if ("ok" in parsed) return parsed;
|
|
252
|
+
if (parsed.kind === "state") return rememberState(body, parsed);
|
|
253
|
+
|
|
254
|
+
if (body.replace_id !== undefined) {
|
|
255
|
+
const replaceId = Number(body.replace_id);
|
|
256
|
+
if (!Number.isInteger(replaceId) || replaceId <= 0)
|
|
257
|
+
return fail(`Invalid replace_id "${String(body.replace_id)}"`);
|
|
258
|
+
return rememberReplacing(replaceId, parsed);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (body.force !== true) {
|
|
262
|
+
const similar = findSimilarMemories(
|
|
263
|
+
parsed.kind,
|
|
264
|
+
parsed.subject,
|
|
265
|
+
parsed.text,
|
|
266
|
+
);
|
|
267
|
+
if (similar.length > 0) return nearDuplicate(similar);
|
|
268
|
+
}
|
|
269
|
+
const { id } = assertMemory(parsed);
|
|
270
|
+
log("gateway", `remember: [${parsed.kind}] ${parsed.subject} #${id}`);
|
|
271
|
+
return storedLine(id);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function recallRows(body: Record<string, unknown>): ActionResult {
|
|
275
|
+
const query = String(body.query ?? "").trim();
|
|
276
|
+
if (!query) return fail("Missing query");
|
|
277
|
+
const kind = body.kind;
|
|
278
|
+
if (kind !== undefined && !isMemoryKind(kind))
|
|
279
|
+
return fail(
|
|
280
|
+
`Unknown kind "${String(kind)}" (expected one of ${KIND_LIST})`,
|
|
281
|
+
);
|
|
282
|
+
const rows = searchMemories(query, {
|
|
283
|
+
...(kind !== undefined ? { kind } : {}),
|
|
284
|
+
limit: clampLimit(body.limit, MAX_RECALL_LIMIT),
|
|
285
|
+
});
|
|
286
|
+
if (rows.length === 0)
|
|
287
|
+
return { ok: true, rows: [], note: "nothing stored matches" };
|
|
288
|
+
// A retrieval hit is ranking feedback, not a content change: `touchMemory`
|
|
289
|
+
// writes no history row, and nothing here invalidates the prompt cache.
|
|
290
|
+
for (const row of rows) touchMemory(row.id);
|
|
291
|
+
return { ok: true, rows: rows.map((row) => formatMemory(row)) };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Why a context is not allowed to retire this row. */
|
|
295
|
+
function forgetRefusal(
|
|
296
|
+
id: number,
|
|
297
|
+
rowTrust: MemoryTrust,
|
|
298
|
+
context: MemoryTrust,
|
|
299
|
+
): string {
|
|
300
|
+
// No chat context ever reaches `operator`, so an operator row can only
|
|
301
|
+
// be retired out of band — name the command rather than stonewalling.
|
|
302
|
+
if (rowTrust === "operator")
|
|
303
|
+
return `Memory #${id} is an operator memory — only the operator can forget it (talon memory forget ${id})`;
|
|
304
|
+
return `Memory #${id} was recorded at ${rowTrust} trust; a ${context} context cannot forget it`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function forgetRow(
|
|
308
|
+
body: Record<string, unknown>,
|
|
309
|
+
chatKey: string,
|
|
310
|
+
): ActionResult {
|
|
311
|
+
const id = Number(body.id);
|
|
312
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
313
|
+
return fail(`Invalid id "${String(body.id ?? "")}"`);
|
|
314
|
+
const reason = String(body.reason ?? "").trim();
|
|
315
|
+
if (!reason)
|
|
316
|
+
return fail(
|
|
317
|
+
"A reason is required — every drop is auditable and reversible",
|
|
318
|
+
);
|
|
319
|
+
const row = getMemory(id);
|
|
320
|
+
if (!row) return fail(`No memory with id ${id}`);
|
|
321
|
+
const context = trustForChat(chatKey);
|
|
322
|
+
if (!outranks(context, row.trust))
|
|
323
|
+
return fail(forgetRefusal(id, row.trust, context));
|
|
324
|
+
dropMemory(id, reason);
|
|
325
|
+
log("gateway", `forget: #${id} (${reason})`);
|
|
326
|
+
return { ok: true, id, text: `Dropped #${id} to the graveyard: ${reason}` };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export const memoryHandlers: SharedActionHandlers = {
|
|
330
|
+
remember: (body, _chatId, _backend, chatKey) =>
|
|
331
|
+
attempt(() => rememberClaim(body, chatKey)),
|
|
332
|
+
recall: (body) => attempt(() => recallRows(body)),
|
|
333
|
+
forget: (body, _chatId, _backend, chatKey) =>
|
|
334
|
+
attempt(() => forgetRow(body, chatKey)),
|
|
335
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `native_bash` with background:true — the detached local launch.
|
|
3
|
+
*
|
|
4
|
+
* POSIX-only by contract (own process group, `kill -- -pid` to stop, survives
|
|
5
|
+
* a daemon restart), with a short settle window so a command that dies
|
|
6
|
+
* immediately still reports like a normal foreground run.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { closeSync, openSync } from "node:fs";
|
|
11
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { renderExec, type Result } from "./results.js";
|
|
15
|
+
|
|
16
|
+
/** How long a background launch waits to catch fast failures. */
|
|
17
|
+
const BACKGROUND_SETTLE_MS = 1_200;
|
|
18
|
+
/** Where background job output lands (one log file per job). */
|
|
19
|
+
const BACKGROUND_LOG_DIR = join(tmpdir(), "talon-bash");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Launch a command detached from the request cycle: its own process group,
|
|
23
|
+
* stdout+stderr appended to a per-job log file, tool returns immediately.
|
|
24
|
+
* This is the sanctioned path for streaming/long-running commands (adb
|
|
25
|
+
* logcat, dev servers, watchers) that would otherwise burn the whole
|
|
26
|
+
* foreground timeout and come back "killed".
|
|
27
|
+
*
|
|
28
|
+
* A short settle window catches fast failures (typo'd binary, instant
|
|
29
|
+
* non-zero exit) so those still surface as a normal error instead of a
|
|
30
|
+
* "started" message pointing at a log with one line in it.
|
|
31
|
+
*/
|
|
32
|
+
export async function bashBackground(
|
|
33
|
+
cmd: string,
|
|
34
|
+
cwd: string | undefined,
|
|
35
|
+
): Promise<Result> {
|
|
36
|
+
// The background contract is POSIX-shaped end to end: detached process
|
|
37
|
+
// group, `kill -- -pid` to stop, survives daemon restarts. Windows has
|
|
38
|
+
// none of those (and the CI legs showed the detached writer's output not
|
|
39
|
+
// reaching the log) — refuse loudly with the native alternative instead
|
|
40
|
+
// of pretending.
|
|
41
|
+
if (process.platform === "win32") {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
text:
|
|
45
|
+
"background:true needs POSIX process groups and isn't supported on a Windows " +
|
|
46
|
+
"daemon host. Run it foreground with a bound command (`timeout 30 …`, `head -n 200`) " +
|
|
47
|
+
"or start it yourself: `powershell Start-Process -WindowStyle Hidden` with output redirected to a file.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await mkdir(BACKGROUND_LOG_DIR, { recursive: true });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
text: `Cannot create log dir ${BACKGROUND_LOG_DIR}: ${(err as Error).message}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const slug =
|
|
59
|
+
cmd
|
|
60
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
61
|
+
.replace(/^-+|-+$/g, "")
|
|
62
|
+
.slice(0, 40) || "job";
|
|
63
|
+
const logPath = join(BACKGROUND_LOG_DIR, `${Date.now()}-${slug}.log`);
|
|
64
|
+
let fd: number;
|
|
65
|
+
try {
|
|
66
|
+
fd = openSync(logPath, "a");
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
text: `Cannot open log file ${logPath}: ${(err as Error).message}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Always detached: the win32 guard above returned already, so this only
|
|
74
|
+
// runs on POSIX where the job gets its own process group.
|
|
75
|
+
const child = spawn("bash", ["-c", cmd], {
|
|
76
|
+
...(cwd ? { cwd } : {}),
|
|
77
|
+
env: process.env,
|
|
78
|
+
detached: true,
|
|
79
|
+
stdio: ["ignore", fd, fd],
|
|
80
|
+
});
|
|
81
|
+
return new Promise((resolvePromise) => {
|
|
82
|
+
let settled = false;
|
|
83
|
+
const done = (r: Result) => {
|
|
84
|
+
if (settled) return;
|
|
85
|
+
settled = true;
|
|
86
|
+
try {
|
|
87
|
+
closeSync(fd);
|
|
88
|
+
} catch {
|
|
89
|
+
// parent's dup only; the child keeps its own copy either way
|
|
90
|
+
}
|
|
91
|
+
resolvePromise(r);
|
|
92
|
+
};
|
|
93
|
+
child.on("error", (err) =>
|
|
94
|
+
done({ ok: false, text: `Failed to start: ${err.message}` }),
|
|
95
|
+
);
|
|
96
|
+
// Fast failure inside the settle window → report it like a normal run.
|
|
97
|
+
child.on("close", (code) => {
|
|
98
|
+
void (async () => {
|
|
99
|
+
let logged = "";
|
|
100
|
+
try {
|
|
101
|
+
logged = await readFile(logPath, "utf8");
|
|
102
|
+
} catch {
|
|
103
|
+
// log unreadable — report the exit alone
|
|
104
|
+
}
|
|
105
|
+
done({
|
|
106
|
+
ok: (code ?? 0) === 0,
|
|
107
|
+
text:
|
|
108
|
+
`Background command exited almost immediately (exit ${code ?? 0}).\n` +
|
|
109
|
+
renderExec("local", `exit ${code ?? 0}`, logged, "") +
|
|
110
|
+
`\nFull log: ${logPath}`,
|
|
111
|
+
});
|
|
112
|
+
})();
|
|
113
|
+
});
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
if (settled) return;
|
|
116
|
+
child.unref();
|
|
117
|
+
done({
|
|
118
|
+
ok: true,
|
|
119
|
+
text: [
|
|
120
|
+
`🚀 Started in background [local] — pid ${child.pid}.`,
|
|
121
|
+
`Output (stdout+stderr) → ${logPath}`,
|
|
122
|
+
`Follow it with read/bash (e.g. \`tail -n 50 ${logPath}\`).`,
|
|
123
|
+
`Stop it with \`kill -- -${child.pid}\` (whole process group).`,
|
|
124
|
+
`Unsupervised: it keeps running until it exits or is killed — it even survives a Talon restart.`,
|
|
125
|
+
].join("\n"),
|
|
126
|
+
});
|
|
127
|
+
}, BACKGROUND_SETTLE_MS);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `native_bash` on a teleported device — dispatched over the mesh exec
|
|
3
|
+
* channel.
|
|
4
|
+
*
|
|
5
|
+
* The command is wrapped so the shell's resulting working directory comes
|
|
6
|
+
* back in markers and persists across calls (a `cd` carries forward) without
|
|
7
|
+
* disturbing the real exit code. glob/search reuse this path for their
|
|
8
|
+
* on-device rg/find/grep fallbacks.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getMeshService } from "../../../mesh/index.js";
|
|
12
|
+
import { getTeleport, setTeleportCwd } from "../../../mesh/teleport.js";
|
|
13
|
+
import { renderExec, type Result } from "./results.js";
|
|
14
|
+
import { shellQuote } from "./shell.js";
|
|
15
|
+
|
|
16
|
+
/** Same self-correction hint, phrased for a teleported (on-device) run. */
|
|
17
|
+
const TELEPORT_TIMEOUT_HINT =
|
|
18
|
+
"(Streaming/never-ending commands can't ride a teleported foreground call — " +
|
|
19
|
+
"background it in-shell instead: `cmd > /tmp/out.log 2>&1 &`, then poll the log " +
|
|
20
|
+
"with read, or bound the command itself: `logcat -d`, `timeout 30 …`, `head -n 200`.)";
|
|
21
|
+
/** Markers used to recover the post-command working dir from a teleport shell. */
|
|
22
|
+
const CWD_OPEN = "__TALON_CWD__";
|
|
23
|
+
const CWD_CLOSE = "__TALON_CWD_END__";
|
|
24
|
+
|
|
25
|
+
export async function bashTeleported(
|
|
26
|
+
chatId: number,
|
|
27
|
+
deviceId: string,
|
|
28
|
+
cmd: string,
|
|
29
|
+
timeoutMs: number,
|
|
30
|
+
): Promise<Result> {
|
|
31
|
+
const active = await getTeleport(chatId);
|
|
32
|
+
const cwd = active?.cwd;
|
|
33
|
+
// Wrap so the resulting working dir is reported back and persists across
|
|
34
|
+
// calls (a `cd` in `cmd` carries forward), while the real exit code is
|
|
35
|
+
// preserved. printf can't fail in a way that masks the command's status.
|
|
36
|
+
const wrapped =
|
|
37
|
+
`${cwd ? `cd ${shellQuote(cwd)} 2>/dev/null; ` : ""}` +
|
|
38
|
+
`{ ${cmd}\n}; __talon_rc=$?; ` +
|
|
39
|
+
`printf '${CWD_OPEN}%s${CWD_CLOSE}' "$(pwd 2>/dev/null)"; exit $__talon_rc`;
|
|
40
|
+
const dispatched = await getMeshService().dispatchCommand(
|
|
41
|
+
deviceId,
|
|
42
|
+
"exec",
|
|
43
|
+
{ cmd: wrapped, timeoutMs },
|
|
44
|
+
timeoutMs + 5_000,
|
|
45
|
+
);
|
|
46
|
+
if ("error" in dispatched) return { ok: false, text: dispatched.error };
|
|
47
|
+
const { target, result } = dispatched;
|
|
48
|
+
const data = result.data ?? {};
|
|
49
|
+
let stdout = typeof data.stdout === "string" ? data.stdout : "";
|
|
50
|
+
const stderr = typeof data.stderr === "string" ? data.stderr : "";
|
|
51
|
+
const via =
|
|
52
|
+
typeof data.via === "string" && data.via ? ` via ${data.via}` : "";
|
|
53
|
+
const exitCode =
|
|
54
|
+
typeof data.exitCode === "number" ? data.exitCode : undefined;
|
|
55
|
+
// Recover + strip the trailing cwd marker.
|
|
56
|
+
const open = stdout.lastIndexOf(CWD_OPEN);
|
|
57
|
+
if (open !== -1) {
|
|
58
|
+
const close = stdout.indexOf(CWD_CLOSE, open);
|
|
59
|
+
if (close !== -1) {
|
|
60
|
+
const newCwd = stdout.slice(open + CWD_OPEN.length, close).trim();
|
|
61
|
+
stdout = stdout.slice(0, open);
|
|
62
|
+
if (newCwd) await setTeleportCwd(chatId, newCwd);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!result.ok && exitCode === undefined) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
text: result.message ?? `${target.name} could not run the command.`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// The device marks a command it had to kill at its own exec budget with
|
|
72
|
+
// this stderr marker (see the companion's device_exec) — surface the same
|
|
73
|
+
// self-correction hint the local timeout path gets.
|
|
74
|
+
const timedOutOnDevice = stderr.includes("[killed: timeout]");
|
|
75
|
+
const body = renderExec(
|
|
76
|
+
`${target.name}${via}`,
|
|
77
|
+
`exit ${exitCode ?? "?"}`,
|
|
78
|
+
stdout,
|
|
79
|
+
stderr,
|
|
80
|
+
);
|
|
81
|
+
return {
|
|
82
|
+
ok: exitCode === 0,
|
|
83
|
+
text: timedOutOnDevice ? `${body}\n${TELEPORT_TIMEOUT_HINT}` : body,
|
|
84
|
+
};
|
|
85
|
+
}
|