@linxin666/dsh-client-ui-task-board 0.2.8 → 0.3.0
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/LICENSE +202 -29
- package/README.i18n.yaml +2 -2
- package/README.md +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +18 -159
- package/lib/client.js.map +1 -1
- package/lib/index.js +123 -46
- package/lib/types/core/controller.d.ts +10 -31
- package/lib/types/core/tasks.d.ts +17 -1
- package/lib/types/host-ledger.d.ts +28 -0
- package/lib/types/host-service.d.ts +0 -1
- package/lib/types/http.d.ts +42 -0
- package/package.json +1 -1
- package/src/client/board/TaskBoard.tsx +2 -0
- package/src/client/board.module.css +185 -13
- package/src/client/sidebar-entry.ts +2 -2
- package/src/core/controller.ts +16 -152
- package/src/core/tasks.ts +31 -2
- package/src/host-ledger.ts +69 -3
- package/src/host-routes.ts +10 -14
- package/src/host-service.ts +32 -28
- package/src/http.ts +105 -0
- package/lib/types/core/execution.d.ts +0 -197
- package/src/core/execution.ts +0 -425
package/src/host-routes.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { timingSafeEqual } from 'node:crypto'
|
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
3
3
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
4
4
|
import type { TaskBoardHostService } from './host-service.ts'
|
|
5
|
+
import { writeJson } from './http.ts'
|
|
5
6
|
import { isLoopbackAddress, isLoopbackRequest } from './loopback.ts'
|
|
6
7
|
import { parseActionEnvelope, TASK_BOARD_API_PREFIX } from './protocol.ts'
|
|
7
8
|
|
|
@@ -23,11 +24,6 @@ interface ResolvedRouteAccess {
|
|
|
23
24
|
proxyToken?: string
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function json(res: ServerResponse, status: number, body: unknown): void {
|
|
27
|
-
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
28
|
-
res.end(JSON.stringify(body))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
27
|
function parseAuthority(authority: string): { canonical: string; url: URL } | undefined {
|
|
32
28
|
if (authority.trim() !== authority) return undefined
|
|
33
29
|
const match = authority.startsWith('[')
|
|
@@ -127,38 +123,38 @@ export function makeTaskBoardRoutes(service: TaskBoardHostService, access: TaskB
|
|
|
127
123
|
const resolvedAccess = resolveAccess(access)
|
|
128
124
|
const guard = (req: IncomingMessage, res: ServerResponse): boolean => {
|
|
129
125
|
if (isTrustedTaskBoardRequest(req, resolvedAccess)) return true
|
|
130
|
-
|
|
126
|
+
writeJson(res, 403, { ok: false, error: 'forbidden' }, { 'cache-control': 'no-store' })
|
|
131
127
|
return false
|
|
132
128
|
}
|
|
133
129
|
const state: WebRoute = {
|
|
134
130
|
kind: 'exact',
|
|
135
131
|
path: `${TASK_BOARD_API_PREFIX}/state`,
|
|
136
132
|
handler: (req, res): void => {
|
|
137
|
-
if (req.method !== 'GET') return
|
|
133
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'method-not-allowed' }, { 'cache-control': 'no-store' })
|
|
138
134
|
if (!guard(req, res)) return
|
|
139
|
-
|
|
135
|
+
writeJson(res, 200, service.snapshot(), { 'cache-control': 'no-store' })
|
|
140
136
|
},
|
|
141
137
|
}
|
|
142
138
|
const action: WebRoute = {
|
|
143
139
|
kind: 'exact',
|
|
144
140
|
path: `${TASK_BOARD_API_PREFIX}/action`,
|
|
145
141
|
handler: async (req, res): Promise<void> => {
|
|
146
|
-
if (req.method !== 'POST') return
|
|
142
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'method-not-allowed' }, { 'cache-control': 'no-store' })
|
|
147
143
|
if (!guard(req, res)) return
|
|
148
144
|
if (!(req.headers['content-type'] ?? '').toLowerCase().startsWith('application/json')) {
|
|
149
|
-
return
|
|
145
|
+
return writeJson(res, 415, { ok: false, error: 'json-required' }, { 'cache-control': 'no-store' })
|
|
150
146
|
}
|
|
151
147
|
try {
|
|
152
148
|
const body = await readBody(req)
|
|
153
149
|
const parsed = parseActionEnvelope(body.value)
|
|
154
|
-
if (parsed === undefined) return
|
|
150
|
+
if (parsed === undefined) return writeJson(res, 400, { ok: false, error: 'invalid-action' }, { 'cache-control': 'no-store' })
|
|
155
151
|
if (parsed.action.kind !== 'import' && Buffer.byteLength(body.raw) > ACTION_LIMIT) {
|
|
156
|
-
return
|
|
152
|
+
return writeJson(res, 413, { ok: false, error: 'body-too-large' }, { 'cache-control': 'no-store' })
|
|
157
153
|
}
|
|
158
|
-
|
|
154
|
+
writeJson(res, 200, service.apply(parsed.requestId, parsed.action), { 'cache-control': 'no-store' })
|
|
159
155
|
} catch (error) {
|
|
160
156
|
const message = error instanceof Error ? error.message : String(error)
|
|
161
|
-
|
|
157
|
+
writeJson(res, message === 'body-too-large' ? 413 : 400, { ok: false, error: message }, { 'cache-control': 'no-store' })
|
|
162
158
|
}
|
|
163
159
|
},
|
|
164
160
|
}
|
package/src/host-service.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy'
|
|
2
2
|
import { nextRunAtMs } from './core/schedule.ts'
|
|
3
|
-
import type
|
|
4
|
-
import { HostTaskLedger, type OpenedRun } from './host-ledger.ts'
|
|
3
|
+
import { HostTaskLedger, type OpenedRun, type OpenExecutionReference } from './host-ledger.ts'
|
|
5
4
|
import { HostExecutionRunner, SessionLaunchError, type SessionCommandDispatcher, type SessionSummary } from './host-runner.ts'
|
|
6
5
|
import { PowerInhibitor } from './power-inhibitor.ts'
|
|
7
6
|
import type { TaskBoardAction, TaskBoardEventPayload, TaskBoardSnapshot } from './protocol.ts'
|
|
@@ -136,31 +135,43 @@ export class TaskBoardHostService {
|
|
|
136
135
|
|
|
137
136
|
private async pollSessions(): Promise<void> {
|
|
138
137
|
if (this.disposed) return
|
|
139
|
-
if (!this.active &&
|
|
138
|
+
if (!this.active && this.ledger.runtimeView().openExecutions.length === 0) return
|
|
140
139
|
const running = await this.runner.listRunning()
|
|
141
140
|
const previous = this.power.snapshot()
|
|
141
|
+
if (!running.known) {
|
|
142
|
+
this.power.updateReasons({
|
|
143
|
+
runningSessions: previous.runningSessions,
|
|
144
|
+
armedSchedules: this.ledger.armedScheduleCount(),
|
|
145
|
+
sessionStateKnown: false,
|
|
146
|
+
})
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
// Read after the RPC so executions attached while it was in flight are
|
|
150
|
+
// included in this pass, matching the former full-state snapshot timing.
|
|
151
|
+
const runtime = this.ledger.runtimeView()
|
|
142
152
|
this.power.updateReasons({
|
|
143
|
-
runningSessions: running.
|
|
144
|
-
armedSchedules:
|
|
145
|
-
sessionStateKnown:
|
|
153
|
+
runningSessions: running.count,
|
|
154
|
+
armedSchedules: runtime.armedSchedules,
|
|
155
|
+
sessionStateKnown: true,
|
|
146
156
|
})
|
|
147
157
|
// No unconditional emit here: real changes already emit through the
|
|
148
158
|
// ledger subscription (settles) and the gated power listener above.
|
|
149
|
-
|
|
159
|
+
await this.reconcileExecutions(running.items, runtime.openExecutions)
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
/** Reuse the session list this poll already fetched: one list RPC per tick, not 1 + E. */
|
|
153
|
-
private async reconcileExecutions(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
private async reconcileExecutions(
|
|
164
|
+
sessions: readonly SessionSummary[],
|
|
165
|
+
executions: readonly OpenExecutionReference[],
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
for (const execution of executions) {
|
|
168
|
+
if (execution.sessionId === undefined) continue
|
|
169
|
+
try {
|
|
170
|
+
const result = await this.runner.inspect(execution.sessionId, execution.startedAt, sessions)
|
|
171
|
+
if (result.outcome === 'pending') continue
|
|
172
|
+
this.ledger.settle(execution.taskId, execution.executionId, result.outcome, 'error' in result ? result.error : undefined)
|
|
173
|
+
} catch {
|
|
174
|
+
// A transient inspection failure never settles a running execution.
|
|
164
175
|
}
|
|
165
176
|
}
|
|
166
177
|
}
|
|
@@ -175,22 +186,15 @@ export class TaskBoardHostService {
|
|
|
175
186
|
this.ledger.skipMissed(now)
|
|
176
187
|
return
|
|
177
188
|
}
|
|
178
|
-
for (const
|
|
179
|
-
if (task.archivedAt !== undefined) continue
|
|
180
|
-
const schedule = task.schedule
|
|
181
|
-
if (schedule === undefined || !schedule.enabled || schedule.nextRunAt === undefined || schedule.nextRunAt > now) continue
|
|
189
|
+
for (const schedule of this.ledger.dueSchedules(now)) {
|
|
182
190
|
const next = nextRunAtMs(schedule.cron, schedule.nextRunAt)
|
|
183
|
-
const opened = this.ledger.openScheduled(
|
|
191
|
+
const opened = this.ledger.openScheduled(schedule.taskId, next, now)
|
|
184
192
|
if (opened !== undefined) this.scheduleLaunch(opened)
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
private armedSchedules(): number {
|
|
189
|
-
return this.ledger.
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private hasOpenExecutions(): boolean {
|
|
193
|
-
return this.ledger.state().tasks.some(task => task.executions.some(execution => execution.endedAt === undefined))
|
|
197
|
+
return this.ledger.armedScheduleCount()
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
private scheduleLaunch(opened: OpenedRun): void {
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/http.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Shared JSON body/response helpers for the host route families: one strict
|
|
4
|
+
* bounded body reader, one lenient bounded body reader, one JSON object
|
|
5
|
+
* narrow, and one JSON writer. Previously these were copy-pasted across the
|
|
6
|
+
* package route files (routes.ts, update-routes.ts, mobile-api.ts, and each
|
|
7
|
+
* family's route module) with drifting contracts: body caps ranging 4 KiB to
|
|
8
|
+
* 1 MiB and four distinct overflow behaviors (reject, undefined, null, throw).
|
|
9
|
+
*
|
|
10
|
+
* Packages receive this file as a generated copy via scripts/sync-shared.mjs;
|
|
11
|
+
* edit this shared source and re-run the sync instead of editing a copy.
|
|
12
|
+
* Consumer code is migrated onto it in follow-up waves; no call site changes
|
|
13
|
+
* belong in the same change as its introduction.
|
|
14
|
+
* @module dsh-web-ui-shared/host/http
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'node:http'
|
|
18
|
+
|
|
19
|
+
/** Default body cap for readJsonBody: 64 KiB. */
|
|
20
|
+
const DEFAULT_JSON_BODY_MAX_BYTES = 64 * 1024
|
|
21
|
+
|
|
22
|
+
/** Family-default JSON response headers; callers may append or override. */
|
|
23
|
+
const JSON_HEADERS = {
|
|
24
|
+
'content-type': 'application/json; charset=utf-8',
|
|
25
|
+
'referrer-policy': 'no-referrer',
|
|
26
|
+
} satisfies OutgoingHttpHeaders
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Strict bounded body reader: parse a request body of at most maxBytes as
|
|
30
|
+
* JSON.
|
|
31
|
+
* @throws 'body too large' past the cap, or the JSON.parse error for an
|
|
32
|
+
* invalid or empty payload.
|
|
33
|
+
*/
|
|
34
|
+
export async function readBoundedJson(req: IncomingMessage, maxBytes: number): Promise<unknown> {
|
|
35
|
+
const chunks: Buffer[] = []
|
|
36
|
+
let size = 0
|
|
37
|
+
for await (const chunk of req) {
|
|
38
|
+
const buffer = chunk as Buffer
|
|
39
|
+
size += buffer.length
|
|
40
|
+
if (size > maxBytes) throw new Error('body too large')
|
|
41
|
+
chunks.push(buffer)
|
|
42
|
+
}
|
|
43
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Lenient bounded body reader: parse a request body as JSON, or null on an
|
|
48
|
+
* empty body, invalid JSON, or a body past maxBytes (default 64 KiB).
|
|
49
|
+
* Overflow destroys the request instead of draining the remainder (no drain
|
|
50
|
+
* call, matching the current repo-wide behavior); callers must not keep
|
|
51
|
+
* reading the request afterwards. With objectOnly, non-JSON-object payloads
|
|
52
|
+
* also yield null.
|
|
53
|
+
*/
|
|
54
|
+
export async function readJsonBody(
|
|
55
|
+
req: IncomingMessage,
|
|
56
|
+
opts: { maxBytes?: number; objectOnly?: boolean } = {},
|
|
57
|
+
): Promise<unknown | null> {
|
|
58
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_JSON_BODY_MAX_BYTES
|
|
59
|
+
const chunks: Buffer[] = []
|
|
60
|
+
let size = 0
|
|
61
|
+
for await (const chunk of req) {
|
|
62
|
+
const buffer = chunk as Buffer
|
|
63
|
+
size += buffer.length
|
|
64
|
+
if (size > maxBytes) {
|
|
65
|
+
req.destroy()
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
chunks.push(buffer)
|
|
69
|
+
}
|
|
70
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
71
|
+
if (text === '') return null
|
|
72
|
+
try {
|
|
73
|
+
const parsed: unknown = JSON.parse(text)
|
|
74
|
+
if (opts.objectOnly && !isJsonObject(parsed)) return null
|
|
75
|
+
return parsed
|
|
76
|
+
} catch {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether a value is a JSON object: typeof object, not null, not an array. */
|
|
82
|
+
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
83
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Narrow a value to a JSON object, or undefined when it is not one. */
|
|
87
|
+
export function asJsonObject(value: unknown): Record<string, unknown> | undefined {
|
|
88
|
+
return isJsonObject(value) ? value : undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Write one JSON response. Default headers are the family defaults
|
|
93
|
+
* (content-type and referrer-policy); caller headers are appended or
|
|
94
|
+
* override them.
|
|
95
|
+
*/
|
|
96
|
+
export function writeJson(
|
|
97
|
+
res: ServerResponse,
|
|
98
|
+
status: number,
|
|
99
|
+
body: unknown,
|
|
100
|
+
headers: OutgoingHttpHeaders = {},
|
|
101
|
+
): void {
|
|
102
|
+
const payload = JSON.stringify(body)
|
|
103
|
+
res.writeHead(status, { ...JSON_HEADERS, ...headers })
|
|
104
|
+
res.end(payload)
|
|
105
|
+
}
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Execution service: runs a task through dsh's real session machinery.
|
|
3
|
-
*
|
|
4
|
-
* The board's "run" button must make dsh actually work, not fake a status:
|
|
5
|
-
* the service connects a real session (workspace blank-session reuse or
|
|
6
|
-
* `session.create` on the host via the workspaces service), renames it to
|
|
7
|
-
* the task title, sends the task prompt with `session.prompt`, and then
|
|
8
|
-
* watches the session's conversation snapshot until its turn settles. The
|
|
9
|
-
* task board controller consumes {@link ExecutionEvent}s to move the card
|
|
10
|
-
* running → done/failed and to keep the execution record.
|
|
11
|
-
*
|
|
12
|
-
* Deliberately framework-free: the runtime faces are declared structurally
|
|
13
|
-
* (a narrow slice of the real `ctx.sessions` / `ctx.workspaces` contracts)
|
|
14
|
-
* so tests drive it with plain fakes.
|
|
15
|
-
*/
|
|
16
|
-
import type { ExecutionRecord, TaskRecord } from './tasks.ts';
|
|
17
|
-
/** One list-row summary the execution service reads (the narrow slice of a SessionSummary). */
|
|
18
|
-
export interface ExecutionSessionSummary {
|
|
19
|
-
running: boolean;
|
|
20
|
-
completed?: boolean;
|
|
21
|
-
/** Empty-log bit: the preset can only be recomposed while the session is blank. */
|
|
22
|
-
blank?: boolean;
|
|
23
|
-
/** The preset the session currently runs (absent on deployments without presets). */
|
|
24
|
-
agentPreset?: string;
|
|
25
|
-
}
|
|
26
|
-
/** The narrow sessions face the service needs. */
|
|
27
|
-
export interface SessionsExecutionFace {
|
|
28
|
-
list: {
|
|
29
|
-
getSnapshot(): {
|
|
30
|
-
/** Baseline arrival lifecycle — 'pending' until the host list has loaded. */
|
|
31
|
-
phase: 'pending' | 'ready';
|
|
32
|
-
byId: Record<string, ExecutionSessionSummary>;
|
|
33
|
-
};
|
|
34
|
-
subscribe(fn: () => void): () => void;
|
|
35
|
-
};
|
|
36
|
-
binding(id: string): {
|
|
37
|
-
session: SessionDriver;
|
|
38
|
-
} | undefined;
|
|
39
|
-
/** Record a host-confirmed preset switch so the list label moves immediately. */
|
|
40
|
-
noteAgentPreset?(sessionId: string, agentPreset: string): void;
|
|
41
|
-
}
|
|
42
|
-
/** The narrow agent-preset wire face the service needs (`agentPreset.select`). */
|
|
43
|
-
export interface PresetsExecutionFace {
|
|
44
|
-
/** Recompose a blank session's agent from a preset. */
|
|
45
|
-
select(sessionId: string, agentPreset: string): Promise<{
|
|
46
|
-
ok: true;
|
|
47
|
-
} | {
|
|
48
|
-
ok: false;
|
|
49
|
-
error: unknown;
|
|
50
|
-
}>;
|
|
51
|
-
}
|
|
52
|
-
/** The narrow workspaces face the service needs. */
|
|
53
|
-
export interface WorkspacesExecutionFace {
|
|
54
|
-
list: {
|
|
55
|
-
getSnapshot(): {
|
|
56
|
-
items: readonly {
|
|
57
|
-
workspaceId: string;
|
|
58
|
-
}[];
|
|
59
|
-
recentWorkspaceId: string | undefined;
|
|
60
|
-
};
|
|
61
|
-
};
|
|
62
|
-
connectWorkspace(workspaceId: string): Promise<string>;
|
|
63
|
-
}
|
|
64
|
-
/** One raw session-history event narrowed to the failure signal reconcile needs. */
|
|
65
|
-
export interface ExecutionHistoryEvent {
|
|
66
|
-
type: string;
|
|
67
|
-
data?: unknown;
|
|
68
|
-
}
|
|
69
|
-
/** Optional raw-history face used to detect failures of never-opened sessions. */
|
|
70
|
-
export interface HistoryExecutionFace {
|
|
71
|
-
loadTail(sessionId: string): Promise<{
|
|
72
|
-
events: readonly ExecutionHistoryEvent[];
|
|
73
|
-
} | undefined>;
|
|
74
|
-
}
|
|
75
|
-
/** The behavior verbs the service invokes on an execution session. */
|
|
76
|
-
export interface SessionDriver {
|
|
77
|
-
rename(title: string): Promise<unknown>;
|
|
78
|
-
prompt(content: readonly unknown[], mode: 'queue'): Promise<{
|
|
79
|
-
ok: true;
|
|
80
|
-
} | {
|
|
81
|
-
ok: false;
|
|
82
|
-
error: unknown;
|
|
83
|
-
}>;
|
|
84
|
-
/**
|
|
85
|
-
* Admit one slash-command line against the session's agent (the
|
|
86
|
-
* `/permission <id>` mechanism). `matched` reports whether a command
|
|
87
|
-
* claimed the line.
|
|
88
|
-
*/
|
|
89
|
-
command(line: string): Promise<{
|
|
90
|
-
ok: true;
|
|
91
|
-
matched: boolean;
|
|
92
|
-
} | {
|
|
93
|
-
ok: false;
|
|
94
|
-
error: unknown;
|
|
95
|
-
}>;
|
|
96
|
-
getSnapshot(): {
|
|
97
|
-
running: boolean;
|
|
98
|
-
lastAgentError: string | null;
|
|
99
|
-
turnEnds: ReadonlyMap<number, number>;
|
|
100
|
-
};
|
|
101
|
-
subscribe(fn: () => void): () => void;
|
|
102
|
-
}
|
|
103
|
-
/** Everything the service needs from the runtime. */
|
|
104
|
-
export interface ExecutionEnvironment {
|
|
105
|
-
sessions: SessionsExecutionFace;
|
|
106
|
-
workspaces: WorkspacesExecutionFace;
|
|
107
|
-
/** Agent-preset wire face; absent on deployments without preset support. */
|
|
108
|
-
presets?: PresetsExecutionFace;
|
|
109
|
-
/** Raw-history reader for failure detection of never-opened sessions. */
|
|
110
|
-
history?: HistoryExecutionFace;
|
|
111
|
-
}
|
|
112
|
-
/** Outcome events the service emits to the controller. */
|
|
113
|
-
export type ExecutionEvent = {
|
|
114
|
-
kind: 'started';
|
|
115
|
-
taskId: string;
|
|
116
|
-
executionId: string;
|
|
117
|
-
sessionId: string;
|
|
118
|
-
} | {
|
|
119
|
-
kind: 'settled';
|
|
120
|
-
taskId: string;
|
|
121
|
-
executionId: string;
|
|
122
|
-
outcome: 'succeeded' | 'failed' | 'cancelled';
|
|
123
|
-
error?: string;
|
|
124
|
-
};
|
|
125
|
-
/**
|
|
126
|
-
* Run one task to completion (or to a settled failure).
|
|
127
|
-
*
|
|
128
|
-
* @param task - the task being executed.
|
|
129
|
-
* @param execution - the freshly opened execution record (id + start time).
|
|
130
|
-
* @param onEvent - callback for started/settled events.
|
|
131
|
-
* @returns resolves when the run settles (or fails to start); never rejects —
|
|
132
|
-
* every failure path is reported as a settled event.
|
|
133
|
-
*/
|
|
134
|
-
export declare class ExecutionService {
|
|
135
|
-
private readonly env;
|
|
136
|
-
/**
|
|
137
|
-
* In-flight preset switches keyed by `sessionId\u0000mode`. The session
|
|
138
|
-
* list mirror can lag behind a just-applied switch, so concurrent runs
|
|
139
|
-
* against one blank session would otherwise each issue the same select
|
|
140
|
-
* RPC (an amplified storm); sharing one in-flight call keeps the wire to
|
|
141
|
-
* a single request. The settled entry is forgotten so a later run can
|
|
142
|
-
* switch again.
|
|
143
|
-
*/
|
|
144
|
-
private readonly presetSwitches;
|
|
145
|
-
/** @param env - the runtime faces (real or fake). */
|
|
146
|
-
constructor(env: ExecutionEnvironment);
|
|
147
|
-
run(task: TaskRecord, execution: ExecutionRecord, onEvent: (event: ExecutionEvent) => void): Promise<void>;
|
|
148
|
-
/**
|
|
149
|
-
* Recompose the execution session's agent from the task-pinned preset.
|
|
150
|
-
* No-op when the task pins none or the session already runs it; fails the
|
|
151
|
-
* run when the session is no longer blank, the preset face is missing, or
|
|
152
|
-
* the wire refuses.
|
|
153
|
-
*/
|
|
154
|
-
private applyMode;
|
|
155
|
-
/**
|
|
156
|
-
* One in-flight `select` per (session, preset): concurrent runs against
|
|
157
|
-
* the same blank session share the same wire call instead of each issuing
|
|
158
|
-
* a duplicate RPC. The entry is removed once the call settles, so a later
|
|
159
|
-
* run (after a shared failure, say) issues a fresh switch.
|
|
160
|
-
*/
|
|
161
|
-
private switchPreset;
|
|
162
|
-
/**
|
|
163
|
-
* Apply the task-pinned permission preset through the `/permission <id>`
|
|
164
|
-
* slash command. No-op when the task pins none; fails the run when the
|
|
165
|
-
* admission is rejected or no command claimed the line.
|
|
166
|
-
*/
|
|
167
|
-
private applyPermission;
|
|
168
|
-
/**
|
|
169
|
-
* Inspect a reloaded/background task that was left 'running' and emit a
|
|
170
|
-
* settled event when its session already finished.
|
|
171
|
-
*
|
|
172
|
-
* A session that was never opened keeps a cold conversation snapshot (the
|
|
173
|
-
* runtime only maintains the window for the staged/current session), so the
|
|
174
|
-
* settled outcome is decided by the strongest available signal, in order:
|
|
175
|
-
* 1. the list summary — missing session → cancelled; still running → pending;
|
|
176
|
-
* 2. a warm conversation snapshot → `lastAgentError` decides failed/succeeded;
|
|
177
|
-
* 3. the raw history tail (when a history face is wired) — a `turn/end`
|
|
178
|
-
* error reason proves failure;
|
|
179
|
-
* 4. otherwise a finished session counts as succeeded.
|
|
180
|
-
*
|
|
181
|
-
* @param task - a task whose latest execution has no endedAt.
|
|
182
|
-
* @returns a settled event when the session state proves completion, else undefined.
|
|
183
|
-
*/
|
|
184
|
-
reconcile(task: TaskRecord): Promise<ExecutionEvent | undefined>;
|
|
185
|
-
/** Best-effort failure probe over the raw history tail (false when unavailable). */
|
|
186
|
-
private historyShowsFailure;
|
|
187
|
-
private connectSession;
|
|
188
|
-
private driverOf;
|
|
189
|
-
private sendPrompt;
|
|
190
|
-
/**
|
|
191
|
-
* Subscribe to the execution session and settle the run once the accepted
|
|
192
|
-
* turn completes (turn counter advanced past the acceptance baseline and
|
|
193
|
-
* the session is no longer running). Never settles while the session is
|
|
194
|
-
* still running; unsubscribes on settle.
|
|
195
|
-
*/
|
|
196
|
-
private watchForSettlement;
|
|
197
|
-
}
|