@linxin666/dsh-client-ui-task-board 0.4.2 → 0.4.4
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.i18n.yaml +2 -2
- package/README.md +21 -13
- package/README.zh.md +21 -13
- package/lib/client.js +1566 -929
- package/lib/client.js.map +1 -1
- package/lib/index.js +934 -176
- package/lib/types/client/board/TaskForm.d.ts +19 -1
- package/lib/types/client/board/preset-label.d.ts +22 -0
- package/lib/types/client/host-api.d.ts +24 -1
- package/lib/types/client/index.d.ts +15 -1
- package/lib/types/client/locales.d.ts +38 -2
- package/lib/types/client/native-panel.d.ts +62 -0
- package/lib/types/client/schedule-zone.d.ts +47 -0
- package/lib/types/client/settings-entry-form.d.ts +49 -0
- package/lib/types/core/controller.d.ts +42 -2
- package/lib/types/core/degraded-reason.d.ts +46 -0
- package/lib/types/core/package-name.d.ts +9 -0
- package/lib/types/core/schedule.d.ts +95 -20
- package/lib/types/core/subtask.d.ts +30 -11
- package/lib/types/core/tasks.d.ts +26 -2
- package/lib/types/core/use-cases/task-create.d.ts +2 -1
- package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host-ledger.d.ts +65 -4
- package/lib/types/host-runner.d.ts +76 -3
- package/lib/types/host-service.d.ts +77 -5
- package/lib/types/index.d.ts +11 -1
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/protocol.d.ts +9 -3
- package/package.json +26 -23
- package/src/client/board/LinkSubtaskModal.tsx +21 -11
- package/src/client/board/NewTaskModal.tsx +271 -150
- package/src/client/board/TaskCard.tsx +24 -5
- package/src/client/board/TaskDetail.tsx +83 -33
- package/src/client/board/TaskForm.tsx +48 -3
- package/src/client/board/preset-label.ts +44 -0
- package/src/client/board.module.css +130 -136
- package/src/client/host-api.ts +109 -6
- package/src/client/index.ts +50 -18
- package/src/client/locales.ts +82 -8
- package/src/client/native-panel.tsx +124 -0
- package/src/client/schedule-zone.ts +135 -0
- package/src/client/settings-entry-form.ts +142 -0
- package/src/core/controller.ts +66 -4
- package/src/core/degraded-reason.ts +81 -0
- package/src/core/package-name.ts +9 -0
- package/src/core/schedule.ts +211 -55
- package/src/core/store.ts +10 -1
- package/src/core/subtask.ts +77 -22
- package/src/core/tasks.ts +38 -4
- package/src/core/use-cases/task-create.ts +14 -2
- package/src/core/use-cases/task-schedule.ts +17 -6
- package/src/core/use-cases/task-update.ts +4 -1
- package/src/host/agent-tools.ts +40 -8
- package/src/host-ledger.ts +223 -23
- package/src/host-routes.ts +37 -1
- package/src/host-runner.ts +164 -14
- package/src/host-service.ts +232 -39
- package/src/index.ts +23 -1
- package/src/mount-once.ts +107 -13
- package/src/protocol.ts +27 -8
- package/lib/types/client/board-mount.d.ts +0 -23
- package/lib/types/client/body-mutations.d.ts +0 -45
- package/lib/types/client/panel-mount-core.d.ts +0 -56
- package/lib/types/client/sidebar-entry-core.d.ts +0 -55
- package/lib/types/client/sidebar-entry.d.ts +0 -29
- package/lib/types/host/run-guarded.d.ts +0 -39
- package/src/client/board-mount.tsx +0 -43
- package/src/client/body-mutations.ts +0 -133
- package/src/client/panel-mount-core.ts +0 -155
- package/src/client/sidebar-entry-core.ts +0 -237
- package/src/client/sidebar-entry.ts +0 -53
- package/src/host/run-guarded.ts +0 -76
|
@@ -24,28 +24,40 @@ export declare const DEFAULT_SUBTASK_DEPTH = 1;
|
|
|
24
24
|
export declare function normalizeSubtaskDepth(value: unknown): number;
|
|
25
25
|
/** Whether an unknown value is a settled execution outcome. */
|
|
26
26
|
export declare function isExecutionOutcome(value: unknown): value is ExecutionOutcome;
|
|
27
|
+
/**
|
|
28
|
+
* Reusable lineage index over ONE task list: the id lookup and the direct
|
|
29
|
+
* children per parent. A caller that runs several lineage queries over the same
|
|
30
|
+
* ledger (the link-subtask candidate gate runs one per task) builds this once
|
|
31
|
+
* instead of rescanning the list per query.
|
|
32
|
+
*/
|
|
33
|
+
export interface LineageIndex {
|
|
34
|
+
readonly byId: ReadonlyMap<string, TaskRecord>;
|
|
35
|
+
readonly childrenByParent: ReadonlyMap<string, readonly TaskRecord[]>;
|
|
36
|
+
}
|
|
37
|
+
/** Build the reusable index for a task list (ledger order preserved). */
|
|
38
|
+
export declare function buildLineageIndex(tasks: readonly TaskRecord[]): LineageIndex;
|
|
27
39
|
/**
|
|
28
40
|
* Nearest-first ancestor chain of a task. The walk is cycle-safe and bounded
|
|
29
41
|
* by the ledger size rather than by the deployment maximum: a chain stored
|
|
30
42
|
* under a larger limit survives that limit being lowered, and restore and
|
|
31
43
|
* inheritance must still see all of it.
|
|
32
44
|
*/
|
|
33
|
-
export declare function ancestorChain(tasks: readonly TaskRecord[], task: TaskRecord): TaskRecord[];
|
|
45
|
+
export declare function ancestorChain(tasks: readonly TaskRecord[], task: TaskRecord, lineage?: LineageIndex): TaskRecord[];
|
|
34
46
|
/** Root-to-task depth: a root task is 0, its subtask 1, and so on. */
|
|
35
|
-
export declare function taskDepth(tasks: readonly TaskRecord[], id: string): number;
|
|
47
|
+
export declare function taskDepth(tasks: readonly TaskRecord[], id: string, lineage?: LineageIndex): number;
|
|
36
48
|
/** Direct subtasks of a task, in ledger order. */
|
|
37
|
-
export declare function directSubtasks(tasks: readonly TaskRecord[], id: string): TaskRecord[];
|
|
49
|
+
export declare function directSubtasks(tasks: readonly TaskRecord[], id: string, lineage?: LineageIndex): TaskRecord[];
|
|
38
50
|
/**
|
|
39
51
|
* Descendants of a task in breadth-first order, bounded by maxSubtaskDepth
|
|
40
52
|
* total depth. The visited set keeps a malformed ledger from looping.
|
|
41
53
|
*/
|
|
42
|
-
export declare function descendantTasks(tasks: readonly TaskRecord[], id: string, maxSubtaskDepth: number): TaskRecord[];
|
|
54
|
+
export declare function descendantTasks(tasks: readonly TaskRecord[], id: string, maxSubtaskDepth: number, lineage?: LineageIndex): TaskRecord[];
|
|
43
55
|
/**
|
|
44
56
|
* Height of a task's own subtree (0 for a leaf), bounded by the hard maximum.
|
|
45
57
|
* The visited set makes the walk terminate on a cyclic hand-edited ledger
|
|
46
58
|
* instead of recursing until the stack overflows.
|
|
47
59
|
*/
|
|
48
|
-
export declare function subtreeHeight(tasks: readonly TaskRecord[], id: string, visited?: ReadonlySet<string
|
|
60
|
+
export declare function subtreeHeight(tasks: readonly TaskRecord[], id: string, visited?: ReadonlySet<string>, lineage?: LineageIndex): number;
|
|
49
61
|
/** Why a parent link was refused; also the source of the Host error text. */
|
|
50
62
|
export type ParentLinkRejection = 'unknown-task' | 'unknown-parent' | 'archived-parent' | 'self-parent' | 'cycle' | 'depth-exceeded';
|
|
51
63
|
export interface ParentLinkCheck {
|
|
@@ -61,7 +73,7 @@ export interface ParentLinkCheck {
|
|
|
61
73
|
* and the resulting depth (parent depth + 1 + the child own subtree height)
|
|
62
74
|
* must stay within maxSubtaskDepth.
|
|
63
75
|
*/
|
|
64
|
-
export declare function checkParentLink(tasks: readonly TaskRecord[], childId: string, parentId: string | null, maxSubtaskDepth: number): ParentLinkCheck;
|
|
76
|
+
export declare function checkParentLink(tasks: readonly TaskRecord[], childId: string, parentId: string | null, maxSubtaskDepth: number, lineage?: LineageIndex): ParentLinkCheck;
|
|
65
77
|
/**
|
|
66
78
|
* The tasks one run request opens executions for: the requested task first,
|
|
67
79
|
* then its on-board descendants within the configured depth. Archived tasks
|
|
@@ -110,13 +122,20 @@ export declare function effectiveTaskPermission(task: TaskRecord): TaskRecord['p
|
|
|
110
122
|
*/
|
|
111
123
|
/**
|
|
112
124
|
* The immutable teammate name for one team-mode member. Teammate names are
|
|
113
|
-
* lower-kebab-case and must stay unique for the lifetime of the Team
|
|
114
|
-
* run-group token
|
|
115
|
-
*
|
|
116
|
-
*
|
|
125
|
+
* lower-kebab-case and must stay unique for the lifetime of the Team: the
|
|
126
|
+
* run-group token separates two runs of the same tree, and the member
|
|
127
|
+
* discriminator separates two members of one run that reduce to the same slug.
|
|
128
|
+
*
|
|
129
|
+
* The discriminator is not cosmetic. A title with no ASCII word characters (a
|
|
130
|
+
* CJK title, for example) carries no slug at all, so every such title of one
|
|
131
|
+
* run group collides on the generic prefix; Agent Teams answers every member
|
|
132
|
+
* after the first with `teammate name "..." was already used in this Team`,
|
|
133
|
+
* and those subtasks never start. Two ASCII titles that share a slug
|
|
134
|
+
* (`AI 强度预测` next to `AI 配比优化`) collide the same way.
|
|
117
135
|
* @param title - the member task's title.
|
|
118
136
|
* @param token - the run group id (the task id when no group is recorded).
|
|
137
|
+
* @param memberId - the member task's id, which makes the name unique in the run.
|
|
119
138
|
* @returns the teammate name handed to the Agent Teams service.
|
|
120
139
|
*/
|
|
121
|
-
export declare function teammateName(title: string, token: string): string;
|
|
140
|
+
export declare function teammateName(title: string, token: string, memberId?: string): string;
|
|
122
141
|
export declare function resolveExecutionTargets(task: TaskRecord, tasks: readonly TaskRecord[], maxSubtaskDepth?: number): TaskRecord;
|
|
@@ -75,6 +75,13 @@ export interface ScheduleRule {
|
|
|
75
75
|
enabled: boolean;
|
|
76
76
|
/** 5-field cron expression: `分 时 日 月 周`. */
|
|
77
77
|
cron: string;
|
|
78
|
+
/**
|
|
79
|
+
* IANA zone the cron wall clock is read in (issue #1722). Absent means the
|
|
80
|
+
* Host process zone, which is what a rule written before zones were
|
|
81
|
+
* persisted keeps using; the Host stamps its own zone at ledger migration so
|
|
82
|
+
* a later `TZ` change cannot silently move an existing rule.
|
|
83
|
+
*/
|
|
84
|
+
timeZone?: string;
|
|
78
85
|
/** Next due instant (ms epoch); maintained by the scheduler/controller. */
|
|
79
86
|
nextRunAt: number | undefined;
|
|
80
87
|
/** Instant of the latest scheduled trigger (ms epoch). */
|
|
@@ -214,6 +221,16 @@ export interface TaskRecord {
|
|
|
214
221
|
* and a subtask's own permission pin cannot be honored in this mode.
|
|
215
222
|
*/
|
|
216
223
|
teamRun?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Start every execution of this task with dsh's built-in `/goal` command,
|
|
226
|
+
* armed with the composed execution prompt as its objective. The session then
|
|
227
|
+
* keeps working automatic continuation rounds until the agent marks the goal
|
|
228
|
+
* complete, and this board settles the execution on the goal's own end
|
|
229
|
+
* instead of at the first turn end. Absent means ON — the option is checked
|
|
230
|
+
* by default, so only an explicit `false` runs the task as a single plain
|
|
231
|
+
* turn. A session whose `/goal` command is refused still runs the prompt.
|
|
232
|
+
*/
|
|
233
|
+
goalRun?: boolean;
|
|
217
234
|
/**
|
|
218
235
|
* Frozen context snapshot for a continuation card; absent on plain tasks.
|
|
219
236
|
* Sanitized before it enters the ledger (redaction, slash-command taint,
|
|
@@ -282,14 +299,21 @@ export interface NewTaskInput {
|
|
|
282
299
|
reuseSession?: boolean;
|
|
283
300
|
/** Run the subtree as an Agent Team (Team Lead session plus one teammate per direct subtask). */
|
|
284
301
|
teamRun?: boolean;
|
|
302
|
+
/**
|
|
303
|
+
* Start the execution with dsh's built-in `/goal`. Absent/true keeps the
|
|
304
|
+
* default (goal run); an explicit false requests a single plain turn.
|
|
305
|
+
*/
|
|
306
|
+
goalRun?: boolean;
|
|
285
307
|
/**
|
|
286
308
|
* Optional scheduled-run rule requested at creation time (the new-task
|
|
287
|
-
* dialog): an enable flag
|
|
288
|
-
*
|
|
309
|
+
* dialog): an enable flag, a 5-field cron expression, and the IANA zone its
|
|
310
|
+
* wall clock is read in (absent means the Host zone). The create use case
|
|
311
|
+
* arms it only when enabled and the expression is valid.
|
|
289
312
|
*/
|
|
290
313
|
schedule?: {
|
|
291
314
|
enabled: boolean;
|
|
292
315
|
cron: string;
|
|
316
|
+
timeZone?: string;
|
|
293
317
|
};
|
|
294
318
|
/**
|
|
295
319
|
* Optional frozen context snapshot (goal/progress/next, sanitized by the
|
|
@@ -22,5 +22,6 @@ export interface CreateTaskResult {
|
|
|
22
22
|
* @param now - clock instant (ms epoch).
|
|
23
23
|
* @param id - minted task id.
|
|
24
24
|
* @param maxSubtaskDepth - deployment subtask depth limit.
|
|
25
|
+
* @param hostTimeZone - zone a rule that stores no zone of its own follows.
|
|
25
26
|
*/
|
|
26
|
-
export declare function applyCreateTask(tasks: readonly TaskRecord[], input: NewTaskInput, now: number, id: string, maxSubtaskDepth?: number): CreateTaskResult;
|
|
27
|
+
export declare function applyCreateTask(tasks: readonly TaskRecord[], input: NewTaskInput, now: number, id: string, maxSubtaskDepth?: number, hostTimeZone?: string): CreateTaskResult;
|
|
@@ -3,6 +3,8 @@ import { type TaskRecord } from '../tasks.ts';
|
|
|
3
3
|
export interface SetSchedulePatch {
|
|
4
4
|
enabled?: boolean;
|
|
5
5
|
cron?: string;
|
|
6
|
+
/** IANA zone for the cron wall clock; `null` clears it back to the Host zone. */
|
|
7
|
+
timeZone?: string | null;
|
|
6
8
|
}
|
|
7
9
|
/** Result of arming/disarming a rule. */
|
|
8
10
|
export interface SetScheduleResult {
|
|
@@ -12,15 +14,17 @@ export interface SetScheduleResult {
|
|
|
12
14
|
applied: boolean;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
|
-
* Set an on-board task's schedule rule. A blank or invalid cron, or
|
|
16
|
-
* archived task, is rejected (state untouched); an enabled
|
|
17
|
-
* next run instant immediately
|
|
17
|
+
* Set an on-board task's schedule rule. A blank or invalid cron, an unknown or
|
|
18
|
+
* archived task, or an unusable zone is rejected (state untouched); an enabled
|
|
19
|
+
* rule computes the next run instant immediately in the rule's own zone, a
|
|
20
|
+
* disabled one carries no next-run instant.
|
|
18
21
|
* @param tasks - current ledger.
|
|
19
22
|
* @param id - the task to schedule.
|
|
20
23
|
* @param patch - rule fields to change (absent fields keep their current value).
|
|
21
24
|
* @param now - clock instant (ms epoch).
|
|
25
|
+
* @param hostTimeZone - zone a rule with no stored zone follows.
|
|
22
26
|
*/
|
|
23
|
-
export declare function applySetSchedule(tasks: readonly TaskRecord[], id: string, patch: SetSchedulePatch, now: number): SetScheduleResult;
|
|
27
|
+
export declare function applySetSchedule(tasks: readonly TaskRecord[], id: string, patch: SetSchedulePatch, now: number, hostTimeZone: string): SetScheduleResult;
|
|
24
28
|
/**
|
|
25
29
|
* Roll a task's schedule rule forward (scheduler callback): persist the next
|
|
26
30
|
* due instant and the trigger instant. No-op for tasks without a rule (deleted
|
|
@@ -15,7 +15,7 @@ import type { TaskHandoverInput } from '../handover.ts';
|
|
|
15
15
|
* Editable fields on a task (the update patch surface). `freeze` replaces the
|
|
16
16
|
* continuation-card snapshot (restamping frozenAt); an explicit null clears it.
|
|
17
17
|
*/
|
|
18
|
-
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission' | 'model' | 'reuseSession' | 'teamRun'>> & {
|
|
18
|
+
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission' | 'model' | 'reuseSession' | 'teamRun' | 'goalRun'>> & {
|
|
19
19
|
freeze?: FreezeSnapshot & {
|
|
20
20
|
redacted?: boolean;
|
|
21
21
|
} | null;
|
|
@@ -23,11 +23,20 @@ export interface OpenExecutionReference {
|
|
|
23
23
|
readonly executionId: string;
|
|
24
24
|
readonly sessionId: string | undefined;
|
|
25
25
|
readonly startedAt: number;
|
|
26
|
+
/**
|
|
27
|
+
* True for a member of a team-mode run other than its Lead. A teammate is a
|
|
28
|
+
* durable member of the Team: it stays alive after its turn ends, so the
|
|
29
|
+
* roster may keep reporting its session as running, and the turn it completed
|
|
30
|
+
* is the only verdict it exposes.
|
|
31
|
+
*/
|
|
32
|
+
readonly teamMember: boolean;
|
|
26
33
|
}
|
|
27
34
|
/** Minimal value copy used by the Host scheduler. */
|
|
28
35
|
export interface DueScheduleReference {
|
|
29
36
|
readonly taskId: string;
|
|
30
37
|
readonly cron: string;
|
|
38
|
+
/** Zone this rule's wall clock is read in. */
|
|
39
|
+
readonly timeZone: string;
|
|
31
40
|
readonly nextRunAt: number;
|
|
32
41
|
}
|
|
33
42
|
/** Derived runtime data for one session-poll pass. */
|
|
@@ -116,6 +125,15 @@ export declare class HostTaskLedger {
|
|
|
116
125
|
runtimeView(): LedgerRuntimeView;
|
|
117
126
|
/** Count armed, non-archived schedules without cloning task histories. */
|
|
118
127
|
armedScheduleCount(): number;
|
|
128
|
+
/**
|
|
129
|
+
* The earliest armed `nextRunAt` strictly after the supplied Host time, or
|
|
130
|
+
* undefined when no schedule is armed in the future. This is the instant the
|
|
131
|
+
* Host arms its native timer at, so the board wakes exactly when work is due
|
|
132
|
+
* instead of polling a fixed heartbeat.
|
|
133
|
+
* @param now - current Host time in ms epoch.
|
|
134
|
+
* @returns the nearest future trigger, or undefined when none is armed.
|
|
135
|
+
*/
|
|
136
|
+
nextArmedRunAt(now: number): number | undefined;
|
|
119
137
|
/** Return value-only references for schedules due at the supplied Host time. */
|
|
120
138
|
dueSchedules(now: number): DueScheduleReference[];
|
|
121
139
|
subscribe(listener: () => void): () => void;
|
|
@@ -182,13 +200,56 @@ export declare class HostTaskLedger {
|
|
|
182
200
|
* (failure dominates, then cancellation).
|
|
183
201
|
*/
|
|
184
202
|
private settleCascade;
|
|
203
|
+
/**
|
|
204
|
+
* Close every run that is already decided, and fold every lineage whose
|
|
205
|
+
* members are all settled. The Host poll calls this on every tick, so a run
|
|
206
|
+
* can no longer be left in the running column by a settle nobody observed:
|
|
207
|
+
*
|
|
208
|
+
* - A team run is governed by its Lead. Once the Lead's own outcome is
|
|
209
|
+
* recorded, its still-open members are settled with that verdict. A
|
|
210
|
+
* teammate is a durable member of the Team and may never report a turn of
|
|
211
|
+
* its own, and without this the whole lineage waits on it forever.
|
|
212
|
+
* - A cascade parent whose members are all settled is finalized here even
|
|
213
|
+
* when the settle that completed the tree happened in another process, or
|
|
214
|
+
* was interrupted between its child write and its parent write.
|
|
215
|
+
*
|
|
216
|
+
* Idempotent: a run that is already folded is left untouched, so a caller may
|
|
217
|
+
* invoke it on every tick.
|
|
218
|
+
* @param persist - false to leave the document for the caller to commit.
|
|
219
|
+
* @returns whether the document changed.
|
|
220
|
+
*/
|
|
221
|
+
finalizeReadyRuns(persist?: boolean): boolean;
|
|
222
|
+
/**
|
|
223
|
+
* The verdict a team run's Lead has already recorded: its own turn outcome
|
|
224
|
+
* once that turn ended, or the folded outcome once its card settled.
|
|
225
|
+
*/
|
|
226
|
+
private leadVerdict;
|
|
227
|
+
/**
|
|
228
|
+
* Settle every still-open member of a team run with the Lead's verdict.
|
|
229
|
+
* A teammate that already reported its own outcome keeps it: the ordinary
|
|
230
|
+
* fold still lets a failure dominate.
|
|
231
|
+
* @param leadId - the Lead card's task id (never settled here).
|
|
232
|
+
* @param groupId - the team run's group.
|
|
233
|
+
* @param verdict - the Lead's outcome, inherited by members that never reported.
|
|
234
|
+
* @param error - the Lead's failure text, inherited with a failed verdict.
|
|
235
|
+
* @param now - clock instant (ms epoch).
|
|
236
|
+
* @returns whether the document changed.
|
|
237
|
+
*/
|
|
238
|
+
private closeTeamMembers;
|
|
185
239
|
private repairSchedules;
|
|
186
240
|
private reconcileInterruptedStarts;
|
|
187
241
|
/**
|
|
188
|
-
* Field-preserving
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
242
|
+
* Field-preserving migration of a pre-v4 document. It first proves every task
|
|
243
|
+
* row is structurally valid, so a document that would silently drop or coerce
|
|
244
|
+
* rows fails loudly instead (no quarantined-empty restart), and then reuses
|
|
245
|
+
* the current normalization.
|
|
246
|
+
*
|
|
247
|
+
* v4 adds `ScheduleRule.timeZone`. A rule written before v4 was evaluated in
|
|
248
|
+
* whatever zone the Host process happened to report, so the migration stamps
|
|
249
|
+
* that zone onto every enabled rule: the trigger instant is preserved exactly
|
|
250
|
+
* (the stored `nextRunAt` already encodes the old zone), but the rule stops
|
|
251
|
+
* following a later `TZ` change, which is what made an existing schedule
|
|
252
|
+
* silently move when the Host's zone changed.
|
|
192
253
|
*/
|
|
193
254
|
private migrateLegacyDocument;
|
|
194
255
|
private load;
|
|
@@ -23,8 +23,18 @@ type ExecutionSessionId = SessionSummary['sessionId'];
|
|
|
23
23
|
export interface SessionCommandDispatcher {
|
|
24
24
|
execute(sessionId: ExecutionSessionId, line: string, signal: AbortSignal): Promise<CommandResult | undefined>;
|
|
25
25
|
}
|
|
26
|
-
export type ExecutionInspection =
|
|
26
|
+
export type ExecutionInspection =
|
|
27
|
+
/**
|
|
28
|
+
* No verdict yet. `unreadable` separates "the work may still be in progress"
|
|
29
|
+
* from "this session's history could not be read at all": a reader failure is
|
|
30
|
+
* not progress, and the caller decides how long to keep waiting before it
|
|
31
|
+
* reports the execution as undeterminable instead of holding a card open
|
|
32
|
+
* forever.
|
|
33
|
+
*/
|
|
34
|
+
{
|
|
27
35
|
outcome: 'pending';
|
|
36
|
+
unreadable?: true;
|
|
37
|
+
reason?: string;
|
|
28
38
|
} | {
|
|
29
39
|
outcome: 'succeeded';
|
|
30
40
|
} | {
|
|
@@ -64,6 +74,17 @@ export interface PromptContext {
|
|
|
64
74
|
peers?: readonly PromptPeer[];
|
|
65
75
|
/** True when those members run as teammates inside this session (Team Lead). */
|
|
66
76
|
team?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Scheduled-run provenance: the instant the cron rule actually fired and the
|
|
79
|
+
* IANA zone its wall clock was read in. Present only for a cron-triggered
|
|
80
|
+
* run, so the model can resolve an unqualified date in the run's own terms
|
|
81
|
+
* instead of guessing from the Host clock (issue #1722).
|
|
82
|
+
*/
|
|
83
|
+
schedule?: {
|
|
84
|
+
triggeredAt: number;
|
|
85
|
+
timeZone: string;
|
|
86
|
+
cron: string;
|
|
87
|
+
};
|
|
67
88
|
}
|
|
68
89
|
/**
|
|
69
90
|
* Build the execution prompt for one task.
|
|
@@ -72,6 +93,16 @@ export interface PromptContext {
|
|
|
72
93
|
* @returns the prompt text.
|
|
73
94
|
*/
|
|
74
95
|
export declare function promptText(task: TaskRecord, context?: PromptContext): string;
|
|
96
|
+
/**
|
|
97
|
+
* Build the objective the built-in `/goal` command receives. A prompt whose
|
|
98
|
+
* text would be read as a goal operation is prefixed with a neutral label: the
|
|
99
|
+
* instruction stays intact, and the command arms an objective instead of
|
|
100
|
+
* clearing, pausing, or editing an unrelated goal (or refusing for a missing
|
|
101
|
+
* one).
|
|
102
|
+
* @param prompt - the composed execution prompt.
|
|
103
|
+
* @returns the objective text for `/goal <objective>`.
|
|
104
|
+
*/
|
|
105
|
+
export declare function goalObjective(prompt: string): string;
|
|
75
106
|
export declare class HostExecutionRunner {
|
|
76
107
|
private readonly gateway;
|
|
77
108
|
private readonly commands?;
|
|
@@ -122,6 +153,37 @@ export declare class HostExecutionRunner {
|
|
|
122
153
|
* the same permission/model pins before the prompt.
|
|
123
154
|
*/
|
|
124
155
|
private pinAndPrompt;
|
|
156
|
+
/**
|
|
157
|
+
* Arm dsh's built-in goal on the session. The task prompt is queued FIRST, so
|
|
158
|
+
* the session's first turn carries the instruction verbatim; `/goal
|
|
159
|
+
* <objective>` then turns the same text into a persistent objective, and
|
|
160
|
+
* dsh's goal-round driver keeps starting continuation rounds until the agent
|
|
161
|
+
* marks it complete (see {@link goalVerdict} for how the run settles).
|
|
162
|
+
*
|
|
163
|
+
* Opt-out: a task whose `goalRun` is an explicit false runs one plain turn.
|
|
164
|
+
* A refusal (no command dispatcher, no `/goal` command in this cohort, an
|
|
165
|
+
* objective the command rejects) is reported and the run continues as that
|
|
166
|
+
* plain turn: the task was asked to run, and a missing goal mode must not
|
|
167
|
+
* lose the work.
|
|
168
|
+
*/
|
|
169
|
+
private armGoal;
|
|
170
|
+
/**
|
|
171
|
+
* The verdict an armed dsh goal imposes on an otherwise completed turn.
|
|
172
|
+
*
|
|
173
|
+
* A goal run is one session working many automatic continuation rounds:
|
|
174
|
+
* dsh's goal-round driver queues the next round whenever the agent goes
|
|
175
|
+
* idle, so the first completed `turn/end` is not the execution's result.
|
|
176
|
+
* Settling there would leave the card in `done` while the session keeps
|
|
177
|
+
* working, and a scheduled task would fire a second, concurrent session.
|
|
178
|
+
* The session projection is the authority: an `active` goal keeps the
|
|
179
|
+
* execution pending, a `blocked` goal fails it with the recorded reason, and
|
|
180
|
+
* any other phase (complete, paused, or no goal at all) leaves the turn
|
|
181
|
+
* verdict in place.
|
|
182
|
+
*
|
|
183
|
+
* An unreadable projection falls back to that turn verdict: a cohort whose
|
|
184
|
+
* gateway withdrew `session/projections` must not hang every execution.
|
|
185
|
+
*/
|
|
186
|
+
private goalVerdict;
|
|
125
187
|
listRunning(): Promise<{
|
|
126
188
|
known: true;
|
|
127
189
|
count: number;
|
|
@@ -129,6 +191,17 @@ export declare class HostExecutionRunner {
|
|
|
129
191
|
} | {
|
|
130
192
|
known: false;
|
|
131
193
|
}>;
|
|
132
|
-
/**
|
|
133
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Resolve an execution outcome from the session list and bounded history pages.
|
|
196
|
+
* @param sessionId - the session the execution runs in.
|
|
197
|
+
* @param startedAt - instant the execution opened; earlier events are not its own.
|
|
198
|
+
* @param sessions - roster from the caller's own poll, when it already has one.
|
|
199
|
+
* @param options.whileRunning - read the first completed turn even while the
|
|
200
|
+
* roster reports the session as running. A durable Agent Teams teammate
|
|
201
|
+
* stays alive after its turn ends, so `running` never clears for it and the
|
|
202
|
+
* turn it completed is the only verdict it will ever expose.
|
|
203
|
+
*/
|
|
204
|
+
inspect(sessionId: string, startedAt?: number, sessions?: readonly SessionSummary[], options?: {
|
|
205
|
+
whileRunning?: boolean;
|
|
206
|
+
}): Promise<ExecutionInspection>;
|
|
134
207
|
}
|
|
@@ -31,16 +31,40 @@ export interface TeamSpawnResult {
|
|
|
31
31
|
export interface TaskBoardTeamDispatcher {
|
|
32
32
|
spawn(input: TeamSpawnInput): Promise<TeamSpawnResult>;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Provenance of one cron-triggered cascade: when the rule fired and the zone
|
|
36
|
+
* its wall clock was read in. Passed into every launched prompt of that run so
|
|
37
|
+
* a scheduled job knows its own clock rather than inferring one.
|
|
38
|
+
*/
|
|
39
|
+
export interface ScheduledRunContext {
|
|
40
|
+
triggeredAt: number;
|
|
41
|
+
timeZone: string;
|
|
42
|
+
cron: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The native timer face the Host arms through. The cordis `timer` service
|
|
46
|
+
* (dsh-base's own `cordis-plugin-timer` row) provides it: its handles are
|
|
47
|
+
* registered on the owning fiber, so unloading the board clears every armed
|
|
48
|
+
* timer without this service tracking handles by hand. A composition that
|
|
49
|
+
* serves no timer service falls back to the process globals.
|
|
50
|
+
*/
|
|
51
|
+
export interface HostTimerFace {
|
|
52
|
+
timeout(callback: () => void, delay: number): () => void;
|
|
53
|
+
interval(callback: () => void, delay: number): () => void;
|
|
54
|
+
}
|
|
34
55
|
export declare class TaskBoardHostService {
|
|
35
56
|
readonly ledger: HostTaskLedger;
|
|
36
57
|
readonly runner: HostExecutionRunner;
|
|
37
58
|
readonly power: PowerInhibitor;
|
|
38
59
|
private readonly listeners;
|
|
39
|
-
|
|
40
|
-
private
|
|
60
|
+
/** The one recurring timer: the session-roster poll. */
|
|
61
|
+
private pollTimer;
|
|
62
|
+
/** The armed schedule timer, if a trigger is pending. */
|
|
63
|
+
private scheduleTimer;
|
|
64
|
+
/** The instant the armed schedule timer targets (ms epoch), for resume detection. */
|
|
65
|
+
private scheduleTarget;
|
|
41
66
|
private disposed;
|
|
42
67
|
private pollInFlight;
|
|
43
|
-
private tickInFlight;
|
|
44
68
|
private active;
|
|
45
69
|
/**
|
|
46
70
|
* Ids the last roster poll saw as present and idle; undefined while the
|
|
@@ -49,8 +73,15 @@ export declare class TaskBoardHostService {
|
|
|
49
73
|
* conversation instead of prompting into a session it cannot see.
|
|
50
74
|
*/
|
|
51
75
|
private idleSessionIds;
|
|
76
|
+
/**
|
|
77
|
+
* Consecutive unreadable-history polls per open execution (see
|
|
78
|
+
* {@link noteUnreadableInspection}). Cleared as soon as an inspection
|
|
79
|
+
* resolves, so a transient reader failure never fails a card.
|
|
80
|
+
*/
|
|
81
|
+
private readonly unreadablePolls;
|
|
52
82
|
private preventIdleSleep;
|
|
53
83
|
private readonly team;
|
|
84
|
+
private readonly timers;
|
|
54
85
|
private lastPowerJson;
|
|
55
86
|
private readonly now;
|
|
56
87
|
constructor(gateway: TypertGateway, options?: {
|
|
@@ -62,6 +93,7 @@ export declare class TaskBoardHostService {
|
|
|
62
93
|
sessionDefaultPermission?: TaskPermission;
|
|
63
94
|
maxSubtaskDepth?: number;
|
|
64
95
|
team?: TaskBoardTeamDispatcher;
|
|
96
|
+
timers?: HostTimerFace;
|
|
65
97
|
});
|
|
66
98
|
start(): void;
|
|
67
99
|
setConfiguration(active: boolean, preventIdleSleep: boolean): void;
|
|
@@ -84,7 +116,42 @@ export declare class TaskBoardHostService {
|
|
|
84
116
|
private pollSessions;
|
|
85
117
|
/** Reuse the session list this poll already fetched: one list RPC per tick, not 1 + E. */
|
|
86
118
|
private reconcileExecutions;
|
|
87
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Count one poll whose session history could not be read. A reader failure is
|
|
121
|
+
* not progress: the session is not running (the runner only reads history for
|
|
122
|
+
* one that is idle), so nothing will ever change that verdict. After
|
|
123
|
+
* {@link UNREADABLE_SETTLE_POLLS} consecutive polls the execution is reported
|
|
124
|
+
* failed with the recorded reason, instead of holding its card — and every
|
|
125
|
+
* ancestor of it — in the running column with no way out. The first poll of
|
|
126
|
+
* each streak is logged, so the Host log names the session.
|
|
127
|
+
*/
|
|
128
|
+
private noteUnreadableInspection;
|
|
129
|
+
/** Drop the armed schedule timer and forget its target. */
|
|
130
|
+
private clearScheduleTimer;
|
|
131
|
+
/**
|
|
132
|
+
* Boot / resume recovery: skip every occurrence that came due while the
|
|
133
|
+
* board was not running and roll each schedule to its next future target,
|
|
134
|
+
* then arm the timer for the nearest one. Rendering the occurrence is
|
|
135
|
+
* deliberately not attempted: the ACL of a card that fired hours ago is
|
|
136
|
+
* stale, and the board's own recovery contract is "missed triggers are
|
|
137
|
+
* skipped, never replayed".
|
|
138
|
+
*/
|
|
139
|
+
private recoverSchedule;
|
|
140
|
+
/**
|
|
141
|
+
* Arm the native timer at the nearest armed future trigger. One timer serves
|
|
142
|
+
* every schedule: the ledger's next target is the only instant the Host has
|
|
143
|
+
* to wake for. A target beyond the platform's timer ceiling re-arms in
|
|
144
|
+
* segments, and a target already past (the wall clock jumped, or the process
|
|
145
|
+
* was suspended) is handled immediately as a recovery.
|
|
146
|
+
*/
|
|
147
|
+
private armSchedule;
|
|
148
|
+
/**
|
|
149
|
+
* One armed target became due. A fire landing well past its target is a
|
|
150
|
+
* resume (suspend, throttle, forward clock jump) rather than a normal
|
|
151
|
+
* occurrence, so it takes the recovery path instead of launching a run for a
|
|
152
|
+
* long-stale instant.
|
|
153
|
+
*/
|
|
154
|
+
private onScheduleFire;
|
|
88
155
|
private armedSchedules;
|
|
89
156
|
/**
|
|
90
157
|
* Launch one run set. The root goes first and receives the run shape in its
|
|
@@ -96,7 +163,12 @@ export declare class TaskBoardHostService {
|
|
|
96
163
|
private dispatchRuns;
|
|
97
164
|
private scheduleLaunch;
|
|
98
165
|
private schedulePoll;
|
|
99
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Re-arm from the ledger's current targets. Callers that just changed a
|
|
168
|
+
* schedule (the host routes, the agent tools) invoke this after the write
|
|
169
|
+
* commits, so a new or edited trigger arms without waiting for the next fire.
|
|
170
|
+
*/
|
|
171
|
+
refreshSchedule(): void;
|
|
100
172
|
private syncPowerReasons;
|
|
101
173
|
private emit;
|
|
102
174
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
|
9
9
|
import z from '@deepseek-ai/schemastery';
|
|
10
10
|
import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
11
11
|
import type { LlmRuntime } from '@deepseek-ai/dsh-llm';
|
|
12
|
-
import { type TaskBoardTeamDispatcher } from './host-service.ts';
|
|
12
|
+
import { type HostTimerFace, type TaskBoardTeamDispatcher } from './host-service.ts';
|
|
13
13
|
import { type TaskPermission } from './core/tasks.ts';
|
|
14
14
|
/** Default environment variable holding the authenticated proxy token. */
|
|
15
15
|
export declare const DEFAULT_PROXY_TOKEN_ENV = "DSH_TASK_BOARD_PROXY_TOKEN";
|
|
@@ -139,6 +139,16 @@ interface AgentToolRegistry {
|
|
|
139
139
|
* @returns the registry, or undefined when this deployment serves none.
|
|
140
140
|
*/
|
|
141
141
|
export declare function resolveToolRegistry(ctx: Context): AgentToolRegistry | undefined;
|
|
142
|
+
/**
|
|
143
|
+
* Resolve the native cordis timer service (dsh-base's own `cordis-plugin-timer`
|
|
144
|
+
* row). The board arms its schedule through it so unloading the row clears
|
|
145
|
+
* every pending timer with the owning fiber, instead of the row tracking raw
|
|
146
|
+
* process handles by hand. A composition that serves no timer service keeps
|
|
147
|
+
* working on the process globals.
|
|
148
|
+
* @param ctx - the plugin context.
|
|
149
|
+
* @returns the timer face, or undefined when this deployment serves none.
|
|
150
|
+
*/
|
|
151
|
+
export declare function resolveHostTimers(ctx: Context): HostTimerFace | undefined;
|
|
142
152
|
/** The subset of the Agent Teams service this plugin drives. */
|
|
143
153
|
interface AgentTeamsFace {
|
|
144
154
|
spawnTeammate(caller: unknown, request: {
|
|
@@ -4,19 +4,50 @@
|
|
|
4
4
|
* the loader accepts a standalone install of the same package side by side;
|
|
5
5
|
* without this guard the second instance would still re-register the same
|
|
6
6
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
7
|
-
* and fail the boot. mountOnce makes
|
|
8
|
-
*
|
|
9
|
-
* package name in the client module host).
|
|
7
|
+
* and fail the boot. mountOnce makes a mount of an already-mounted package a
|
|
8
|
+
* no-op for as long as the first instance lives (the browser half is already
|
|
9
|
+
* deduped by package name in the client module host).
|
|
10
|
+
*
|
|
11
|
+
* A no-op is only safe while the holder is ALIVE. The holder can be disposed
|
|
12
|
+
* long after the refused mount ran its course: the Host reloads a profile by
|
|
13
|
+
* creating the new loader entries before the old ones are torn down (a
|
|
14
|
+
* plugin-manager enable/disable/install write, a settings-driven row reload,
|
|
15
|
+
* HMR), so the new aggregate shell entry mounts its family plugin while the
|
|
16
|
+
* previous entry still owns the name. Dropping that refused mount lost the
|
|
17
|
+
* plugin for good - the previous entry then disposed its own mount, releasing
|
|
18
|
+
* the name with nobody left to take it, and the family row stayed listed as
|
|
19
|
+
* active while its host routes 404ed (the task-board panel showed
|
|
20
|
+
* "board.hostError.notMounted", no degraded record appeared, and only a Host
|
|
21
|
+
* restart recovered it).
|
|
22
|
+
*
|
|
23
|
+
* The refused mount is therefore QUEUED, not dropped, and replayed the moment
|
|
24
|
+
* the holder releases the name - if the waiting fiber is still alive then. The
|
|
25
|
+
* single-instance guarantee is unchanged: exactly one mount is live per
|
|
26
|
+
* package name, and the replay re-enters the guard so a later mount still
|
|
27
|
+
* dedupes against it.
|
|
10
28
|
*
|
|
11
29
|
* The registry rides a global symbol so two module instances of the same
|
|
12
|
-
* package (npm copy vs repository link) still share one verdict.
|
|
13
|
-
*
|
|
30
|
+
* package (npm copy vs repository link) still share one verdict. That symbol
|
|
31
|
+
* is a CROSS-REPOSITORY contract, not this file's private state: the four
|
|
32
|
+
* satellite packages (dsh-skins / dsh-pet / dsh-presets /
|
|
33
|
+
* dsh-community-plugins) are separate repositories carrying their own copy of
|
|
34
|
+
* this guard, rebuilt on their own schedule, so the value under `MOUNTED`
|
|
35
|
+
* must keep the shape every published copy reads (a `Set` of package names
|
|
36
|
+
* with `has`/`add`/`delete`). The wait queues this guard added therefore live
|
|
37
|
+
* under their own additive key, and the registry reads back a `Set` even when
|
|
38
|
+
* some other build left a different value there. Changing `MOUNTED`'s shape
|
|
39
|
+
* in place broke that contract once: a satellite's legacy copy created a
|
|
40
|
+
* `Set`, the family's new copy read it as a `Map`, and every family row
|
|
41
|
+
* mounted after it failed with "claims.get is not a function".
|
|
42
|
+
*
|
|
43
|
+
* cordis `ctx.effect` runs its callback immediately and treats the callback's
|
|
14
44
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
15
45
|
*/
|
|
16
46
|
/**
|
|
17
47
|
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
18
|
-
* The first mount registers normally and
|
|
19
|
-
*
|
|
48
|
+
* The first mount registers normally and releases the name when its fiber
|
|
49
|
+
* disposes; a mount refused while that name is held waits for the release and
|
|
50
|
+
* then runs, unless its own fiber disposes first.
|
|
20
51
|
* @param packageName - npm package identity shared by every install source.
|
|
21
52
|
* @param fn - the original plugin apply.
|
|
22
53
|
* @returns an apply of the same shape.
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { TaskUpdatePatch } from './core/use-cases/task-update.ts';
|
|
2
2
|
import { type NewTaskInput, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts';
|
|
3
|
-
export declare const TASK_BOARD_SCHEMA_VERSION:
|
|
4
|
-
/** Ledger documents written before
|
|
5
|
-
export declare const TASK_BOARD_LEGACY_SCHEMA_VERSION:
|
|
3
|
+
export declare const TASK_BOARD_SCHEMA_VERSION: 4;
|
|
4
|
+
/** Ledger documents written before v4; loaded once and migrated on startup. */
|
|
5
|
+
export declare const TASK_BOARD_LEGACY_SCHEMA_VERSION: 3;
|
|
6
|
+
/** Ledger documents written before v3; migrated through the v3 normalization too. */
|
|
7
|
+
export declare const TASK_BOARD_OLDER_SCHEMA_VERSION: 2;
|
|
6
8
|
export declare const TASK_BOARD_API_PREFIX = "/api/task-board";
|
|
7
9
|
export type PowerPhase = 'disabled' | 'idle' | 'acquiring' | 'active' | 'error' | 'unsupported';
|
|
8
10
|
export interface TaskBoardPowerSnapshot {
|
|
@@ -90,12 +92,16 @@ export type TaskBoardAction = {
|
|
|
90
92
|
} | {
|
|
91
93
|
kind: 'restore';
|
|
92
94
|
taskId: string;
|
|
95
|
+
} | {
|
|
96
|
+
kind: 'settle';
|
|
97
|
+
taskId: string;
|
|
93
98
|
} | {
|
|
94
99
|
kind: 'set-schedule';
|
|
95
100
|
taskId: string;
|
|
96
101
|
patch: {
|
|
97
102
|
enabled?: boolean;
|
|
98
103
|
cron?: string;
|
|
104
|
+
timeZone?: string | null;
|
|
99
105
|
};
|
|
100
106
|
} | {
|
|
101
107
|
kind: 'run';
|