Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
5153ff2
test(ios): make SessionSplit width assertion responsive (#1907)
techotaku39 Oct 5, 2026
c2d2cfe
Add 130% app font size (#1975)
rapind Oct 5, 2026
d3d8bf2
feat(ios): add Russian locale (#1964)
mrudybtw Oct 5, 2026
0ef5f97
fix(web): render Codex goal responses as cards (#1954)
junmo-kim Oct 5, 2026
efc9004
feat(web): add Russian locale and detect initial language (#1958)
mrudybtw Oct 5, 2026
99cd3d4
feat(android): add Russian locale (#1963)
mrudybtw Oct 5, 2026
53db5bb
feat: session-attached long-running jobs (#1404)
heavygee Aug 7, 2026
e19aee7
feat(jobs): agent guidance + wall-clock elapsed on list chrome
heavygee Aug 7, 2026
465c62d
fix(jobs): address Opus cold-review Majors for #1404
heavygee Aug 7, 2026
2c1c39c
fix(jobs): honor explicit startedAt on PUT; document late-attach clock
heavygee Aug 8, 2026
0cffc28
fix(jobs): keep merge redirect keys in MetadataSchema (#1404)
heavygee Aug 8, 2026
da543d6
feat(jobs): MCP session_job tool for catalog-level discoverability
heavygee Aug 8, 2026
84a621b
feat(web): tri-state pin — default long-running jobs with #1404
heavygee Aug 8, 2026
c0fea37
fix(jobs): cold-review pass 3 Blocker + Majors (#1404)
heavygee Aug 8, 2026
b4d02a1
fix(jobs): close pass-3 minors — stable primary, own-client heartbeats
heavygee Aug 8, 2026
7df619f
fix(jobs): refresh JWT for long job-run heartbeats (#1404)
heavygee Aug 8, 2026
fe6b5ff
test(jobs): cover proactive JWT refresh; map 401 to auth_failed
heavygee Aug 8, 2026
a70535a
fix(jobs): stamp fresh startedAt on job run; prefer supervisor
heavygee Aug 8, 2026
8ff6c94
fix(jobs): typecheck-safe assertion for run startedAt
heavygee Aug 8, 2026
b409383
fix(jobs): type put mock args so startedAt assertion typechecks
heavygee Aug 8, 2026
f497545
fix(jobs): address Codex Majors — redirect ancestry, heartbeat status…
heavygee Aug 8, 2026
f274d1a
fix(jobs): type from-session metadata when inheriting job redirects
heavygee Aug 8, 2026
bd34c82
fix(jobs): version attachedJob SSE; keep live job on merge collision
heavygee Aug 8, 2026
87c7ea6
fix(jobs): monotonic attachedJob emit watermark (not primary.updatedAt)
heavygee Aug 9, 2026
4f0c8e1
fix(jobs): one attachedJob watermark allocator for REST and SSE
heavygee Aug 9, 2026
aefdda6
fix(jobs): stub allocateAttachedJobVersion in sessions route mocks
heavygee Aug 9, 2026
3c11870
fix(jobs): always record merge redirects even with zero jobs transferred
heavygee Aug 9, 2026
f612f0d
fix(jobs): install source redirect before merge awaits; allow empty u…
heavygee Aug 9, 2026
8fd194e
fix(jobs): expect empty MCP update as heartbeat
heavygee Aug 9, 2026
969c91f
fix(jobs): treat merge redirect metadata as hub-owned
heavygee Aug 9, 2026
5f9e9b7
fix(jobs): reconcile REST list against attachedJob watermark
heavygee Aug 9, 2026
28070b5
fix(jobs): retry terminal status write; reject startedAt on update
heavygee Aug 9, 2026
05ce40f
fix(jobs): keep both live jobs on same-key merge
heavygee Aug 9, 2026
4686d86
fix(jobs): hard-refuse MCP set; louder stale heartbeat chrome
heavygee Aug 9, 2026
e014c96
fix(jobs): preserve jobKeyRedirects across metadata merge + A→B→C
heavygee Aug 9, 2026
f200d44
fix(jobs): keep running-job stubs in sidebar; align bar precedence
heavygee Aug 9, 2026
308f990
fix(jobs): block delete while job running; keep jobful dedup losers
heavygee Aug 9, 2026
c9f96ca
fix(jobs): guard store delete for running jobs; transfer on Codex merge
heavygee Aug 9, 2026
3a1ecb6
test(jobs): stub transferAttachedJobs in Codex duplicate-merge mock
heavygee Aug 9, 2026
c79469e
fix(jobs): fence supervisor PATCH with expectedStartedAt
heavygee Aug 9, 2026
744dfb6
fix(jobs): CAS supervisor writes with UUID runId (schema V24)
heavygee Aug 9, 2026
ce870a4
fix(jobs): heartbeat PATCH must not re-send launch --detail
heavygee Aug 9, 2026
2091c41
fix(jobs): stamp heartbeatAt on the hub only
heavygee Aug 9, 2026
c6fc6fe
docs(jobs): note schema tip V25 after A2A V23 collision
heavygee Aug 10, 2026
70c58e4
fix(jobs): clear stale jobsTransferred redirect on reclaim
heavygee Aug 10, 2026
21b8f79
fix(jobs): transfer attached jobs on OpenCode /clear
heavygee Aug 10, 2026
d4b6e9e
fix(jobs): follow job redirects with cycle detection
heavygee Aug 10, 2026
ab130cb
fix(jobs): defer no-engine Codex merge; pending outranks Jobs
heavygee Aug 10, 2026
5323641
fix(jobs): allocate list watermark before reading jobs
heavygee Aug 10, 2026
31e3309
fix(jobs): stamp supervised startedAt on the hub clock
heavygee Aug 10, 2026
b4a443f
fix(cli): preserve -- for hapi job run under normalizeCliArgs
heavygee Aug 11, 2026
f7ebe98
fix(jobs): allow PATCH null clears from CLI and MCP
heavygee Aug 11, 2026
f281f0c
fix(jobs): scope -- keep to job run; expose runId fence on CLI
heavygee Aug 11, 2026
e5b9b09
fix(jobs): fence DELETE/clear with expectedRunId
heavygee Aug 11, 2026
fa2c5af
fix(jobs): include runId on listSessionJobs for fence recovery
heavygee Aug 11, 2026
c4e7fe8
fix(web): stamp attachedJob on new SessionSummary test helpers
heavygee Aug 11, 2026
c0cfa12
fix(jobs): fail job run when terminal status never persists
heavygee Aug 11, 2026
056eb2c
fix(cursor,web): stop attached-job sessions bouncing Running/Jobs (#1…
heavygee Aug 13, 2026
17f1e15
fix(web): do not mask foreground thinking when attached job runs (#1553)
heavygee Aug 13, 2026
c31c809
fix: rebase onto upstream/main — resolve test mock conflicts
heavygee Aug 15, 2026
dcf42b2
fix(jobs): preserve child exit 0 when meter cleared during job run
heavygee Aug 15, 2026
7890917
fix(cursor): filter only debounced running chatter while queue-idle (…
heavygee Aug 15, 2026
a8f4a7d
fix(#1424): trust ACP foreground signals; preflight merge job check
heavygee Aug 15, 2026
70559bf
fix(cli): steer agents away from sleep-stub jobs and remaining=0 traps
heavygee Aug 15, 2026
606cf4d
fix(cli): steer MCP session_job not_found even when instanceof breaks
heavygee Aug 15, 2026
3b14ce5
fix(cli): idempotent MCP clear-not-found; reject --heartbeat-sec off run
heavygee Aug 15, 2026
033f70c
fix(cli): reject mutation flags on job list/clear and status on run
heavygee Aug 15, 2026
3cb90bd
fix(cli): reject stray MCP fields and non-run command suffix
heavygee Aug 15, 2026
b71234d
fix(hub): preserve runId on corrective job PUT without runId
heavygee Aug 15, 2026
1a21298
docs: mark ALL_DONE late-attach example as completed status
heavygee Aug 15, 2026
0378cde
fix(web,cli): finish #1424 rebase onto upstream pin buckets
heavygee Aug 16, 2026
51dabb6
fix(web): rename All activity — quiet connected is not in-progress
heavygee Aug 17, 2026
b318f3e
docs(guide): session-id contract and remaining vs bar fill
heavygee Aug 17, 2026
04c7a99
fix(web): bind isNewerVersionedPatch after sessionPatch extract
heavygee Aug 18, 2026
0c7ea3d
fix(hub): commit job-row move and owner redirects together
heavygee Aug 18, 2026
ad34a65
fix(jobs): fail-closed session id for remote Cursor job run
heavygee Aug 19, 2026
85b0c5c
fix(hub): refresh job redirects before scratchlist merge await
heavygee Aug 19, 2026
9f66408
fix(cursor): ignore ambient ACP running without a prompt in flight
heavygee Aug 19, 2026
8471f3a
fix(cursor): drop leftover rebase conflict markers in ACP launcher
heavygee Aug 20, 2026
9503354
fix(cli): sanitize job text before terminal stdout
heavygee Aug 20, 2026
4eb16cb
fix(cli): sanitize MCP session_job output via shared formatter
heavygee Aug 20, 2026
dd2a6e9
fix(cli): scrub C1 controls in shared job-output sanitizer
heavygee Aug 20, 2026
3162f3c
test(cli): fix C1 sanitizer assertion (content may remain)
heavygee Aug 20, 2026
1b95ecc
fix(hub): remap late job PUT when merge left no key redirect
heavygee Sep 6, 2026
7f5bd14
fix(hub): also remap redirected job set without runId on collision
heavygee Sep 6, 2026
c25031c
fix(cli): tree-kill supervised job children on SIGINT/SIGTERM
heavygee Sep 6, 2026
0aba7d2
fix(cli): preserve SIGINT on job tree-kill and await teardown
heavygee Sep 6, 2026
35f3306
test(cli): type the tree-kill teardown patch mock for typecheck
heavygee Sep 6, 2026
d73de4e
test(web): supply attachedJob fields in upstream session fixtures
heavygee Sep 9, 2026
75ade95
fix(hub): record identity jobKeyRedirects on no-collision transfer
heavygee Sep 12, 2026
cc9224e
fix(hub): resolve job owner after PUT/PATCH body parse
heavygee Sep 12, 2026
5e840d6
fix(hub): honor identity jobKeyRedirects on corrective PUT
heavygee Sep 12, 2026
5fbf662
fix(hub): persist identity redirects on late free-key job PUT
heavygee Sep 12, 2026
06fe9b7
fix(hub): skip redirect inheritance on late job-key PUT
heavygee Sep 12, 2026
13e5364
test(web): supply attachedJob on new upstream search fixtures
heavygee Sep 14, 2026
37b32be
fix(cli): pass XDG_CACHE_HOME through vitest hub globalSetup
heavygee Sep 14, 2026
28c3b2f
test(web): retarget bucketRunningSessions for jobs pin modes
heavygee Sep 16, 2026
01aceea
fix(web): add Russian strings for session jobs pin modes
heavygee Oct 5, 2026
e06db98
docs(cli): steer agents to list session jobs before done/archive
heavygee Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Start with the task's files; read only relevant sections of these references, no
| Shared wire types and validation | `shared/src/types.ts`, `schemas.ts`, `socket.ts`, `modes.ts` |
| Native API contract, chat conformance | [client contract](docs/api/client-contract/index.md), [iOS](ios/README.md), [Android](android/README.md) |
| Encrypted native push relay | [relay/README.md](relay/README.md) |
| Session-attached jobs (outliving work) | [session jobs guide](docs/guide/session-jobs.md), `cli/src/commands/job.ts` |
| User docs / marketing site | `docs/` (VitePress) / `website/` |

## Repository conventions
Expand All @@ -39,6 +40,20 @@ Start with the task's files; read only relevant sections of these references, no
- Metadata/state updates are versioned; preserve stale-update rejection. Permission controls use per-flavor catalogs in `shared/src/modes.ts`, further constrained by session capabilities.
- `shared/fixtures/**` is generated from the web chat pipeline, the source of truth for native conformance. Never hand-edit fixtures. For changes to fixture inputs or generation (paths in [.github/workflows/fixtures.yml](.github/workflows/fixtures.yml)), run `bun run gen:fixtures` and include any generated changes in the deliverable. CI checks drift and runs native conformance on fixture changes.

## Session-attached jobs (outliving work)

When an agent starts process-shaped work that will keep running after the agent goes idle (`nohup`, batch imports, long scripts, external daemons), attach it so the session list stays truthful while `active: false`. This is **not** thinking progress / todos / in-agent background tools.

Agent contract (idle agents cannot heartbeat — bare set + nohup freezes the bar):

1. **Required for process-shaped work:** Shell `hapi job run <session-id> <key> --label … -- <cmd>` (auto-heartbeat + exit status). Use `"$HAPI_SESSION_ID"` only when it matches the operator chat row (`/sessions/<id>` in the web URL).
2. MCP `session_job` **refuses `action=set`**. Use it only for `update` / `clear` / `list` on a job the supervisor already created.
3. Manual CLI `set` only with a self-heartbeating wrapper (`update` ≥~10m); never MCP set + nohup.
4. Prefer honest `--remaining` or `--done`/`--total`; omit counts if unknown — never invent a percent.
5. Elapsed wall clock is always shown from `startedAt` (not an ETA).

Full guide: [docs/guide/session-jobs.md](docs/guide/session-jobs.md). CLI: `hapi job --help`.

## Verification and completion

Choose checks by the change's impact, not by the number of workflow steps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map

/**
* App language choice. [ENGLISH]/[SIMPLIFIED_CHINESE] mirror the web's
* `Locale` (`'en' | 'zh-Hans'`); [SYSTEM] is the Android-only default —
* follow the device language (empty per-app locale list).
* App language choice. [ENGLISH]/[SIMPLIFIED_CHINESE]/[RUSSIAN] mirror the
* web's `Locale` (`'en' | 'zh-Hans'` plus the fork's `'ru'`); [SYSTEM] is the
* Android-only default — follow the device language (empty per-app locale
* list).
*
* B-M5a wires the selection through `AppCompatDelegate.setApplicationLocales`
* ([localeTags] is the BCP-47 tag list to apply); the Settings screen applies
Expand All @@ -24,7 +25,8 @@ import kotlinx.coroutines.flow.map
enum class AppLanguage(val storageKey: String, val localeTags: String) {
SYSTEM("system", ""),
ENGLISH("en", "en"),
SIMPLIFIED_CHINESE("zh-Hans", "zh-Hans");
SIMPLIFIED_CHINESE("zh-Hans", "zh-Hans"),
RUSSIAN("ru", "ru");

companion object {
fun fromStorageKey(raw: String?): AppLanguage =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,4 +377,5 @@ private fun languageLabel(language: AppLanguage): String = when (language) {
AppLanguage.SYSTEM -> stringResource(R.string.settings_language_system)
AppLanguage.ENGLISH -> "English"
AppLanguage.SIMPLIFIED_CHINESE -> "简体中文"
AppLanguage.RUSSIAN -> "Русский"
}
573 changes: 573 additions & 0 deletions android/app/src/main/res/values-ru/strings.xml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions android/app/src/main/res/xml/locales_config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en" />
<locale android:name="zh-CN" />
<locale android:name="ru" />
</locale-config>
1 change: 1 addition & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Choose a supported coding agent from your terminal and control its sessions remo
- `hapi resume [sessionId]` - List resumable sessions for this machine or resume one locally.
- `hapi ping-peer <session-id-prefix> <message>` - Resume (if needed) and message another session. Prefer this or MCP `ping_peer` / `list_peers` over reinventing JWT+curl. Also `--message-file` / `--list`.
- `hapi inspect-peer <session-id-or-prefix>` - Read-only peer metadata + recent message text (no resume). Prefer this or MCP `inspect_peer` when a user cites `[title](/sessions/<id>)` or Copy-reference `See session "…" (/sessions/<id>) for context`. `/sessions/<id>` is a hub path, not a local file. Optional `--limit`.
- `hapi job set|update|clear|list|run` - Attach long-running outliving work to a session so the list UI shows progress while the agent is idle (`tiann/hapi#1404`). Prefer `"$HAPI_SESSION_ID"`. Heartbeat via `update` (or `run`); honest `--remaining` or `--done`/`--total` (omit counts if unknown — never invent a percent). Late-attach clock fix: `set --started-at <epoch-ms>` or clear+set. See `docs/guide/session-jobs.md` and `hapi job --help`.

The picker lists agents alphabetically by command name. Use Up/Down and Enter
to choose; Esc or Ctrl-C cancels. It appears on every bare invocation, even
Expand Down
25 changes: 19 additions & 6 deletions cli/src/agent/backends/acp/AcpSdkBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ import { AcpStdioTransport, type AcpStderrError } from './AcpStdioTransport';
import { AcpMessageHandler, type AcpTextChunkMode } from './AcpMessageHandler';
import { ACP_SESSION_UPDATE_TYPES } from './constants';
import { thinkingHintFromSessionUpdate } from './shouldBumpThinkingFromSessionUpdate';

/** Why the backend is bumping/clearing hub thinking — launchers filter chatter here (#1553). */
export type AgentActivitySource =
| 'state_update_running'
| 'state_update_idle'
| 'state_update_requires_action'
| 'permission';

export type AgentActivityListener = (thinking: boolean, source: AgentActivitySource) => void;
import { logger } from '@/ui/logger';
import { withRetry } from '@/utils/time';
import packageJson from '../../../../package.json';
Expand Down Expand Up @@ -93,7 +102,7 @@ export class AcpSdkBackend implements AgentBackend {
private usageUpdateListener: ((msg: AgentMessage) => void) | null = null;
private sessionInfoUpdateListener: ((update: AcpSessionInfoUpdate) => void) | null = null;
/** Fired on foreground ACP state / permission so launchers can bump hub thinking (#1470). */
private agentActivityListener: ((thinking: boolean) => void) | null = null;
private agentActivityListener: AgentActivityListener | null = null;
/** Debounce timer for state_update running → thinking (#1502 chatter). */
private runningThinkingTimer: ReturnType<typeof setTimeout> | null = null;
private lastForwardedUsageUpdate: AcpUsageUpdate | null = null;
Expand Down Expand Up @@ -524,7 +533,7 @@ export class AcpSdkBackend implements AgentBackend {
* `false` = `state_update` idle (skipped while a HAPI prompt turn is still draining).
* Launchers should ignore no-ops when session.thinking already matches.
*/
setAgentActivityListener(listener: ((thinking: boolean) => void) | null): void {
setAgentActivityListener(listener: AgentActivityListener | null): void {
this.agentActivityListener = listener;
}

Expand Down Expand Up @@ -988,7 +997,7 @@ export class AcpSdkBackend implements AgentBackend {
if (this.isProcessingMessage) {
return;
}
this.agentActivityListener(false);
this.agentActivityListener(false, 'state_update_idle');
return;
}

Expand All @@ -999,13 +1008,17 @@ export class AcpSdkBackend implements AgentBackend {
}
this.runningThinkingTimer = setTimeout(() => {
this.runningThinkingTimer = null;
this.agentActivityListener?.(true);
this.agentActivityListener?.(true, 'state_update_running');
}, AcpSdkBackend.RUNNING_THINKING_DEBOUNCE_MS);
return;
}

this.clearRunningThinkingTimer();
this.agentActivityListener(true);
const source: AgentActivitySource = update.sessionUpdate === 'state_update'
&& update.state === 'requires_action'
? 'state_update_requires_action'
: 'state_update_running';
this.agentActivityListener(true, source);
}

private clearRunningThinkingTimer(): void {
Expand Down Expand Up @@ -1191,7 +1204,7 @@ export class AcpSdkBackend implements AgentBackend {
if (this.permissionHandler) {
try {
// Permission prompts imply the agent is awake (#1470).
this.agentActivityListener?.(true);
this.agentActivityListener?.(true, 'permission');
this.permissionHandler(request);
} catch (error) {
this.pendingPermissions.delete(toolCallId);
Expand Down
4 changes: 2 additions & 2 deletions cli/src/agent/runners/runAgentSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ vi.mock('@/claude/utils/startHappyServer', () => ({
harness.startHappyServerOptions = options
return {
url: 'http://127.0.0.1:1234',
toolNames: ['change_title', 'display_image', 'display_video', 'display_media', 'list_peers', 'ping_peer', 'inspect_peer', 'skill_lookup'],
toolNames: ['change_title', 'display_image', 'list_peers', 'ping_peer', 'inspect_peer', 'session_job', 'skill_lookup'],
stop: harness.stopServer
}
})
Expand Down Expand Up @@ -167,7 +167,7 @@ describe('runAgentSession', () => {
'--url',
'http://127.0.0.1:1234',
'--tools',
'change_title,display_image,display_video,display_media,list_peers,ping_peer,inspect_peer,skill_lookup'
'change_title,display_image,list_peers,ping_peer,inspect_peer,session_job,skill_lookup'
])
expect(harness.newSessionOptions).toMatchObject({
cwd: '/tmp/project',
Expand Down
35 changes: 35 additions & 0 deletions cli/src/agent/sessionFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,41 @@ describe('bootstrapExistingSession', () => {
)
})

it('exports the requested existing session id for Cursor runner resume (#1404)', async () => {
// Runner passes --existing-session-id (buildCliArgs); child bootstrap must
// publish that same hub row into HAPI_SESSION_ID for shell `hapi job run`.
const session = createSession()
session.metadata = {
...session.metadata!,
flavor: 'cursor',
cursorSessionId: 'cursor-csid-1',
cursorSessionProtocol: 'acp'
}
const sessionClient = { updateMetadata: vi.fn() }
getSessionMock.mockResolvedValue(session)
getOrCreateMachineMock.mockResolvedValue({ id: 'machine-1' })
sessionSyncClientMock.mockReturnValue(sessionClient)
readSettingsMock.mockResolvedValue({ machineId: 'machine-1' })

await bootstrapExistingSession({
sessionId: 'hapi-session-1',
flavor: 'cursor',
startedBy: 'runner',
workingDirectory: '/tmp/project'
})

expect(getSessionMock).toHaveBeenCalledWith('hapi-session-1')
expect(process.env[HAPI_SESSION_ID_ENV]).toBe('hapi-session-1')
expect(notifyRunnerSessionStartedMock).toHaveBeenCalledWith(
'hapi-session-1',
expect.objectContaining({
flavor: 'cursor',
startedBy: 'runner',
startedFromRunner: true
})
)
})

it('refuses to reopen a hub-archived session (hub-archive resurrection guard)', async () => {
// #1911 M1 belt: hub-archived only (matches store merge-preserve scope).
const session = createSession()
Expand Down
28 changes: 21 additions & 7 deletions cli/src/claude/utils/startHappyServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('startHappyServer skill_lookup', () => {
async function connect(enableSkillLookup = true): Promise<Client> {
sendAgentMessage = vi.fn()
const sessionClient = {
sessionId: 'test-session-id',
updateMetadata: vi.fn(),
sendAgentMessage,
sendClaudeSessionMessage: vi.fn()
Expand Down Expand Up @@ -107,15 +108,16 @@ describe('startHappyServer skill_lookup', () => {
const mcp = await connect(false)
const tools = await mcp.listTools()

expect(tools.tools.map((tool) => tool.name)).toEqual([
expect(tools.tools.map((tool) => tool.name).sort()).toEqual([
'change_title',
'display_image',
'display_video',
'display_media',
'ping_peer',
'inspect_peer',
'list_peers'
])
'list_peers',
'ping_peer',
'session_job',
].sort())
})

it('describes display_image as user output rather than image input', async () => {
Expand Down Expand Up @@ -170,6 +172,7 @@ describe('startHappyServer skill_lookup', () => {

it('does not expose change_title when native ACP titles are enabled', async () => {
const sessionClient = {
sessionId: 'test-session-id',
updateMetadata: vi.fn(),
sendAgentMessage: vi.fn(),
sendClaudeSessionMessage: vi.fn()
Expand All @@ -182,15 +185,24 @@ describe('startHappyServer skill_lookup', () => {
await mcp.connect(new StreamableHTTPClientTransport(new URL(server.url)))
const tools = await mcp.listTools()

expect(server.toolNames).toEqual(['display_image', 'display_video', 'display_media', 'list_peers', 'ping_peer', 'inspect_peer'])
expect(tools.tools.map((tool) => tool.name)).toEqual([
expect(server.toolNames).toEqual([
'display_image',
'display_video',
'display_media',
'list_peers',
'ping_peer',
'inspect_peer',
'list_peers'
'session_job',
])
expect(tools.tools.map((tool) => tool.name).sort()).toEqual([
'display_image',
'display_media',
'display_video',
'inspect_peer',
'list_peers',
'ping_peer',
'session_job',
].sort())
})

})
Expand Down Expand Up @@ -317,11 +329,13 @@ describe('toClaudeAllowedHapiMcpTools', () => {
'list_peers',
'ping_peer',
'inspect_peer',
'session_job',
'skill_lookup'
])).toEqual([
'mcp__hapi__change_title',
'mcp__hapi__display_image',
'mcp__hapi__list_peers',
'mcp__hapi__session_job',
'mcp__hapi__skill_lookup'
])
expect(toClaudeAllowedHapiMcpTools(['display_video'])).not.toContain('mcp__hapi__display_video')
Expand Down
24 changes: 22 additions & 2 deletions cli/src/claude/utils/startHappyServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ import {
} from '@hapi/protocol/sessionCitation'
import { PingPeerError, formatInspectPeerReport, formatPeerSessionsList, inspectPeer, listPeerSessions, peerListFetchLimit, pingPeer } from "@/modules/pingPeer/pingPeer";
import { applySessionDisplayRename, normalizeSessionDisplayTitle } from "@/agent/sessionDisplayRename";
import {
SESSION_JOB_TOOL_DESCRIPTION,
SESSION_JOB_TOOL_NAME,
handleSessionJobTool,
sessionJobInputSchema,
type SessionJobToolArgs,
} from "@/modules/sessionJob/sessionJobMcp";

type StartHappyServerOptions = {
/**
Expand Down Expand Up @@ -376,6 +383,19 @@ function createHapiMcpServer(
}
});

mcp.registerTool<any, any>(SESSION_JOB_TOOL_NAME, {
description: SESSION_JOB_TOOL_DESCRIPTION,
title: 'Session-Attached Job',
inputSchema: sessionJobInputSchema,
}, async (args: SessionJobToolArgs) => {
logger.debug('[hapiMCP] session_job:', args.action, args.jobKey);
const result = await handleSessionJobTool(args, client.sessionId);
return {
content: [{ type: 'text' as const, text: result.text }],
isError: result.isError,
};
});

mcp.registerTool<any, any>('list_peers', {
description: 'List peer HAPI sessions on the same hub/namespace (id prefix, active, flavor, name). Uses this session\'s hub credentials - works from runner-spawned agents without being on the hub host. Prefer this over shelling `hapi ping-peer --list`. Then call inspect_peer / ping_peer with a listed id.',
title: 'List Peer Sessions',
Expand Down Expand Up @@ -541,8 +561,8 @@ export async function startHappyServer(client: ApiSessionClient, options: StartH
}));

const toolNames = enableChangeTitle
? ['change_title', 'display_image', 'display_video', 'display_media', 'list_peers', 'ping_peer', 'inspect_peer']
: ['display_image', 'display_video', 'display_media', 'list_peers', 'ping_peer', 'inspect_peer'];
? ['change_title', 'display_image', 'display_video', 'display_media', 'list_peers', 'ping_peer', 'inspect_peer', SESSION_JOB_TOOL_NAME]
: ['display_image', 'display_video', 'display_media', 'list_peers', 'ping_peer', 'inspect_peer', SESSION_JOB_TOOL_NAME];
if (options.skillLookup) {
toolNames.push('skill_lookup');
}
Expand Down
3 changes: 2 additions & 1 deletion cli/src/claude/utils/systemPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { trimIdent } from "@/utils/trimIdent";
import { buildSessionCitationSteerInstruction } from "@hapi/protocol/sessionCitation";
import { shouldIncludeCoAuthoredBy } from "./claudeSettings";
import { DISPLAY_IMAGE_PROMPT_CLAUDE, DISPLAY_MEDIA_PROMPT_CLAUDE, DISPLAY_VIDEO_PROMPT_CLAUDE } from "@/modules/common/displayImagePrompt";
import { withSessionJobInstruction } from "@/modules/common/sessionJobInstruction";
import { withSessionSummaryInstruction } from "@/modules/common/sessionSummaryInstruction";

/**
Expand Down Expand Up @@ -42,5 +43,5 @@ export function getSystemPrompt(): string {
const base = includeCoAuthored
? BASE_SYSTEM_PROMPT + '\n\n' + CO_AUTHORED_CREDITS
: BASE_SYSTEM_PROMPT;
return withSessionSummaryInstruction(base);
return withSessionSummaryInstruction(withSessionJobInstruction(base));
}
32 changes: 27 additions & 5 deletions cli/src/codex/happyMcpStdioBridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,20 @@ describe('runHappyMcpStdioBridge tool forwarding', () => {
'--url',
'http://127.0.0.1:43006',
'--tools',
'change_title,display_image,display_video,display_media,skill_lookup'
'change_title,display_image,display_video,display_media,list_peers,ping_peer,inspect_peer,session_job,skill_lookup'
])

expect([...harness.tools.keys()]).toEqual([
expect([...harness.tools.keys()].sort()).toEqual([
'change_title',
'display_image',
'display_video',
'display_media',
'skill_lookup'
])
'display_video',
'inspect_peer',
'list_peers',
'ping_peer',
'session_job',
'skill_lookup',
].sort())

const handler = harness.tools.get('skill_lookup')
expect(handler).toBeDefined()
Expand Down Expand Up @@ -140,6 +144,24 @@ describe('runHappyMcpStdioBridge tool forwarding', () => {
])
})

it('registers session_job when included in --tools', async () => {
await runHappyMcpStdioBridge([
'--url',
'http://127.0.0.1:43006',
'--tools',
'change_title,display_image,list_peers,ping_peer,inspect_peer,session_job'
])

expect([...harness.tools.keys()].sort()).toEqual([
'change_title',
'display_image',
'inspect_peer',
'list_peers',
'ping_peer',
'session_job',
].sort())
})

it('registers inspect_peer when included in --tools', async () => {
await runHappyMcpStdioBridge([
'--url',
Expand Down
Loading