dsh-live-teams 0.1.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 +176 -0
- package/NOTICE +11 -0
- package/README.md +85 -0
- package/cordis.patch.yml +25 -0
- package/lib/binding.d.ts +18 -0
- package/lib/binding.js +42 -0
- package/lib/changed-paths.d.ts +26 -0
- package/lib/changed-paths.js +69 -0
- package/lib/client.js +6753 -0
- package/lib/command-queue.d.ts +60 -0
- package/lib/command-queue.js +185 -0
- package/lib/compatibility.js +109 -0
- package/lib/context-provider.d.ts +110 -0
- package/lib/context-provider.js +249 -0
- package/lib/dispatch.d.ts +174 -0
- package/lib/dispatch.js +624 -0
- package/lib/errors.d.ts +36 -0
- package/lib/errors.js +103 -0
- package/lib/git-artifacts.d.ts +50 -0
- package/lib/git-artifacts.js +242 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.js +14 -0
- package/lib/mailbox.d.ts +274 -0
- package/lib/mailbox.js +721 -0
- package/lib/member-tools.d.ts +57 -0
- package/lib/member-tools.js +1265 -0
- package/lib/migrations.d.ts +17 -0
- package/lib/migrations.js +47 -0
- package/lib/plugin.d.ts +106 -0
- package/lib/plugin.js +1003 -0
- package/lib/roles.d.ts +35 -0
- package/lib/roles.js +284 -0
- package/lib/routes.d.ts +586 -0
- package/lib/routes.js +2816 -0
- package/lib/scope.d.ts +62 -0
- package/lib/scope.js +133 -0
- package/lib/session-bridge.d.ts +76 -0
- package/lib/session-bridge.js +147 -0
- package/lib/session-title.js +35 -0
- package/lib/storage.d.ts +9 -0
- package/lib/storage.js +65 -0
- package/lib/task-store.d.ts +729 -0
- package/lib/task-store.js +2205 -0
- package/lib/team-store.d.ts +216 -0
- package/lib/team-store.js +765 -0
- package/lib/tree-snapshot.d.ts +28 -0
- package/lib/tree-snapshot.js +80 -0
- package/lib/types/client/TeamView.d.ts +26 -0
- package/lib/types/client/TeamView.dom.test.d.ts +1 -0
- package/lib/types/client/api.d.ts +522 -0
- package/lib/types/client/api.test.d.ts +1 -0
- package/lib/types/client/attention.d.ts +65 -0
- package/lib/types/client/attention.test.d.ts +1 -0
- package/lib/types/client/index.d.ts +31 -0
- package/lib/types/client/locales.d.ts +577 -0
- package/lib/types/client/member-name.d.ts +14 -0
- package/lib/types/client/member-name.test.d.ts +1 -0
- package/lib/types/client/roster.d.ts +26 -0
- package/lib/types/client/roster.test.d.ts +1 -0
- package/lib/types/client/styles.d.ts +3 -0
- package/package.json +104 -0
- package/roles/builder.md +40 -0
- package/roles/delegate.md +36 -0
- package/roles/lead.md +46 -0
- package/roles/oracle.md +36 -0
- package/roles/researcher.md +37 -0
- package/roles/reviewer.md +45 -0
- package/roles/scout.md +36 -0
- package/roles/verifier.md +36 -0
package/lib/scope.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/scope.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Declared task scope: what a task says it may touch, and how two declarations are
|
|
4
|
+
* compared. The comparison is deliberately conservative — it reports an overlap when
|
|
5
|
+
* a declaration *could* collide, because a missed overlap costs somebody's edits while
|
|
6
|
+
* a reported one costs throughput (B30).
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_PATHS = 32;
|
|
9
|
+
/**
|
|
10
|
+
* Work that writes code, and therefore needs the isolation Gate C has not built yet
|
|
11
|
+
* (ADR 0008; design §22), and the same set the parallel-write rules apply to. `work` is
|
|
12
|
+
* listed because a generic kind does not promise it is documentation — the safe reading
|
|
13
|
+
* of an unknown is "it writes", and the refusals name the kinds that are exempt.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CODING_KINDS: ReadonlySet<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Paths that every task effectively shares, so touching one collides with any active
|
|
18
|
+
* writer: manifests and lockfiles, repository internals, CI configuration and build
|
|
19
|
+
* entry points. The deployment may extend this list with `sharedPaths`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SHARED_PATH_DEFAULTS: readonly string[];
|
|
22
|
+
/** Normalise one entry, or refuse it. Returns undefined for an unusable value. */
|
|
23
|
+
export declare function normalizeScopePath(value: unknown): string | undefined;
|
|
24
|
+
/** Normalise a whole declaration; undefined when any entry is unusable, or on duplicates. */
|
|
25
|
+
export declare function normalizeScope(paths: unknown): string[] | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* The literal prefix a pattern can be reduced to: everything before the first segment
|
|
28
|
+
* that contains a wildcard. `src/**\/*.ts` reduces to `src`, `a/b*` to `a`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function literalPrefix(entry: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* True when the two entries could touch the same file. Segment-boundary prefixes only,
|
|
33
|
+
* so `a/b` and `a/bc` stay apart; a wildcard reduces to its literal prefix, which makes
|
|
34
|
+
* this report overlaps that a particular run would not have produced.
|
|
35
|
+
*/
|
|
36
|
+
export declare function scopesIntersect(left: string, right: string): boolean;
|
|
37
|
+
export type ScopeOverlap = {
|
|
38
|
+
left: string;
|
|
39
|
+
right: string;
|
|
40
|
+
shared: boolean;
|
|
41
|
+
};
|
|
42
|
+
export type ScopeConflict = {
|
|
43
|
+
kind: 'overlap';
|
|
44
|
+
left: string;
|
|
45
|
+
right: string;
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'shared';
|
|
48
|
+
left: string;
|
|
49
|
+
right: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Compare two declarations. Shared hotspots are reported separately from concrete
|
|
53
|
+
* overlaps: the lead's action differs — one is "narrow the scope or serialise the two
|
|
54
|
+
* tasks", the other is "these files are shared by everything".
|
|
55
|
+
*/
|
|
56
|
+
export declare function scopeConflict(left: readonly string[], right: readonly string[], shared?: readonly string[]): ScopeConflict | undefined;
|
|
57
|
+
/** Changed paths that fall outside a declaration. */
|
|
58
|
+
export declare function outsideScope(changed: readonly string[], declared: readonly string[]): string[];
|
|
59
|
+
/** Two declarations that a reviewer can compare without re-reading the contract. */
|
|
60
|
+
export declare function scopeSummary(paths: readonly string[] | undefined): string;
|
|
61
|
+
export declare function resolveWorkspacePath(workspacePath: string, entry: string): string;
|
|
62
|
+
//#endregion
|
package/lib/scope.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
//#region src/scope.ts
|
|
3
|
+
/**
|
|
4
|
+
* Declared task scope: what a task says it may touch, and how two declarations are
|
|
5
|
+
* compared. The comparison is deliberately conservative — it reports an overlap when
|
|
6
|
+
* a declaration *could* collide, because a missed overlap costs somebody's edits while
|
|
7
|
+
* a reported one costs throughput (B30).
|
|
8
|
+
*/
|
|
9
|
+
const MAX_PATHS = 32;
|
|
10
|
+
/**
|
|
11
|
+
* Work that writes code, and therefore needs the isolation Gate C has not built yet
|
|
12
|
+
* (ADR 0008; design §22), and the same set the parallel-write rules apply to. `work` is
|
|
13
|
+
* listed because a generic kind does not promise it is documentation — the safe reading
|
|
14
|
+
* of an unknown is "it writes", and the refusals name the kinds that are exempt.
|
|
15
|
+
*/
|
|
16
|
+
const CODING_KINDS = /* @__PURE__ */ new Set([
|
|
17
|
+
"implementation",
|
|
18
|
+
"repair",
|
|
19
|
+
"integration",
|
|
20
|
+
"work"
|
|
21
|
+
]);
|
|
22
|
+
const GLOB_CHARS = /[*?[\]{}!()]/;
|
|
23
|
+
/**
|
|
24
|
+
* Paths that every task effectively shares, so touching one collides with any active
|
|
25
|
+
* writer: manifests and lockfiles, repository internals, CI configuration and build
|
|
26
|
+
* entry points. The deployment may extend this list with `sharedPaths`.
|
|
27
|
+
*/
|
|
28
|
+
const SHARED_PATH_DEFAULTS = Object.freeze([
|
|
29
|
+
"package.json",
|
|
30
|
+
"package-lock.json",
|
|
31
|
+
"pnpm-lock.yaml",
|
|
32
|
+
"pnpm-workspace.yaml",
|
|
33
|
+
"yarn.lock",
|
|
34
|
+
"pyproject.toml",
|
|
35
|
+
"requirements.txt",
|
|
36
|
+
"Cargo.toml",
|
|
37
|
+
"Cargo.lock",
|
|
38
|
+
"go.mod",
|
|
39
|
+
"go.sum",
|
|
40
|
+
".git/**",
|
|
41
|
+
".github/**",
|
|
42
|
+
".gitlab-ci.yml",
|
|
43
|
+
"Jenkinsfile",
|
|
44
|
+
"Dockerfile",
|
|
45
|
+
"docker-compose.yml",
|
|
46
|
+
"docker-compose.yaml",
|
|
47
|
+
"Makefile"
|
|
48
|
+
]);
|
|
49
|
+
/** Normalise one entry, or refuse it. Returns undefined for an unusable value. */
|
|
50
|
+
function normalizeScopePath(value) {
|
|
51
|
+
if (typeof value !== "string") return void 0;
|
|
52
|
+
const trimmed = value.trim();
|
|
53
|
+
if (trimmed.length === 0 || trimmed.length > 512) return void 0;
|
|
54
|
+
const slashed = trimmed.replace(/\\/gu, "/");
|
|
55
|
+
if (slashed.startsWith("/") || /^[A-Za-z]:/u.test(slashed)) return void 0;
|
|
56
|
+
const segments = slashed.split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
57
|
+
if (segments.length === 0) return void 0;
|
|
58
|
+
if (segments.some((segment) => segment === "..")) return void 0;
|
|
59
|
+
return segments.join("/");
|
|
60
|
+
}
|
|
61
|
+
/** Normalise a whole declaration; undefined when any entry is unusable, or on duplicates. */
|
|
62
|
+
function normalizeScope(paths) {
|
|
63
|
+
if (!Array.isArray(paths)) return void 0;
|
|
64
|
+
if (paths.length === 0 || paths.length > 32) return void 0;
|
|
65
|
+
const normalized = [];
|
|
66
|
+
for (const entry of paths) {
|
|
67
|
+
const value = normalizeScopePath(entry);
|
|
68
|
+
if (value === void 0) return void 0;
|
|
69
|
+
if (normalized.includes(value)) return void 0;
|
|
70
|
+
normalized.push(value);
|
|
71
|
+
}
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The literal prefix a pattern can be reduced to: everything before the first segment
|
|
76
|
+
* that contains a wildcard. `src/**\/*.ts` reduces to `src`, `a/b*` to `a`.
|
|
77
|
+
*/
|
|
78
|
+
function literalPrefix(entry) {
|
|
79
|
+
const segments = entry.split("/");
|
|
80
|
+
const first = segments.findIndex((segment) => GLOB_CHARS.test(segment));
|
|
81
|
+
return (first < 0 ? segments : segments.slice(0, first)).join("/");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* True when the two entries could touch the same file. Segment-boundary prefixes only,
|
|
85
|
+
* so `a/b` and `a/bc` stay apart; a wildcard reduces to its literal prefix, which makes
|
|
86
|
+
* this report overlaps that a particular run would not have produced.
|
|
87
|
+
*/
|
|
88
|
+
function scopesIntersect(left, right) {
|
|
89
|
+
if (left.length === 0 || right.length === 0) return true;
|
|
90
|
+
if (left === right) return true;
|
|
91
|
+
return left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Compare two declarations. Shared hotspots are reported separately from concrete
|
|
95
|
+
* overlaps: the lead's action differs — one is "narrow the scope or serialise the two
|
|
96
|
+
* tasks", the other is "these files are shared by everything".
|
|
97
|
+
*/
|
|
98
|
+
function scopeConflict(left, right, shared = SHARED_PATH_DEFAULTS) {
|
|
99
|
+
const reduce = (entry) => literalPrefix(entry);
|
|
100
|
+
const overlaps = (entry, other) => scopesIntersect(reduce(entry), reduce(other));
|
|
101
|
+
let sharedHit;
|
|
102
|
+
for (const a of left) for (const b of right) {
|
|
103
|
+
if (!overlaps(a, b)) continue;
|
|
104
|
+
if (!shared.some((candidate) => overlaps(a, candidate) || overlaps(b, candidate))) return {
|
|
105
|
+
kind: "overlap",
|
|
106
|
+
left: a,
|
|
107
|
+
right: b
|
|
108
|
+
};
|
|
109
|
+
sharedHit ??= {
|
|
110
|
+
kind: "shared",
|
|
111
|
+
left: a,
|
|
112
|
+
right: b
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return sharedHit;
|
|
116
|
+
}
|
|
117
|
+
/** Changed paths that fall outside a declaration. */
|
|
118
|
+
function outsideScope(changed, declared) {
|
|
119
|
+
return changed.filter((candidate) => {
|
|
120
|
+
const normalized = normalizeScopePath(candidate);
|
|
121
|
+
if (normalized === void 0) return false;
|
|
122
|
+
return !declared.some((entry) => scopesIntersect(literalPrefix(entry), normalized));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** Two declarations that a reviewer can compare without re-reading the contract. */
|
|
126
|
+
function scopeSummary(paths) {
|
|
127
|
+
return paths === void 0 || paths.length === 0 ? "not declared" : paths.join(", ");
|
|
128
|
+
}
|
|
129
|
+
function resolveWorkspacePath(workspacePath, entry) {
|
|
130
|
+
return path.join(workspacePath, ...entry.split("/"));
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { CODING_KINDS, MAX_PATHS, SHARED_PATH_DEFAULTS, literalPrefix, normalizeScope, normalizeScopePath, outsideScope, resolveWorkspacePath, scopeConflict, scopeSummary, scopesIntersect };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { LiveTeamsError } from "./errors.js";
|
|
2
|
+
//#region src/session-bridge.d.ts
|
|
3
|
+
export interface SessionDescriptor {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
workspaceId: string | undefined;
|
|
6
|
+
cwd: string | undefined;
|
|
7
|
+
title: string | undefined;
|
|
8
|
+
status: 'idle' | 'running' | 'unknown';
|
|
9
|
+
live: boolean;
|
|
10
|
+
subagentOwned: boolean;
|
|
11
|
+
seq: number;
|
|
12
|
+
agentPreset: string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* DSH's Session-list projection keeps a Session blank until its first
|
|
15
|
+
* `turn/start`, and the session list hides blank Sessions: the Workspace
|
|
16
|
+
* browser adopts one as its provisional "New session" row and never lists it
|
|
17
|
+
* (`sessionVisible` / `connectWorkspace` in @deepseek-ai/dsh-client-ui-workspace).
|
|
18
|
+
*
|
|
19
|
+
* Only `list` reports this flag, so `undefined` means "not reported", never
|
|
20
|
+
* "known to be visible".
|
|
21
|
+
*/
|
|
22
|
+
blank?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface SessionController {
|
|
25
|
+
create(payload: Record<string, unknown>): Promise<unknown>;
|
|
26
|
+
inspect(sessionId: string): Promise<unknown>;
|
|
27
|
+
list?(request: Record<string, unknown>, signal?: AbortSignal): Promise<unknown>;
|
|
28
|
+
rename(payload: {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
title: string;
|
|
31
|
+
}): Promise<unknown>;
|
|
32
|
+
prompt(payload: {
|
|
33
|
+
requestId: string;
|
|
34
|
+
sessionId: string;
|
|
35
|
+
mode: 'queue' | 'steer';
|
|
36
|
+
content: readonly unknown[];
|
|
37
|
+
}, signal: AbortSignal): Promise<unknown>;
|
|
38
|
+
cancel(payload: {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
}): void;
|
|
41
|
+
}
|
|
42
|
+
export interface SessionBridge {
|
|
43
|
+
create(request: {
|
|
44
|
+
sessionId?: string;
|
|
45
|
+
workspaceId?: string;
|
|
46
|
+
cwd?: string;
|
|
47
|
+
agentPreset?: string;
|
|
48
|
+
}): Promise<SessionDescriptor>;
|
|
49
|
+
inspect(sessionId: string): Promise<SessionDescriptor | undefined>;
|
|
50
|
+
validateAttachment(sessionId: string, workspaceId: string): Promise<SessionDescriptor>;
|
|
51
|
+
rename(sessionId: string, title: string): Promise<{
|
|
52
|
+
title: string;
|
|
53
|
+
seq: number | undefined;
|
|
54
|
+
}>;
|
|
55
|
+
selectModel(sessionId: string, selection: unknown): never;
|
|
56
|
+
deliver(sessionId: string, envelope: {
|
|
57
|
+
requestId: string;
|
|
58
|
+
mode: 'queue' | 'steer';
|
|
59
|
+
content: readonly unknown[];
|
|
60
|
+
}): Promise<{
|
|
61
|
+
accepted: true;
|
|
62
|
+
}>;
|
|
63
|
+
interrupt(sessionId: string, reason: string): Promise<{
|
|
64
|
+
accepted: true;
|
|
65
|
+
}>;
|
|
66
|
+
reconcile(sessionIds: readonly string[]): Promise<SessionDescriptor[]>;
|
|
67
|
+
}
|
|
68
|
+
export declare function toBridgeError(error: unknown): LiveTeamsError;
|
|
69
|
+
export interface SessionBridgeDeps {
|
|
70
|
+
controller: SessionController | null | undefined;
|
|
71
|
+
agents?: {
|
|
72
|
+
get?: (sessionId: string) => unknown;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export declare function createSessionBridge({ controller, agents }: SessionBridgeDeps): SessionBridge;
|
|
76
|
+
//#endregion
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { LiveTeamsError, translatePlatformError } from "./errors.js";
|
|
2
|
+
//#region src/session-bridge.ts
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
5
|
+
}
|
|
6
|
+
function text(value) {
|
|
7
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
8
|
+
}
|
|
9
|
+
function isSubagentHeader(header) {
|
|
10
|
+
const value = record(header);
|
|
11
|
+
return value.origin === "subagent" || text(value.parentSession) !== void 0;
|
|
12
|
+
}
|
|
13
|
+
function isMissingSession(error) {
|
|
14
|
+
const value = record(error);
|
|
15
|
+
if (value.code === "session/not-found") return true;
|
|
16
|
+
if (typeof value.code === "string" && value.code.length > 0) return false;
|
|
17
|
+
return /not found/i.test(String(value.message ?? ""));
|
|
18
|
+
}
|
|
19
|
+
function isSubagentOwnership(error) {
|
|
20
|
+
const value = record(error);
|
|
21
|
+
if (value.code !== "session/agent-busy") return false;
|
|
22
|
+
const details = record(value.details);
|
|
23
|
+
return /subagent/i.test(String(details.reason ?? value.message ?? ""));
|
|
24
|
+
}
|
|
25
|
+
function toBridgeError(error) {
|
|
26
|
+
if (error instanceof LiveTeamsError) return error;
|
|
27
|
+
const translated = translatePlatformError(error);
|
|
28
|
+
if (!isSubagentOwnership(error)) return translated;
|
|
29
|
+
return new LiveTeamsError("CAPABILITY_UNAVAILABLE", "Session is owned by subagent routing and cannot be a team member", translated.auditDetail === void 0 ? { cause: error } : {
|
|
30
|
+
cause: error,
|
|
31
|
+
auditDetail: translated.auditDetail
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function descriptorOf(inspection, liveAgent) {
|
|
35
|
+
const liveSession = record(record(liveAgent).session);
|
|
36
|
+
const meta = record(inspection.meta ?? inspection.header ?? liveSession.header);
|
|
37
|
+
const session = record(liveAgent).session;
|
|
38
|
+
const ownerHeader = record(session).header ?? meta;
|
|
39
|
+
const events = Array.isArray(inspection.events) ? inspection.events : [];
|
|
40
|
+
let title = text(meta.title);
|
|
41
|
+
let seq = typeof meta.seq === "number" ? meta.seq : 0;
|
|
42
|
+
for (const event of events) {
|
|
43
|
+
const value = record(event);
|
|
44
|
+
if (typeof value.seq === "number" && value.seq > seq) seq = value.seq;
|
|
45
|
+
if (value.type === "session/title") title = text(record(value.data).title) ?? title;
|
|
46
|
+
}
|
|
47
|
+
const statusValue = record(liveAgent).status;
|
|
48
|
+
return {
|
|
49
|
+
sessionId: text(meta.id) ?? "",
|
|
50
|
+
workspaceId: text(meta.workspaceId),
|
|
51
|
+
cwd: text(meta.cwd),
|
|
52
|
+
title,
|
|
53
|
+
status: statusValue === "idle" || statusValue === "running" ? statusValue : "unknown",
|
|
54
|
+
live: liveAgent !== void 0 && liveAgent !== null,
|
|
55
|
+
subagentOwned: isSubagentHeader(ownerHeader),
|
|
56
|
+
seq,
|
|
57
|
+
agentPreset: text(meta.agentPreset)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function createSessionBridge({ controller, agents }) {
|
|
61
|
+
if (controller === null || typeof controller !== "object") throw new LiveTeamsError("CAPABILITY_UNAVAILABLE", "sessionController is not available in this composition");
|
|
62
|
+
const liveAgentOf = (sessionId) => agents?.get?.(sessionId);
|
|
63
|
+
const bridge = {
|
|
64
|
+
async create(request) {
|
|
65
|
+
const sessionId = text(request?.sessionId);
|
|
66
|
+
if (sessionId === void 0) throw new LiveTeamsError("CAPABILITY_UNAVAILABLE", "create requires an explicit plugin-owned sessionId");
|
|
67
|
+
const payload = { sessionId };
|
|
68
|
+
if (text(request.workspaceId) !== void 0) payload.workspaceId = request.workspaceId;
|
|
69
|
+
if (text(request.cwd) !== void 0) payload.cwd = request.cwd;
|
|
70
|
+
if (text(request.agentPreset) !== void 0) payload.agentPreset = request.agentPreset;
|
|
71
|
+
try {
|
|
72
|
+
await controller.create(payload);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw toBridgeError(error);
|
|
75
|
+
}
|
|
76
|
+
const descriptor = await bridge.inspect(sessionId);
|
|
77
|
+
if (descriptor === void 0) throw new LiveTeamsError("SESSION_NOT_FOUND", `Session "${sessionId}" was not readable after create`, { auditDetail: "create succeeded but inspect could not observe the Session" });
|
|
78
|
+
return descriptor;
|
|
79
|
+
},
|
|
80
|
+
async inspect(sessionId) {
|
|
81
|
+
try {
|
|
82
|
+
return descriptorOf(record(await controller.inspect(sessionId)), liveAgentOf(sessionId));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (isMissingSession(error)) return void 0;
|
|
85
|
+
throw toBridgeError(error);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async validateAttachment(sessionId, workspaceId) {
|
|
89
|
+
const descriptor = await bridge.inspect(sessionId);
|
|
90
|
+
if (descriptor === void 0) throw new LiveTeamsError("SESSION_NOT_FOUND", `Session "${sessionId}" does not exist`, { auditDetail: "validateAttachment: inspect returned no Session" });
|
|
91
|
+
if (descriptor.subagentOwned) throw new LiveTeamsError("CAPABILITY_UNAVAILABLE", `Session "${sessionId}" is owned by subagent routing`, { auditDetail: "validateAttachment: subagent-owned Session cannot be a member" });
|
|
92
|
+
const expected = text(workspaceId);
|
|
93
|
+
if (expected !== void 0 && descriptor.workspaceId !== expected) throw new LiveTeamsError("SESSION_WRONG_WORKSPACE", `Session "${sessionId}" is not attached to workspace "${expected}"`, { auditDetail: `observed workspaceId=${String(descriptor.workspaceId)}` });
|
|
94
|
+
return descriptor;
|
|
95
|
+
},
|
|
96
|
+
async rename(sessionId, title) {
|
|
97
|
+
try {
|
|
98
|
+
const result = record(await controller.rename({
|
|
99
|
+
sessionId,
|
|
100
|
+
title
|
|
101
|
+
}));
|
|
102
|
+
return {
|
|
103
|
+
title: text(result.title) ?? title,
|
|
104
|
+
seq: typeof result.seq === "number" ? result.seq : void 0
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw toBridgeError(error);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
selectModel() {
|
|
111
|
+
throw new LiveTeamsError("CAPABILITY_UNAVAILABLE", "Model selection is intentionally unavailable in this build", { auditDetail: "Phase 2 policy omits member model selection because selectModel also rewrites the deployment-wide default model. This must not be worked around silently." });
|
|
112
|
+
},
|
|
113
|
+
async deliver(sessionId, envelope) {
|
|
114
|
+
if (envelope.mode !== "queue" && envelope.mode !== "steer") throw new LiveTeamsError("DELIVERY_REJECTED", `Unsupported delivery mode "${String(envelope.mode)}"`);
|
|
115
|
+
try {
|
|
116
|
+
await controller.prompt({
|
|
117
|
+
requestId: envelope.requestId,
|
|
118
|
+
sessionId,
|
|
119
|
+
mode: envelope.mode,
|
|
120
|
+
content: envelope.content
|
|
121
|
+
}, new AbortController().signal);
|
|
122
|
+
return { accepted: true };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw toBridgeError(error);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
async interrupt(sessionId, _reason) {
|
|
128
|
+
try {
|
|
129
|
+
controller.cancel({ sessionId });
|
|
130
|
+
return { accepted: true };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw toBridgeError(error);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
async reconcile(sessionIds) {
|
|
136
|
+
const descriptors = [];
|
|
137
|
+
for (const sessionId of sessionIds) {
|
|
138
|
+
const descriptor = await bridge.inspect(sessionId);
|
|
139
|
+
if (descriptor !== void 0) descriptors.push(descriptor);
|
|
140
|
+
}
|
|
141
|
+
return descriptors;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
return Object.freeze(bridge);
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
export { createSessionBridge, toBridgeError };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/session-title.ts
|
|
2
|
+
/** The one place the title format is defined. */
|
|
3
|
+
function sessionTitleFor(displayName, teamName) {
|
|
4
|
+
return `${displayName} - ${teamName}`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Bring one Session's title in line with its membership.
|
|
8
|
+
*
|
|
9
|
+
* @returns the outcome, or `undefined` when the title could not be set — the
|
|
10
|
+
* caller must treat that as a reported inconvenience, not as a failure.
|
|
11
|
+
*/
|
|
12
|
+
async function applySessionTitle(dependencies, request) {
|
|
13
|
+
const to = sessionTitleFor(request.displayName, request.teamName);
|
|
14
|
+
const from = request.previousTitle ?? null;
|
|
15
|
+
if (request.previousTitle === to) return {
|
|
16
|
+
from,
|
|
17
|
+
to,
|
|
18
|
+
reason: request.reason,
|
|
19
|
+
renamed: false
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
await dependencies.rename(request.sessionId, to);
|
|
23
|
+
return {
|
|
24
|
+
from,
|
|
25
|
+
to,
|
|
26
|
+
reason: request.reason,
|
|
27
|
+
renamed: true
|
|
28
|
+
};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
dependencies.warn?.(`live-teams: could not title Session "${request.sessionId}": ${String(error)}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { applySessionTitle, sessionTitleFor };
|
package/lib/storage.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/storage.d.ts
|
|
2
|
+
export declare function withProcessLock<T>(key: string, operation: () => Promise<T>): Promise<T>;
|
|
3
|
+
export declare function atomicWrite(filePath: string, contents: string | Uint8Array): Promise<void>;
|
|
4
|
+
export declare function atomicWriteJson(filePath: string, value: unknown): Promise<void>;
|
|
5
|
+
export declare function readJson(filePath: string): Promise<unknown>;
|
|
6
|
+
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
7
|
+
export declare function clone<T>(value: T): T;
|
|
8
|
+
export declare const CURRENT_SCHEMA_VERSION = 1;
|
|
9
|
+
//#endregion
|
package/lib/storage.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { promises } from "node:fs";
|
|
3
|
+
//#region src/storage.ts
|
|
4
|
+
let temporaryFileCounter = 0;
|
|
5
|
+
const locks = /* @__PURE__ */ new Map();
|
|
6
|
+
async function withProcessLock(key, operation) {
|
|
7
|
+
const previous = locks.get(key) ?? Promise.resolve();
|
|
8
|
+
let release;
|
|
9
|
+
const current = new Promise((resolve) => {
|
|
10
|
+
release = resolve;
|
|
11
|
+
});
|
|
12
|
+
locks.set(key, current);
|
|
13
|
+
await previous;
|
|
14
|
+
try {
|
|
15
|
+
return await operation();
|
|
16
|
+
} finally {
|
|
17
|
+
release();
|
|
18
|
+
if (locks.get(key) === current) locks.delete(key);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function atomicWrite(filePath, contents) {
|
|
22
|
+
const directory = path.dirname(filePath);
|
|
23
|
+
await promises.mkdir(directory, { recursive: true });
|
|
24
|
+
const basename = path.basename(filePath);
|
|
25
|
+
const temporaryPath = path.join(directory, `.${basename}.${process.pid}.${Date.now()}.${temporaryFileCounter++}.tmp`);
|
|
26
|
+
let handle;
|
|
27
|
+
try {
|
|
28
|
+
handle = await promises.open(temporaryPath, "wx", 384);
|
|
29
|
+
await handle.writeFile(contents);
|
|
30
|
+
await handle.sync();
|
|
31
|
+
await handle.close();
|
|
32
|
+
handle = void 0;
|
|
33
|
+
await promises.rename(temporaryPath, filePath);
|
|
34
|
+
const directoryHandle = await promises.open(directory, "r");
|
|
35
|
+
try {
|
|
36
|
+
await directoryHandle.sync();
|
|
37
|
+
} finally {
|
|
38
|
+
await directoryHandle.close();
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
if (handle !== void 0) await handle.close().catch(() => void 0);
|
|
42
|
+
await promises.unlink(temporaryPath).catch(() => void 0);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function atomicWriteJson(filePath, value) {
|
|
46
|
+
await atomicWrite(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
47
|
+
}
|
|
48
|
+
async function readJson(filePath) {
|
|
49
|
+
return JSON.parse(await promises.readFile(filePath, "utf8"));
|
|
50
|
+
}
|
|
51
|
+
async function fileExists(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
await promises.access(filePath);
|
|
54
|
+
return true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error.code === "ENOENT") return false;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function clone(value) {
|
|
61
|
+
return value === void 0 ? value : JSON.parse(JSON.stringify(value));
|
|
62
|
+
}
|
|
63
|
+
const CURRENT_SCHEMA_VERSION = 1;
|
|
64
|
+
//#endregion
|
|
65
|
+
export { CURRENT_SCHEMA_VERSION, atomicWrite, atomicWriteJson, clone, fileExists, readJson, withProcessLock };
|