refactor(jobs): move the human kill to the job namespace
`session.killJob` becomes `job.kill` on `dsh-api-job-controller`, beside `job.observe`, so the Session Controller carries only session control and the roster while every per-job Remote lives on the job namespace. The client `ctx.jobOutput` gains `kill(sessionId, jobId)`; `dsh-client-ui-jobs` routes its stop control through it and no longer requires `sessions`. Unknown or foreign jobs reject with `job/not-found`; the controller requires `ctx.jobs` to load, so `jobs-unavailable` disappears. The two subagent ownership-fence helpers are classified duplicate-install-safe Host exports of the Session Controller for the cross-controller import.
This commit is contained in:
1 parent
121db081a4
commit
e8469d6f6c
50 files changed
+282
-324
No files matched your search
@@ -107,14 +107,6 @@ export interface ISession {
|
||||
* @returns acceptance, or the business error.
|
||||
*/
|
||||
cancel(): Promise<RemoteResult<{ accepted: true }>>
|
||||
/**
|
||||
* Kill one background job from this session's task list. The owning agent
|
||||
* still receives the standard completion notice, because a human kill has no
|
||||
* other model-visible channel; row state converges through the jobs frames.
|
||||
* @param jobId - the job row's registry id.
|
||||
* @returns the registry's admission, or the business/transport error.
|
||||
*/
|
||||
killJob(jobId: string): Promise<RemoteResult<{ outcome: 'requested' | 'already-finished' }>>
|
||||
/**
|
||||
* Rename this session (explicit user title; pins it against automatic
|
||||
* regeneration).
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
|
||||
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
|
||||
import type { JobId } from '@deepseek-ai/dsh-jobs/brand'
|
||||
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import { SessionLogOffset, SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { SessionEventStream } from '../transport.ts'
|
||||
@@ -326,19 +325,6 @@ export class Session implements SessionFace {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill one background job from this session's task list. Pure RPC passthrough:
|
||||
* row state converges through the jobs control frames, and the caller (the
|
||||
* task-list control) owns error presentation, so no snapshot state changes here.
|
||||
* @param jobId - the job row's registry id.
|
||||
* @returns the registry's admission of the kill request.
|
||||
*/
|
||||
async killJob(jobId: string): Promise<RemoteResult<{ outcome: 'requested' | 'already-finished' }>> {
|
||||
// The brand is nominal typing only; a client program stamps it at the
|
||||
// wire boundary rather than value-importing the host-side constructor.
|
||||
return this.remote.session.killJob({ sessionId: this.sessionId, jobId: jobId as JobId })
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename: contract session.rename 1:1. On success settle the 'title'
|
||||
* projection cell from the response's `{title, seq}` under the store's
|
||||
|
||||
@@ -37,8 +37,6 @@ import type {
|
||||
SessionCreateValue,
|
||||
SessionForkRequest,
|
||||
SessionForkValue,
|
||||
SessionKillJobRequest,
|
||||
SessionKillJobValue,
|
||||
SessionPromptRequest,
|
||||
SessionPromptValue,
|
||||
SessionRenameRequest,
|
||||
@@ -450,46 +448,6 @@ export class SessionCommandController {
|
||||
return { accepted: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill one background job on a human's behalf. The kill leaves the terminal
|
||||
* report unclaimed (`reported: false`), so the owning agent still receives
|
||||
* the standard completion notice — a human cancellation has no other
|
||||
* model-visible channel. The Agent lookup is live-only: a running job's
|
||||
* owner is alive by the registry's ownership contract, and listing never
|
||||
* revives a Session, so neither does killing from its list. The service-wide
|
||||
* subagent ownership fence applies exactly as it does to `cancel`.
|
||||
* @param request - Session whose task list carries the job, and the job id.
|
||||
* @returns the registry's admission of the kill request.
|
||||
*/
|
||||
killJob(request: SessionKillJobRequest): SessionKillJobValue {
|
||||
const jobs = this.ctx.get('jobs')
|
||||
if (jobs === undefined) {
|
||||
throw new RemoteError('session/jobs-unavailable', 'background jobs are not available in this composition', {})
|
||||
}
|
||||
const agent = this.ctx.agents.get(request.sessionId)
|
||||
if (agent !== undefined && hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
||||
throw apiSessionSubagentOwnershipError(request.sessionId)
|
||||
}
|
||||
try {
|
||||
jobs.get(request.jobId, agent)
|
||||
} catch (error) {
|
||||
// `unknown job` and `belongs to another session` both mean this session's
|
||||
// list no longer carries a killable row; the client renders one story.
|
||||
throw new RemoteError('session/job-not-found', String(error), {
|
||||
sessionId: request.sessionId,
|
||||
jobId: request.jobId,
|
||||
})
|
||||
}
|
||||
// Same synchronous span as the lookup, so nothing can remove the job in
|
||||
// between — and a producer-cancel throw now propagates per the registry
|
||||
// contract (job state unchanged) instead of masquerading as job-not-found.
|
||||
const outcome = jobs.kill(request.jobId, agent, {
|
||||
reason: 'cancelled by the user',
|
||||
reported: false,
|
||||
})
|
||||
return { outcome }
|
||||
}
|
||||
|
||||
private async resolveAgent(sessionId: SessionId): Promise<Agent> {
|
||||
const found = await this.agents.resolveAgent(sessionId)
|
||||
if ('error' in found) throw found.error
|
||||
|
||||
@@ -38,8 +38,6 @@ import type {
|
||||
SessionFollowRequest,
|
||||
SessionForkRequest,
|
||||
SessionForkValue,
|
||||
SessionKillJobRequest,
|
||||
SessionKillJobValue,
|
||||
SessionListRequest,
|
||||
SessionListValue,
|
||||
SessionOpenWorkspacePathRequest,
|
||||
@@ -59,7 +57,7 @@ import type {
|
||||
} from './types.ts'
|
||||
|
||||
export type * from './types.ts'
|
||||
export { ApiSessionNotFound } from './agent.ts'
|
||||
export { ApiSessionNotFound, apiSessionSubagentOwnershipError, hasApiSessionSubagentOwner } from './agent.ts'
|
||||
export { SessionFileReferences } from './file-references.ts'
|
||||
export { SessionSkillCatalog } from './skill-catalog.ts'
|
||||
|
||||
@@ -369,17 +367,6 @@ export class SessionController extends TypertRemoteService {
|
||||
return this.commands.cancel(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill one background job on a human's behalf, leaving the terminal report
|
||||
* unclaimed so the owning agent still receives the completion notice.
|
||||
* @param request - Session whose task list carries the job, and the job id.
|
||||
* @returns the registry's admission of the kill request.
|
||||
*/
|
||||
@Remote('killJob')
|
||||
killJob(request: SessionKillJobRequest): SessionKillJobValue {
|
||||
return this.commands.killJob(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one cold-safe, message-aligned Session history page.
|
||||
* @param request - durable address, backward cursor, and page budget.
|
||||
|
||||
@@ -194,8 +194,6 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
'session/attachment-invalid': { readonly reason: string }
|
||||
'session/queue-item-not-found': { readonly itemId: MessageId }
|
||||
'session/steer-unavailable': { readonly itemId: MessageId }
|
||||
'session/job-not-found': { readonly sessionId: SessionId; readonly jobId: JobId }
|
||||
'session/jobs-unavailable': Record<never, never>
|
||||
'session/title-invalid': { readonly sessionId: SessionId }
|
||||
'session/fork-unavailable': { readonly sessionId: SessionId }
|
||||
'subagent/not-found': {
|
||||
@@ -349,17 +347,6 @@ export interface SessionCancelValue {
|
||||
readonly accepted: true
|
||||
}
|
||||
|
||||
/** Human-initiated cancellation of one background job visible to a Session. */
|
||||
export interface SessionKillJobRequest {
|
||||
readonly sessionId: SessionId
|
||||
readonly jobId: JobId
|
||||
}
|
||||
|
||||
/** Receipt after the registry accepted the human kill request. */
|
||||
export interface SessionKillJobValue {
|
||||
readonly outcome: 'requested' | 'already-finished'
|
||||
}
|
||||
|
||||
/** Request to open one path prepared by a Session-aware caller on the Host desktop. */
|
||||
export interface SessionOpenWorkspacePathRequest {
|
||||
/** Path after best-effort Session workspace resolution, in Host filesystem syntax. */
|
||||
|
||||
Reference in new issue
Block a user