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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 2 additions & 4 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,15 +201,13 @@ jobs:
if: steps.should-build.outputs.skip != 'true' && env.PUSH != 'true' && matrix.image == 'workspace' && matrix.platform == 'linux/amd64'
run: |
docker run --rm memoh-ci-workspace:debian sh -eux -c '
grep -q "\"contract_version\": 2" /opt/memoh/workspace-contract.json
grep -q "\"contract_version\": 3" /opt/memoh/workspace-contract.json
ldd --version >/dev/null
node --version
python3 --version
uv --version
test -x /opt/memoh/toolkit/bin/codex
test -x /opt/memoh/toolkit/bin/codex-acp
test -x /opt/memoh/toolkit/bin/claude-agent-acp
test -x /opt/memoh/toolkit/bin/hermes-acp
test -x /opt/memoh/toolkit/bin/claude
test -x /opt/memoh/toolkit/display/bin/a11y-cli
sh -n /opt/memoh/scripts/display-prepare.sh
sh -n /opt/memoh/scripts/display-apply-style.sh
Expand Down
11 changes: 8 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,14 @@ Memoh/
│ │ ├── decision/ # User input, tool approval, and stable user-facing feedback
│ │ ├── event/ # Agent events and transport payload vocabulary
│ │ ├── runtime/ # Runtime implementations
│ │ │ ├── acp/ # ACP pool, client process manager, and profiles
│ │ │ ├── acp/ # ACP pool, client process manager, and the generic custom-agent profile
│ │ │ ├── claudecode/ # Claude Code direct runtime (external.Driver)
│ │ │ ├── codex/ # Codex direct runtime (external.Driver)
│ │ │ ├── external/ # Neutral Driver port between the application layer and external runtimes
│ │ │ ├── native/ # Twilight AI native runtime, prompts, streaming, hooks, and guards
│ │ │ └── session/ # Per-thread runtime state and control
│ │ │ ├── agentstate/ # External Agent session publication heads and state storage port
│ │ │ ├── session/ # Per-thread runtime state and control
│ │ │ └── toolmount/ # Memoh tool gateway mounts for direct runtimes
│ │ ├── sessionmode/ # Session mode resolution
│ │ ├── tool/ # Native tool providers (package name remains tools)
│ │ ├── message.go # Send message tool
Expand Down Expand Up @@ -387,7 +392,7 @@ PostgreSQL migrations live in `db/postgres/migrations/`:

The codebase has grown beyond the original agent/channel/container core. When working near these areas, read the local `AGENTS.md` and treat the corresponding `internal/` package as the source of truth; do not guess tool or schema details.

- **ACP (`internal/agent/runtime/acp/`)** — runtime pool, client process manager, profiles, and OAuth integration for external ACP agents such as Claude Code and Codex. Stable user-facing ACP errors live in `internal/agent/decision/feedback/`.
- **External coding-agent runtimes (`internal/agent/runtime/external/`, `codex/`, `claudecode/`)** — the neutral `external.Driver` port plus the direct Codex and Claude Code runtimes (pinned protocol assets, device-code/OAuth login, native thread resume/fork, Memoh tool gateway mounts via `toolmount/`). **ACP (`internal/agent/runtime/acp/`)** is the generic channel for custom user-supplied ACP agents (single generic profile with a managed launch command), folded into the same driver port. Stable user-facing runtime errors live in `internal/agent/decision/feedback/`.
- **Skill Packages (`internal/skillpackages/`, `internal/supermarket/`)** — Supermarket Package discovery and installation state. Installed Packages expand into immutable Registry Skills in the selected workspace target.
- **User input / `ask_user` (`internal/agent/decision/input/`)** — lets the in-process agent ask the user a question mid-conversation and wait for an answer.
- **Bot backup / import / export (`internal/botbackup/`)** — archive-based bot portability with preview and merge/replace/skip strategies.
Expand Down
14 changes: 9 additions & 5 deletions apps/web/src/components/session-select/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { SessionSession } from '@memohai/sdk'
import type { SearchableSelectOption } from '@/components/searchable-select-popover/index.vue'
import type { BotWorkdir } from '@/composables/api/useWorkdirs'
import type { SessionKind } from './session-kind-icon.vue'
import { acpAgentDisplayName, normalizeACPAgentID } from '@/utils/acp'
import { normalizedRuntimeType, normalizedSessionMode } from '@/store/chat-list.utils'
import { acpAgentDisplayName } from '@/utils/acp'
import { sessionAgentProvider } from '@/utils/bot-agent'
import { isAgentRuntimeType, normalizedRuntimeType, normalizedSessionMode } from '@/store/chat-list.utils'

// The per-row mark the picker paints beside a session title.
export interface SessionMark {
Expand All @@ -29,16 +30,19 @@ export function sessionTitle(session: SessionSession, labels: SessionSelectLabel
}

export function sessionMark(session: SessionSession, labels: SessionSelectLabels): SessionMark {
const agentId = normalizeACPAgentID(
session.runtime_metadata?.acp_agent_id ?? session.metadata?.acp_agent_id,
const runtimeType = normalizedRuntimeType(session)
const agentId = sessionAgentProvider(
runtimeType,
session.runtime_metadata,
session.metadata,
)
// Schedule wins over the ACP mark here, unlike the sidebar row: this picker
// exists to tell schedule-owned sessions apart from ordinary chats, and a
// schedule session that happens to run an agent is still a schedule session.
if (normalizedSessionMode(session) === 'schedule') {
return { kind: 'schedule', agentId, label: labels.schedule }
}
if (normalizedRuntimeType(session) === 'acp_agent') {
if (isAgentRuntimeType(runtimeType)) {
return { kind: 'acp', agentId, label: acpAgentDisplayName(agentId, labels.agent) }
}
return NO_MARK
Expand Down
27 changes: 15 additions & 12 deletions apps/web/src/components/sidebar/session-item.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@
@keydown.enter.prevent="$emit('select', session)"
@keydown.space.prevent="$emit('select', session)"
>
<!-- Native session rows stay text-only. ACP rows carry the agent icon
<!-- Native session rows stay text-only. Agent rows carry the agent icon
and schedule runs a yellow clock, because the unified Recents list
mixes model chats, external-agent chats, and schedule runs. -->
<span
v-if="isACPSession"
v-if="isAgentSession"
class="mr-2 flex size-4 shrink-0 items-center justify-center text-muted-foreground"
role="img"
:aria-label="acpAgentLabel"
:aria-label="agentLabel"
>
<component
:is="acpAgentIcon(acpAgentId, true)"
:is="acpAgentIcon(agentProvider, true)"
class="size-4"
aria-hidden="true"
/>
Expand Down Expand Up @@ -157,9 +157,10 @@ import {
DropdownMenuContent,
DropdownMenuItem,
} from '@felinic/ui'
import { acpAgentDisplayName, acpAgentIcon, normalizeACPAgentID } from '@/utils/acp'
import { acpAgentDisplayName, acpAgentIcon } from '@/utils/acp'
import { sessionAgentProvider } from '@/utils/bot-agent'
import { splitScriptRuns } from '@/utils/script-runs'
import { normalizedRuntimeType, normalizedSessionMode } from '@/store/chat-list.utils'
import { isAgentRuntimeType, normalizedRuntimeType, normalizedSessionMode } from '@/store/chat-list.utils'

const props = defineProps<{
session: SessionSummary
Expand Down Expand Up @@ -195,12 +196,14 @@ const isIMSession = computed(() => {
return ct !== '' && !WEB_CHANNELS.has(ct)
})

const acpAgentId = computed(() => normalizeACPAgentID(
props.session.runtime_metadata?.acp_agent_id ?? props.session.metadata?.acp_agent_id,
const agentProvider = computed(() => sessionAgentProvider(
normalizedRuntimeType(props.session),
props.session.runtime_metadata,
props.session.metadata,
))
const isACPSession = computed(() => normalizedRuntimeType(props.session) === 'acp_agent')
const isAgentSession = computed(() => isAgentRuntimeType(normalizedRuntimeType(props.session)))
const isScheduleSession = computed(() => normalizedSessionMode(props.session) === 'schedule')
const acpAgentLabel = computed(() => acpAgentDisplayName(acpAgentId.value, t('chat.sessionTypeACPAgent')))
const agentLabel = computed(() => acpAgentDisplayName(agentProvider.value, t('chat.sessionTypeACPAgent')))

function routeMeta(): Record<string, unknown> {
return props.session.route_metadata ?? {}
Expand All @@ -218,8 +221,8 @@ const displayLabel = computed(() => {
// for IM sessions, agent name for ACP sessions.
const hoverTitle = computed(() => {
const title = props.session.title || t('chat.untitledSession')
if (isACPSession.value) {
return `${title} — ${acpAgentLabel.value}`
if (isAgentSession.value) {
return `${title} — ${agentLabel.value}`
}
if (!isIMSession.value) return title
const meta = routeMeta()
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/composables/api/useChat.chat-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export interface CreateSessionOptions {

export interface CreateACPRuntimeOptions {
agentId: string
botAgentId?: string
projectPath?: string
}

Expand Down Expand Up @@ -224,7 +223,6 @@ export async function createACPRuntime(botId: string, options: CreateACPRuntimeO
path: { bot_id: botId.trim() },
body: {
acp_agent_id: options.agentId.trim(),
bot_agent_id: options.botAgentId?.trim() || undefined,
project_path: options.projectPath?.trim(),
},
throwOnError: true,
Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/composables/api/useChat.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,16 @@ export interface UIErrorMessage {
content: string
}

export type UIMessage = UITextMessage | UIReasoningMessage | UIToolMessage | UIAttachmentsMessage | UIErrorMessage
// Runtime degradation notice (tools unavailable, an interaction declined).
// `name` carries the machine code, `content` the human-readable text.
export interface UINoticeMessage {
id: number
type: 'notice'
name?: string
content: string
}

export type UIMessage = UITextMessage | UIReasoningMessage | UIToolMessage | UIAttachmentsMessage | UIErrorMessage | UINoticeMessage

export interface UISkillActivationSkill {
name: string
Expand Down
Loading
Loading